update
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -1,29 +1,33 @@
|
|||||||
// Command dbtool 是后台管理用的数据库导出脚本。
|
// Command dbtool 是后台管理用的数据库导出 / 导入脚本。
|
||||||
//
|
//
|
||||||
// 用途:把一个 PostgreSQL 库(fashion)的「结构 + 索引 + 约束 + 数据」导出为
|
// 用途:把一个 PostgreSQL 库(fashion)的「结构 + 索引 + 约束 + 数据」导出为
|
||||||
// **单个纯 SQL 文件**,拷到另一台机器后直接用 psql 灌入即可 —— 无需 pg_dump。
|
// **单个纯 SQL 文件**,拷到另一台机器后用 dbtool 或 psql 灌入即可 —— 无需 pg_dump。
|
||||||
//
|
//
|
||||||
// dbtool dump [-config <yml>] [-out <file.sql>] [-with-extension]
|
// dbtool dump [-config <yml>] [-out <file.sql>] [-with-extension] 导出
|
||||||
|
// dbtool import [-config <yml>] [-in <file.sql>] 导入
|
||||||
//
|
//
|
||||||
// 导入方式(目标机器):
|
// 导入(目标机器,二选一):
|
||||||
//
|
//
|
||||||
|
// dbtool import -in db_dump.sql
|
||||||
// psql -U fashion -d fashion -v ON_ERROR_STOP=1 -f db_dump.sql
|
// psql -U fashion -d fashion -v ON_ERROR_STOP=1 -f db_dump.sql
|
||||||
//
|
//
|
||||||
// 结构从哪来:**唯一入口就是本工具的 dump 输出**。项目已不再在服务启动时自动迁移表结构
|
// 结构从哪来:**唯一入口就是本工具的 dump 输出**。项目已不再在服务启动时自动迁移表结构
|
||||||
// (原 database.AutoMigrate / EnsureDedupSchema 已移除),重建一个库就是「dump 出来再灌进去」。
|
// (原 database.AutoMigrate / EnsureDedupSchema 已移除),重建一个库就是「dump 出来再灌进去」。
|
||||||
//
|
//
|
||||||
// 为什么没有 import 子命令:数据段用的是 SQL 标准的 COPY ... FROM stdin,它依赖 PostgreSQL
|
// 数据为什么用 INSERT 而不是 COPY:pg_dump 用的是 COPY ... FROM stdin,它依赖 PostgreSQL
|
||||||
// 前端的 copy 子协议,Go 的 database/sql 无法执行这类脚本。导入统一交给 psql。
|
// 前端的 copy 子协议,而 pgx 底层(pgconn)在简单查询协议的多语句执行中并不处理
|
||||||
|
// CopyInResponse(源码中无该分支),Go 侧无法执行含 COPY 的脚本。改用多行 INSERT 后,
|
||||||
|
// 同一份文件既能被 dbtool import 执行,也能被 psql 执行。
|
||||||
//
|
//
|
||||||
// 与 pg_dump 的关系:本工具不依赖任何外部二进制,等于把「读系统目录 → 拼 DDL → 导数据」
|
// 与 pg_dump 的关系:本工具不依赖任何外部二进制,等于把「读系统目录 → 拼 DDL → 导数据」
|
||||||
// 自己实现一遍,因此**只覆盖本项目实际用到的对象**:
|
// 自己实现一遍,因此**只覆盖本项目实际用到的对象**:
|
||||||
//
|
//
|
||||||
// 表、列(类型含修饰符 / 默认值 / identity / NOT NULL)、主键、索引(含 pgvector 的 HNSW)、
|
// 表、列(类型含修饰符 / 默认值 / identity / NOT NULL)、主键、索引(含 pgvector 的 HNSW)、
|
||||||
// 数据(COPY)、序列当前值。
|
// 数据、序列当前值。
|
||||||
//
|
//
|
||||||
// 视图 / 触发器 / 外键 / 注释 / 权限不导出(当前库中也不存在)。
|
// 视图 / 触发器 / 外键 / 注释 / 权限不导出(当前库中也不存在)。
|
||||||
//
|
//
|
||||||
// 前置条件:目标库必须已启用 pgvector 扩展,否则 vector(64) 列建不出来。
|
// 前置条件:目标库必须为空,且已启用 pgvector 扩展,否则 vector(64) 列建不出来。
|
||||||
// 默认**不**导出扩展语句(可用 -with-extension 带上)。若目标容器由 scripts/pgvector 的
|
// 默认**不**导出扩展语句(可用 -with-extension 带上)。若目标容器由 scripts/pgvector 的
|
||||||
// compose 启动,initdb/01-extensions.sql 会在数据卷首次初始化时自动创建扩展。
|
// compose 启动,initdb/01-extensions.sql 会在数据卷首次初始化时自动创建扩展。
|
||||||
//
|
//
|
||||||
@ -32,6 +36,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -40,12 +45,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
"github.com/jackc/pgx/v5/stdlib"
|
||||||
|
|
||||||
"fashionapi/internal/config"
|
"fashionapi/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// column 是单列的结构信息,用于生成 CREATE TABLE、COPY 列清单与序列重置语句。
|
// column 是单列的结构信息,用于生成 CREATE TABLE、INSERT 列清单与序列重置语句。
|
||||||
type column struct {
|
type column struct {
|
||||||
Name string
|
Name string
|
||||||
// Type 为 format_type(atttypid, atttypmod) 的结果,**含**类型修饰符:
|
// Type 为 format_type(atttypid, atttypmod) 的结果,**含**类型修饰符:
|
||||||
@ -67,6 +72,10 @@ type column struct {
|
|||||||
AutoIncrement bool
|
AutoIncrement bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insertBatchRows 单条 INSERT 里最多写多少行。
|
||||||
|
// 批量写可以让文件更紧凑、导入更快;值都是字面量而非绑定参数,不受参数个数上限限制。
|
||||||
|
const insertBatchRows = 100
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
usage()
|
usage()
|
||||||
@ -75,6 +84,8 @@ func main() {
|
|||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "dump":
|
case "dump":
|
||||||
runDump(os.Args[2:])
|
runDump(os.Args[2:])
|
||||||
|
case "import":
|
||||||
|
runImport(os.Args[2:])
|
||||||
default:
|
default:
|
||||||
usage()
|
usage()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@ -82,20 +93,20 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
fmt.Println(`dbtool - 后台数据库导出脚本(PostgreSQL)
|
fmt.Println(`dbtool - 后台数据库导出 / 导入脚本(PostgreSQL)
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
dbtool dump [-config <yml>] [-out <file>] [-with-extension]
|
dbtool dump [-config <yml>] [-out <file>] [-with-extension]
|
||||||
导出「结构 + 主键 / 索引 + 数据」为单个纯 SQL 文件
|
导出「结构 + 主键 / 索引 + 数据」为单个纯 SQL 文件
|
||||||
|
dbtool import [-config <yml>] [-in <file>]
|
||||||
导入(目标机器):
|
把 dump 出来的 SQL 文件灌入目标库
|
||||||
psql -U fashion -d fashion -v ON_ERROR_STOP=1 -f <file>
|
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
连接信息读取 configs/config.yml 的 database 段(可用 -config 覆盖)。
|
连接信息读取 configs/config.yml 的 database 段(可用 -config 覆盖)。
|
||||||
-with-extension 会在文件开头加 CREATE EXTENSION IF NOT EXISTS vector;
|
-with-extension 会在导出文件开头加 CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
默认不加,此时目标库须已启用 pgvector,否则 vector 列建不出来。
|
默认不加,此时目标库须已启用 pgvector,否则 vector 列建不出来。
|
||||||
目标库必须是**空库**(脚本按 CREATE TABLE IF NOT EXISTS + COPY 写入,不会清表)。`)
|
目标库必须是**空库**(脚本按 CREATE TABLE IF NOT EXISTS + INSERT 写入,不会清表)。
|
||||||
|
import 等价于 psql -v ON_ERROR_STOP=1 -f;整个脚本在一个隐式事务里执行,出错整体回滚。`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// openDB 按 config 加载 DSN 并探活。
|
// openDB 按 config 加载 DSN 并探活。
|
||||||
@ -139,11 +150,7 @@ func runDump(args []string) {
|
|||||||
defer f.Close()
|
defer f.Close()
|
||||||
w := bufio.NewWriter(f)
|
w := bufio.NewWriter(f)
|
||||||
|
|
||||||
writeHeader(w, dcfg)
|
writeHeader(w, dcfg, *withExt)
|
||||||
if *withExt {
|
|
||||||
fmt.Fprintln(w, "CREATE EXTENSION IF NOT EXISTS vector;")
|
|
||||||
fmt.Fprintln(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
tables, err := listTables(db)
|
tables, err := listTables(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -171,7 +178,7 @@ func runDump(args []string) {
|
|||||||
fmt.Fprintln(w, "-- ==================== 数据 ====================")
|
fmt.Fprintln(w, "-- ==================== 数据 ====================")
|
||||||
var total int64
|
var total int64
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
n, err := writeCopyData(w, db, t, cols[t])
|
n, err := writeInsertData(w, db, t, cols[t])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("✗ 导出 %s 数据失败: %v", t, err)
|
log.Fatalf("✗ 导出 %s 数据失败: %v", t, err)
|
||||||
}
|
}
|
||||||
@ -190,7 +197,7 @@ func runDump(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ④ 序列当前值:COPY 写了显式 id,不会推进序列,必须手工推到 MAX+1。
|
// ④ 序列当前值:INSERT 写了显式 id,不会推进序列,必须手工推到 MAX+1。
|
||||||
fmt.Fprintln(w, "-- ==================== 序列当前值 ====================")
|
fmt.Fprintln(w, "-- ==================== 序列当前值 ====================")
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
writeSequenceResets(w, t, cols[t])
|
writeSequenceResets(w, t, cols[t])
|
||||||
@ -202,18 +209,73 @@ func runDump(args []string) {
|
|||||||
log.Printf("✓ 已导出 %d 张表、%d 行 -> %s", len(tables), total, *out)
|
log.Printf("✓ 已导出 %d 张表、%d 行 -> %s", len(tables), total, *out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHeader 写文件头与几个影响字面量解析的会话设置。
|
// runImport 把 dump 出来的 SQL 文件整体灌入目标库。
|
||||||
func writeHeader(w *bufio.Writer, dcfg *config.DatabaseConfig) {
|
//
|
||||||
|
// 直接用 pgx 的「简单查询协议」把整个脚本一次性发给服务端:
|
||||||
|
// - 服务端自己解析多语句,客户端无需写 SQL 解析器;
|
||||||
|
// - 不含 COPY,因此不涉及 copy 子协议;
|
||||||
|
// - 多语句在同一个隐式事务里执行,中途出错整体回滚,不会留下半截的库。
|
||||||
|
func runImport(args []string) {
|
||||||
|
fs := flag.NewFlagSet("import", flag.ExitOnError)
|
||||||
|
in := fs.String("in", "db_dump.sql", "导入文件路径")
|
||||||
|
cfgPath := fs.String("config", "", "配置文件路径(默认 configs/config.yml)")
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
|
||||||
|
script, err := os.ReadFile(*in)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("✗ 读文件 %s 失败: %v", *in, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, dcfg, err := openDB(*cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("✗ %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
log.Printf("→ 导入 %s(%d KB)-> %s ...", *in, len(script)/1024, dcfg.Addr())
|
||||||
|
if err := execScript(context.Background(), db, string(script)); err != nil {
|
||||||
|
log.Fatalf("✗ 导入失败(已整体回滚): %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("✓ 导入完成(%d KB)", len(script)/1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
// execScript 用 pgx 简单查询协议执行整段脚本。
|
||||||
|
//
|
||||||
|
// 必须走底层 *pgx.Conn:database/sql 的 Exec 用扩展协议,不支持一次发多条语句。
|
||||||
|
func execScript(ctx context.Context, db *sql.DB, script string) error {
|
||||||
|
conn, err := db.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取连接失败: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
return conn.Raw(func(dc any) error {
|
||||||
|
std, ok := dc.(*stdlib.Conn)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("驱动连接类型异常: %T", dc)
|
||||||
|
}
|
||||||
|
_, err := std.Conn().PgConn().Exec(ctx, script).ReadAll()
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeHeader 写文件头与几个会话设置。
|
||||||
|
func writeHeader(w *bufio.Writer, dcfg *config.DatabaseConfig, withExtension bool) {
|
||||||
fmt.Fprintf(w, "-- dbtool 导出:%s\n", dcfg.Addr())
|
fmt.Fprintf(w, "-- dbtool 导出:%s\n", dcfg.Addr())
|
||||||
fmt.Fprintf(w, "-- 生成时间:%s\n", time.Now().Format(time.RFC3339))
|
fmt.Fprintf(w, "-- 生成时间:%s\n", time.Now().Format(time.RFC3339))
|
||||||
fmt.Fprintf(w, "-- 导入:psql -U %s -d %s -v ON_ERROR_STOP=1 -f <本文件>\n", dcfg.User, dcfg.Name)
|
fmt.Fprintln(w, "--")
|
||||||
|
fmt.Fprintln(w, "-- 导入(二选一):")
|
||||||
|
fmt.Fprintf(w, "-- dbtool import -in <本文件>\n")
|
||||||
|
fmt.Fprintf(w, "-- psql -U %s -d %s -v ON_ERROR_STOP=1 -f <本文件>\n", dcfg.User, dcfg.Name)
|
||||||
fmt.Fprintln(w, "--")
|
fmt.Fprintln(w, "--")
|
||||||
fmt.Fprintln(w, "-- 注意:目标库必须是空库;本脚本不清表,重复执行会主键冲突。")
|
fmt.Fprintln(w, "-- 注意:目标库必须是空库;本脚本不清表,重复执行会主键冲突。")
|
||||||
fmt.Fprintln(w, "SET client_encoding = 'UTF8';")
|
fmt.Fprintln(w, "SET client_encoding = 'UTF8';")
|
||||||
fmt.Fprintln(w, "SET standard_conforming_strings = on;")
|
|
||||||
fmt.Fprintln(w, "SET check_function_bodies = false;")
|
|
||||||
fmt.Fprintln(w, "SET client_min_messages = warning;")
|
fmt.Fprintln(w, "SET client_min_messages = warning;")
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
|
if withExtension {
|
||||||
|
fmt.Fprintln(w, "CREATE EXTENSION IF NOT EXISTS vector;")
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// listTables 返回 public 下全部基表名(按名字排序,保证导出可复现)。
|
// listTables 返回 public 下全部基表名(按名字排序,保证导出可复现)。
|
||||||
@ -302,18 +364,17 @@ func writeCreateTable(w *bufio.Writer, table string, cols []column) {
|
|||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeCopyData 用 COPY ... FROM stdin 导出单表数据,返回行数。
|
// writeInsertData 用多行 INSERT 导出单表数据,返回行数。
|
||||||
//
|
//
|
||||||
// 全部值按服务端文本表示搬运;NULL 写成 \N,其余转义反斜杠 / 制表符 / 换行 / 回车。
|
// 值一律按服务端文本表示写成字面量:NULL 直接写 NULL,其余走 escapeLit。
|
||||||
func writeCopyData(w *bufio.Writer, db *sql.DB, table string, cols []column) (int64, error) {
|
// 这样既绕开了 COPY 的 copy 子协议限制,也让文件对人类可读、可手工改。
|
||||||
|
func writeInsertData(w *bufio.Writer, db *sql.DB, table string, cols []column) (int64, error) {
|
||||||
names := make([]string, len(cols))
|
names := make([]string, len(cols))
|
||||||
for i, c := range cols {
|
for i, c := range cols {
|
||||||
names[i] = qi(c.Name)
|
names[i] = qi(c.Name)
|
||||||
}
|
}
|
||||||
colList := strings.Join(names, ", ")
|
colList := strings.Join(names, ", ")
|
||||||
|
|
||||||
fmt.Fprintf(w, "COPY %s (%s) FROM stdin;\n", qname(table), colList)
|
|
||||||
|
|
||||||
rows, err := db.Query(fmt.Sprintf("SELECT %s FROM %s", colList, qname(table)))
|
rows, err := db.Query(fmt.Sprintf("SELECT %s FROM %s", colList, qname(table)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -327,28 +388,42 @@ func writeCopyData(w *bufio.Writer, db *sql.DB, table string, cols []column) (in
|
|||||||
}
|
}
|
||||||
|
|
||||||
var count int64
|
var count int64
|
||||||
|
var pending int // 当前这条 INSERT 已写入的行数
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
if err := rows.Scan(ptrs...); err != nil {
|
if err := rows.Scan(ptrs...); err != nil {
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
if pending == 0 {
|
||||||
|
fmt.Fprintf(w, "INSERT INTO %s (%s) VALUES\n", qname(table), colList)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, ",\n")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, " (")
|
||||||
for i := range raw {
|
for i := range raw {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
w.WriteByte('\t')
|
fmt.Fprint(w, ", ")
|
||||||
}
|
}
|
||||||
if raw[i] == nil {
|
if raw[i] == nil {
|
||||||
w.WriteString(`\N`)
|
fmt.Fprint(w, "NULL")
|
||||||
continue
|
} else {
|
||||||
|
fmt.Fprint(w, escapeLit(string(raw[i])))
|
||||||
}
|
}
|
||||||
w.WriteString(escapeCopy(raw[i]))
|
|
||||||
}
|
}
|
||||||
w.WriteByte('\n')
|
fmt.Fprint(w, ")")
|
||||||
|
|
||||||
|
pending++
|
||||||
count++
|
count++
|
||||||
|
if pending == insertBatchRows {
|
||||||
|
fmt.Fprintln(w, ";")
|
||||||
|
pending = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
if pending > 0 {
|
||||||
fmt.Fprintln(w, `\.`)
|
fmt.Fprintln(w, ";")
|
||||||
|
}
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
@ -413,7 +488,7 @@ func writeIndexes(w *bufio.Writer, db *sql.DB, table string) error {
|
|||||||
|
|
||||||
// writeSequenceResets 为每个自增列把序列推到 MAX(col)+1。
|
// writeSequenceResets 为每个自增列把序列推到 MAX(col)+1。
|
||||||
//
|
//
|
||||||
// COPY 写入的是显式 id,不会推进序列;不重置的话,后续 INSERT 会从 1 开始并与存量主键冲突。
|
// INSERT 写入的是显式 id,不会推进序列;不重置的话,后续 INSERT 会从 1 开始并与存量主键冲突。
|
||||||
// setval 用 (值, false) 形式:false 表示「这个值还没被取走」,故下一次 nextval 正好是 max+1;
|
// setval 用 (值, false) 形式:false 表示「这个值还没被取走」,故下一次 nextval 正好是 max+1;
|
||||||
// 空表时为 1,即从 1 开始。
|
// 空表时为 1,即从 1 开始。
|
||||||
func writeSequenceResets(w *bufio.Writer, table string, cols []column) {
|
func writeSequenceResets(w *bufio.Writer, table string, cols []column) {
|
||||||
@ -440,26 +515,15 @@ func isSerial(c column) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeCopy 转义 COPY 文本格式里的特殊字符。
|
// escapeLit 把值编码成 SQL 字符串字面量,用 E'...' 转义串语法。
|
||||||
// 先处理反斜杠,保证字面量 `\N` 被写成 `\\N` 而不会变成 NULL。
|
//
|
||||||
func escapeCopy(b []byte) string {
|
// 选 E'...' 而不是普通 '...':普通字面量里反斜杠的含义取决于会话的
|
||||||
var sb strings.Builder
|
// standard_conforming_strings,而 E'...' 下反斜杠**总是**转义符,行为与设置无关。
|
||||||
sb.Grow(len(b) + 8)
|
// 因此只需处理反斜杠与单引号两个字符,任何文本(含换行、制表符、引号、反斜杠)都能安全往返。
|
||||||
for _, c := range b {
|
func escapeLit(s string) string {
|
||||||
switch c {
|
s = strings.ReplaceAll(s, `\`, `\\`)
|
||||||
case '\\':
|
s = strings.ReplaceAll(s, `'`, `''`)
|
||||||
sb.WriteString(`\\`)
|
return "E'" + s + "'"
|
||||||
case '\t':
|
|
||||||
sb.WriteString(`\t`)
|
|
||||||
case '\n':
|
|
||||||
sb.WriteString(`\n`)
|
|
||||||
case '\r':
|
|
||||||
sb.WriteString(`\r`)
|
|
||||||
default:
|
|
||||||
sb.WriteByte(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// injectIfNotExists 给 pg_get_indexdef 的输出补上 IF NOT EXISTS。
|
// injectIfNotExists 给 pg_get_indexdef 的输出补上 IF NOT EXISTS。
|
||||||
@ -480,5 +544,5 @@ func qname(table string) string { return "public." + qi(table) }
|
|||||||
// qi 安全包裹 SQL 标识符(表名 / 列名 / 索引名)。
|
// qi 安全包裹 SQL 标识符(表名 / 列名 / 索引名)。
|
||||||
func qi(s string) string { return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` }
|
func qi(s string) string { return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` }
|
||||||
|
|
||||||
// ql 安全包裹 SQL 字符串字面量。
|
// ql 安全包裹 SQL 字符串字面量(普通形式,用于表名/列名等受控内容)。
|
||||||
func ql(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
|
func ql(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
|
||||||
|
|||||||
Reference in New Issue
Block a user