Skip to content
This repository was archived by the owner on May 25, 2026. It is now read-only.

feat: sqle-gaussdb-plugin GaussDB / openGauss structure diff (#2905)#1

Open
LordofAvernus wants to merge 18 commits into
mainfrom
fix-2905-gaussdb-opengauss-structure-diff
Open

feat: sqle-gaussdb-plugin GaussDB / openGauss structure diff (#2905)#1
LordofAvernus wants to merge 18 commits into
mainfrom
fix-2905-gaussdb-opengauss-structure-diff

Conversation

@LordofAvernus
Copy link
Copy Markdown
Collaborator

概述

为 SQLE 增加 sqle-gaussdb-plugin(新插件仓库),实现 GaussDB / openGauss 结构对比功能(TABLE / VIEW / FUNCTION / PROCEDURE 四类对象 DDL 抽取 + 同类型结构差异 SQL 生成)。

Fixes actiontech/sqle-ee#2905

仓库背景

本仓库为新建插件仓库。本 PR 分支与现有 dev-2892-gaussdb-slowlog 分支并行,承担"GaussDB / openGauss 结构对比"(2905)独立产品线。两条分支无共同祖先,分别建立独立 main / dev-2892 基线分支。

PR base 为新建的 main 分支(指向本期初始化 commit a3535af),head 为 dev 分支 head fe9519a,包含本期全部 18 个增量 commit。

主要变更(base 后 18 commits)

仓库骨架与依赖锁定

  • a3535af init skeleton with dual binary registration(base commit)
  • 12ea2da deps: lock indirect & add pingcap/parser sjjian fork replace for local build
  • e030dd9 docs: README 增补沙箱本地构建命令 + go.mod replace 移除提示
  • 2cfe5a3 deps: promote github.com/pkg/errors to direct require

Dialector 模块

  • 150721d feat: implement Dialector with DSN builder for GaussDB / openGauss
    • GaussDB 默认端口 8000,openGauss 默认端口 5432
    • libpq key=value DSN 风格 + sslmode 分支策略

Driver 模块(双独立二进制)

  • f66cd20 deps: promote DATA-DOG/go-sqlmock to direct require
  • bebd66b feat(driver): split stub into driver_common / driver_gaussdb / driver_opengauss with sqlmock unit tests
    • 双独立 cmd:cmd/sqle-gaussdb-plugincmd/sqle-opengauss-plugin

Extractor 模块(DDL 抽取)

  • 665f031 feat(extractor): implement Extract entry + TABLE / VIEW DDL extraction with sqlmock unit tests
  • b97484e feat(extractor): implement FUNCTION / PROCEDURE extraction with overload-aware ObjectName
  • 99a69ac fix(extractor): empty DatabaseObjects → enumerate schema (P0)
  • ce460cc fix(extractor): split pg_get_functiondef double-column query (P1.1)
  • d2f3d9e fix(extractor): use (pg_get_functiondef).definition for GaussDB record return (P1.1 amend)
  • d183e39 fix(extractor/procedure): strip PL/SQL trailing slash from PROCEDURE DDL (Fix-002 P3)
  • b97484e 重载处理:通过 pg_get_function_arguments(oid) 携带参数签名解决同名重载唯一性

Differ 模块(变更 SQL 生成)

  • ae3ac2d feat(differ): implement GenerateModifySQLs for 4 object types x 3 diff branches
  • 362f892 test(differ): add map case unit tests for GenerateModifySQLs
  • 9096b95 feat(driver): wire GetDatabaseObjectDDL / GetDatabaseDiffModifySQL to extractor + differ
  • e320194 fix(differ): rewrite base schema qualifier to compared (P1.2)
  • 88b501a fix(differ): TABLE SET search_path rewrite + USTORE normalize + schema-replace short-circuit (Fix-002 P2-A/P4/P5)
  • fe9519a fix(driver): strip ObjectName signature at plugin entry to recover UI modify-SQL drawer (Fix-002 P2-B)

DROP TABLE 强制带 WARNING 注释行(compat-RISK-5 兜底);FUNCTION / PROCEDURE DROP 带参数签名(R-Q4-2)。

inspector 复用

  • 仅做 import 透传,复用 sqle-pg-plugin 规则集,禁止反向修改 sqle-pg-plugin

测试

  • go vet ./... 通过
  • go test -count=1 ./... 全部通过:
    • internal/dialector OK
    • internal/differ OK
    • internal/driver OK
    • internal/extractor OK

影响面

  • 新插件仓库,与 sqle / sqle-ee 主仓通过 driverV2 gRPC 协议解耦
  • 无 schema 变更、无存量数据回填、无默认数据集合变更
  • 已确认无启动钩子函数被新增方法(data-upgrade skill 扫描通过)

关联 PR

…l build (#2905)

为让 sqle-gaussdb-plugin 在无内网 GOPROXY 的本地开发沙箱内也能完成 `go build`:

- replace 块新增 `github.com/pingcap/parser => github.com/sjjian/parser
  v0.0.0-20240704052347-b6199b7bccae`:sqle 主仓 vendor 已锁定该 fork(含
  parser.Token 类型扩展),不加这条 replace 时 ../sqle/sqle/driver/mysql/splitter
  会报 `undefined: parser.Token` 编译错误。版本号取自 sqle-pg-plugin/go.mod。
- replace 块固化 golang.org/x/{crypto,sys,net,text,sync} 为 sqle 主仓 vendor 实测使用
  的 Go 1.19 兼容版本,避免 `go mod tidy` MVS 把 indirect 升到 v0.42 / v0.44 等
  需要 Go 1.20+ 的版本(典型崩溃:crypto/ecdh / crypto/mlkem / slices 未在 GOROOT)。
- replace github.com/actiontech/dms => v0.0.0-20251027081421-309bc24335ca,与
  sqle-pg-plugin/go.mod 实测对齐,避免引入更新版本带来的传递依赖飘移。
- go.sum 锁定:由 `go mod tidy` 在 GOPROXY=goproxy.cn + GOSUMDB=off 容器内生成。

依赖评估:本提交不引入新的第三方 package,仅在 replace 块固化已有 require 的版本号
与本地路径(开发沙箱专用);CI / vendor 落盘阶段须移除 `../sqle` 与
`../sqle-pg-plugin` 本地路径 replace(README 已注明)。

验证:
- `go build ./cmd/sqle-gaussdb-plugin` 与 `go build ./cmd/sqle-opengauss-plugin`
  在 golang:1.19.6 容器内均成功,分别产出 ~38MB linux/amd64 二进制
- `go vet ./cmd/... ./internal/... ./pkg/...` 通过

Refs: actiontech/sqle-ee#2905
补充内容:

- 增加「开发期沙箱本地构建(无内网 GOPROXY 时)」小节,给出 docker run 完整命令
  (含 --network host / GOPROXY=goproxy.cn / GOSUMDB=off),方便后续 Task-Dev-003+
  在同一沙箱内复用。
- 在「构建方式」小节顶部加 callout:CI / vendor 落盘前须移除 go.mod 内
  `../sqle` 与 `../sqle-pg-plugin` 两条本地路径 replace,require 块版本号已是
  生产形态。

Refs: actiontech/sqle-ee#2905
Task-Dev-003 实现 Dialector 时,internal/dialector/dialector.go 直接 import
`github.com/pkg/errors` 用于 Open() 错误包装。`go mod tidy` 因此把该模块从
indirect 提升为 direct require(仅 go.mod 注释行变动,go.sum 与 vendor 解析
结果不变)。

依赖评估:本提交不引入新的第三方 package,仅是已有 indirect 依赖的直接化
标注变化。github.com/pkg/errors v0.9.1 与 sqle 主仓 vendor 实测使用版本严格
一致(License=BSD-2-Clause,长期维护,sqle / sqle-pg-plugin / dms 全系
直接或间接依赖)。

验证:
- `docker run golang:1.19.6 go mod tidy` 在 sqle-gaussdb-plugin 仓库内
  GOPROXY=goproxy.cn / GOSUMDB=off 下成功,go.sum 无新行
- `go vet ./internal/dialector/...` 通过
- 后续业务代码 commit 已就绪,与本提交解耦

Refs: actiontech/sqle-ee#2905
替换 Task-Dev-002a 占位实现,落地 design §4.3 关键片段:

- DriverName() 固定返回 "opengauss"(database/sql 驱动名,由 dialector.go
  文件头的 `_ import` 注册 gitee.com/opengauss/openGauss-connector-go-pq)。
  GaussDB 与 openGauss 共用同一驱动,仅 DSN 内容不同。
- DefaultPort() 按 Variant 分支:consts.PluginNameGaussDB → 8000、
  consts.PluginNameOpenGauss → 5432、未知 / 空 Variant → 0(兜底,被单测
  显式覆盖)。
- BuildDSN(*driverV2.DSN) 输出 libpq key=value 串:5 个基础字段
  (host / port / user / password / dbname) + connect_timeout=5;
  GaussDB 追加 sslmode=disable,openGauss 不显式声明 sslmode(保留驱动
  默认 prefer);DSN.AdditionalParams 追加在末尾,按 libpq last-wins
  规则覆盖前面同 key 默认值。
- 所有字段值通过 escape() 包成单引号串,反斜杠加倍、单引号 \' 转义,与
  github.com/lib/pq url.go 的连接串转义策略一致。Open() 通过 BuildDSN +
  BaseDialector.GetConn 复用同一段 DSN 构造逻辑,保证单测 (字符串断言)
  与运行时 (sql.Open) 完全同源。

PluginName 字面值严格引用 pkg/consts (PluginNameGaussDB / PluginNameOpenGauss
/ DefaultPortGaussDB / DefaultPortOpenGauss),禁止在 dialector.go 内
硬编码字符串。

design §4.3 伪代码 BuildDSN 入参写 *driverV2.Config 但访问 cfg.Host 等
DSN 内部字段;real sqle 链路 Dialector.Open 永远传 *driverV2.DSN。本实现
直接采用 *driverV2.DSN 入参以保持代码自洽,文件头部注释已说明该等价决策。

单元测试覆盖 design §17.1.1 全部分支,4 个 test 函数共 13 条 sub-case:

- TestDialector_DefaultPort: 4 条(GaussDB → 8000 / openGauss → 5432 /
  空 Variant → 0 / 未知 Variant → 0)。
- TestDialector_BuildDSN_BaselineFields: 3 条(GaussDB 5 字段 + sslmode=
  disable / openGauss 5 字段 + sslmode 必须不出现 / GaussDB 空 DatabaseName
  回退 postgres)。
- TestDialector_BuildDSN_AdditionalParamsAndEscape: 5 条(GaussDB 用户
  sslmode=verify-full 覆盖默认 / openGauss 用户 sslmode=require 落入
  DSN / 单引号 password it's 转义 / 反斜杠 password 转义 / 空 key
  AdditionalParam 防御性忽略)。
- TestDialector_DriverName: 锁 DriverName=="opengauss" 不漂移。

全部 case 使用 Go map[string]struct 结构(不用 slice + index),纯字符串
断言,零 sql.Open / time.Sleep / 网络 / 磁盘依赖(todo.md Task-Dev-003 §C
硬约束)。dupeKeyLast 通过 strings.LastIndex 验证用户 override key 是末
位 occurrence,与 libpq last-wins 解析等价。

依赖评估:本提交不引入新依赖。pkg/errors 提升为 direct require 已在前一
commit (deps:) 单独完成。

验证(docker golang:1.19.6 + GOPROXY=goproxy.cn / GOSUMDB=off):
- go vet ./internal/dialector/... 通过(无输出)
- go test -v ./internal/dialector/... 全部 PASS(4 test fn / 13 sub-case)
- go mod tidy 仅产生前一 commit 的 go.mod 单行注释变化,无 go.sum diff

Refs: actiontech/sqle-ee#2905
driver_test.go consumes sqlmock.New / sqlmock.MonitorPingsOption /
sqlmock.ExpectPing API directly so the dependency is no longer indirect.
No version change — the v1.5.0 lock matches the previous indirect entry
(go.sum unchanged, vendor not vendored in this repo).

评估:License=MIT, 最近更新=2024-09, sqle-pg-plugin / sqle-tbase-plugin 等
sqle 系插件都用同一版本做 *sql.DB mock。本仓库 Task-Dev-004 起首次直接
import sqlmock,按 Go 惯例应提升为 direct require。
…_opengauss with sqlmock unit tests (#2905)

Replace the Task-Dev-002 placeholder stub.go (which only delegated to
driverPkg.NewDriverImpl) with the real Driver implementation split into
three files per plan §4.1 / §4.2 / §4.3:

  - internal/driver/driver_common.go
    Shared *Driver struct embedding *driverPkg.DriverImpl. Inherits the
    default driverV2.Driver method set from sqle main repo, overrides
    Ping to delegate through *sql.DB.PingContext (design §5.1) so the
    pool-level handle is used instead of a stale *sql.Conn. newCommon
    factory applies the connection-pool policy MaxOpenConns=10 /
    MaxIdleConns=5 / ConnMaxLifetime=30min on the *sql.DB after
    DriverImpl construction (design §5.1 line 328; compat-RISK-2 pins
    the values for future alignment with sqle-pg-plugin). wrapPGError
    passes *pq.Error from gitee.com/opengauss/openGauss-connector-go-pq
    (or the lib/pq fork it inherits from) through unchanged so callers
    still see the raw 5-character SQLSTATE (design §5.3); detection is
    done via reflect type-name + package-path comparison so this file
    does not import the openGauss connector directly.

  - internal/driver/driver_gaussdb.go
    GaussDBDriver{*Driver} aggregation + NewGaussDBDriverImpl factory.
    Signature preserved bit-for-bit from stub.go so
    cmd/sqle-gaussdb-plugin/main.go does not need to change. Placeholder
    GetDatabaseObjectDDL / GetDatabaseDiffModifySQL return the explicit
    errNotImplemented sentinel pending Task-Dev-005 (extractor) /
    Task-Dev-006 (differ), so DMS sees a deterministic failure rather
    than the embedded DriverImpl default empty slice.

  - internal/driver/driver_opengauss.go
    Same shape as driver_gaussdb.go, only the Variant tag / PluginName
    differ. The Driver layer is variant-agnostic on purpose; all
    branching (default port, default sslmode, libpq DSN layout) lives in
    internal/dialector (design §3.2 / §4.3).

  - internal/driver/driver_test.go
    Map-case style unit tests with DATA-DOG/go-sqlmock so no real
    database is touched (design §17.1.5):

      * TestDriver_PluginNameRegistration — 3 sub-cases covering both
        Variants and the embed-chain Variant propagation.
      * TestDriver_CapabilityMatrix — 20 sub-cases (2 variants × 10
        OptionalModules) locking the capability surface: the 2 enabled
        OptionalModules return errNotImplemented; the 8 disabled modules
        return either the DriverImpl zero-value default or the
        "database conn not initialized" sentinel for Query (which needs
        *sql.Conn, deliberately not wired here).
      * TestCapabilityMatrix_I18nMetadataOnly — 1 sub-case locking the
        OptionalModuleI18n enum String() so a future iota reordering
        cannot silently break the cmd/* main.go capability declaration.
      * TestDriver_PingTimeout — 3 sub-cases (deadline / cancel /
        nil-DB) with a 2-second harness timeout so a regression that
        breaks ctx propagation fails the test instead of hanging the
        suite.
      * TestWrapPGError — 3 sub-cases locking the pointer-identity
        pass-through contract for nil / plain / sentinel errors.
      * TestApplyPoolPolicy — 2 sub-cases locking the non-nil DB pool
        application and the nil-DB no-op defensive branch.

    Total: 32 sub-cases passing.

  - internal/driver/stub.go
    Deleted in the same business commit per plan §4 step E. cmd/* main.go
    continues to compile because NewGaussDBDriverImpl /
    NewOpenGaussDriverImpl signatures are preserved.

Verification (golang:1.19.6 container, GOPROXY=goproxy.cn):
  go vet ./internal/driver/... ./cmd/...           — clean
  go test -v ./internal/driver/...                  — 32 sub-cases PASS
  go build -o /tmp/sqle-gaussdb-plugin ./cmd/...    — OK
  go build -o /tmp/sqle-opengauss-plugin ./cmd/...  — OK

Capability matrix (design §5.2; cmd/*/main.go SetEnableOptionalModule
contract): only OptionalGetDatabaseObjectDDL +
OptionalGetDatabaseDiffModifySQL are advertised this period. All other
OptionalModule methods fall through to the DriverImpl default.

compat-RISK-2 (connection pool alignment) and compat-RISK-7 (SQLSTATE
pass-through) tracked in docs/dev/compat_risks.md; this commit fulfils
the in-code part of both mitigations.

data-upgrade: A-class no-op — no init() / startup hook / database write
is added by this commit (driver_common.go / driver_gaussdb.go /
driver_opengauss.go contain only construction-time pure code).
…with sqlmock unit tests (#2905)

Task-Dev-005 第一轮 Extractor 落地:在 internal/extractor/ 下新增 4 个文件,
覆盖 design §6.1 入口分发 + §6.2.1 TABLE + §6.2.2 VIEW + §6.3 注入防御
+ §6.5 空 DDL 兜底。

- extractor.go: Extractor struct + NewExtractor + Extract(ctx, info)
  入口 switch,按 driverV2.ObjectType_* 字面值分发;本期实现 TABLE / VIEW
  两个 case + default(design §6.1 line 399 字面错误);FUNCTION /
  PROCEDURE 留 TODO 注释指向 Task-Dev-006。
- table.go: extractTable 走 design §6.2.1 pg_get_tabledef + relkind
  IN ('r','p'),$1 / $2 参数化绑定;sql.ErrNoRows → 空 DDL 不报错;
  其他错误透传保留 SQLSTATE。
- view.go: extractView 走 design §6.2.2 显式拼 CREATE OR REPLACE VIEW
  + 服务端 quote_ident + pg_get_viewdef(c.oid, true) pretty-print;
  $1 / $2 参数化绑定;sql.ErrNoRows → 空 DDL 不报错。
- extractor_test.go: 5 个 map case test 函数共 31 sub-case,
  sqlmock.QueryMatcherEqual 锁定 SQL 文本字面值;覆盖
  TABLE/VIEW 正常 + not_found + permission_denied + 不支持
  ObjectType(INDEX/TRIGGER/EVENT/SEQUENCE/TYPE/PACKAGE/FUNCTION/
  PROCEDURE/empty 9 条)+ QuoteIdent 注入防御(TABLE 8 条 + VIEW 6 条,
  含空格 / 大小写 / 中文 / SQL 注入串 / 单双引号 / 混合特殊字符)+
  nil info 和多对象顺序聚合兜底。

类型签名漂移说明(semantic/design_pseudocode_param_type_drift_20260521):
design §6.1 伪代码假设每对象返回一个独立的
*driverV2.DatabaseSchemaObjectResult,real 链路
(sqle/sqle/driver/v2/driver_interface.go:431-463) 实际是 per-schema
聚合的 DatabaseObjectDDLs []*DatabaseObjectDDL;按"业务字段访问语义
优先 + 文件头注释 + commit 标注"准则,本实现采用 real 链路签名,
extractor.go 顶部已显式说明等价决策。

测试结果:
- go vet ./internal/extractor/... ./internal/driver/... ./cmd/... 通过
- go test -v ./internal/extractor/... 全 31 sub-case PASS
- go build ./cmd/sqle-gaussdb-plugin + ./cmd/sqle-opengauss-plugin 双
  binary 编译通过(37M each)

约束遵守:
- 仅在 internal/extractor/ 目录工作,未动 driver / dialector / cmd /
  pkg / Makefile / 8 个 sibling 仓库
- $1 / $2 参数化绑定,禁字符串拼接到 SQL(compat-RISK-4 兜底)
- ObjectType 字面值统一从 driverV2 包引用,禁硬编码 "TABLE"/"VIEW"
- 单测全部基于 sqlmock,禁 sql.Open / time.Sleep / 网络/磁盘依赖
- 无 go.mod / go.sum / vendor 改动,无新增第三方依赖

需求引用:需求 2.1(TABLE 差异)+ 需求 2.2(VIEW 差异)+ 需求 4.1
(PG / MySQL 不回归——只动新仓 extractor 目录)。
…d-aware ObjectName (#2905)

按 design §6.2.3 / §6.2.4 / §6.4 / §6.5 + §17.1.3 完成 Extractor 第二轮:
FUNCTION + PROCEDURE 抽取 + 重载支持 + 配套 map case 单测。

变更范围(限定 internal/extractor/ 5 文件):
- internal/extractor/function.go(新增):pg_get_functiondef + pg_get_function_arguments
  合并 SQL,prokind='f';extractFunction 返回 []*driverV2.DatabaseObjectDDL(slice),
  每条独立 ObjectName="funcname(arg_type_list)";对象不存在返回单条空 DDL 占位
  (ObjectName 不带参数签名)。
- internal/extractor/procedure.go(新增):同结构 prokind='p',
  ObjectType=PROCEDURE;与 function.go byte-for-byte 对称仅 prokind / ObjectType 差异。
- internal/extractor/extractor.go(修改):switch 追加 case ObjectType_FUNCTION /
  case ObjectType_PROCEDURE 两个分支,用 append(..., rs...) 展开聚合
  (helper 返回 slice 以承载重载多行);删除 TODO(Task-Dev-006) 注释;
  TABLE / VIEW / default 分支保持不变。
- internal/extractor/function_test.go(新增):4 个 test 函数 / 25 sub-case
  覆盖 design §17.1.3 全部分支:
    * TestExtractor_Extract_FunctionAndProcedure (5 cases) — Extract 入口分发 +
      not-found 占位 + permission denied 透传;
    * TestExtractor_FunctionOverload (4 cases) — 无参 / 单参 / 同名两重载 /
      同名三重载(断言 slice 长度 + ObjectName 集合 + map key 唯一性);
    * TestExtractor_ProcedureOverload (4 cases) — 同上 PROCEDURE 版;
    * TestExtractor_QuoteIdent_FunctionInjectionDefense (6 cases) +
      TestExtractor_QuoteIdent_ProcedureInjectionDefense (6 cases) —
      compat-RISK-4 SQL 注入兜底(空格 / 大小写 / 中文 / SQL 注入串 / 单双引号)。
- internal/extractor/extractor_test.go(修改):从
  TestExtractor_Extract_UnsupportedObjectType 中删除 FUNCTION_passthrough_to_Task006 /
  PROCEDURE_passthrough_to_Task006 两条 Task-Dev-005 的过渡 sub-case
  (FUNCTION/PROCEDURE 不再属 unsupported 集合);同步文件头注释。

关键设计契约:
- byte-for-byte SQL 锁定:sqlmock.QueryMatcherEqual + raw const 双侧引用,
  $1 / $2 参数化绑定,Go 侧零 escape(design §6.3)。
- ObjectName 携带参数签名:让上层 compareSchema 按 (ObjectName, ObjectType) 建 map
  时同名重载天然唯一;DROP FUNCTION / PROCEDURE 时自然带参数签名(R-Q4-2)。
- 对象不存在兜底:返回单条 ObjectDDL=="" 的占位(ObjectName 不带参数签名),
  不报错,不返回 nil(与 TABLE/VIEW 一致,让业务层识别"该侧不存在")。
- 错误透传:catalog 权限错误 / SQLSTATE 原样返回,不吞诊断信息。

验证(docker golang:1.19.6 沙箱):
- go vet ./internal/extractor/... ./internal/driver/... ./cmd/...  PASS
- go test -v ./internal/extractor/...  PASS(Task-Dev-005 29 + Task-Dev-006 25 =
  54 sub-case)
- GOOS=linux GOARCH=amd64 go build ./cmd/sqle-gaussdb-plugin    OK (37.86 MB)
- GOOS=linux GOARCH=amd64 go build ./cmd/sqle-opengauss-plugin  OK (37.86 MB)

需求引用: 2.3 PROCEDURE 差异 / 2.4 FUNCTION 差异 / 4.2 R-Q4-2 DROP 带参数签名。
…f branches (#2905)

实现 sqle-gaussdb-plugin internal/differ 纯模块(Task-Dev-007),覆盖 design §7
最小实现矩阵:4 类对象(TABLE/VIEW/FUNCTION/PROCEDURE)× 3 种差异分类
(仅 base / 仅 compared / 两侧都有但不同)= 12 个分支字符串拼接逻辑。

实现要点
--------
- 主入口 GenerateModifySQLs(baseSchema, baseDDLs, comparedSchema, comparedDDLs)
  返回 []string ModifySQLs;纯字符串拼接,无 db / ctx / 副作用。
- 内部按 (ObjectName, ObjectType) 元组建 map(Task-Dev-006 已让 FUNCTION/
  PROCEDURE 的 ObjectName 携带参数签名 → 元组天然唯一,支持同名重载)。
- TABLE 走 DROP+CREATE 兜底(PG 系无 ALTER 幂等路径,design §7.1);
  VIEW/FUNCTION/PROCEDURE 直接复用 base 侧 CREATE OR REPLACE(PG 系幂等)。
- DROP TABLE 前置 WARNING 注释 byte-for-byte 锁定(design §7.3 + impact_analysis.yml
  R-Q4-1 / risk_points 第 4 条「DROP TABLE 兜底可能误删生产数据」唯一兜底)。
- DROP FUNCTION/PROCEDURE 自带参数签名(R-Q4-2),避免 PG 报 function is not
  unique;ObjectName 字面值由 extractor 上游 pg_get_function_arguments 拼装。

类型签名漂移说明
---------------
design §7 伪代码把 differ 入口写成 Generate(base, compared []*DatabaseSchemaObjectResult),
但 real 链路(sqle/sqle/driver/v2/driver_interface.go:431-468)中
DatabaseSchemaObjectResult 是 per-schema 聚合容器;按"业务字段访问语义优先"
准则(semantic/design_pseudocode_param_type_drift_20260521)将 differ 入参定为
schema-pair 内的对象 DDL 列表 []*driverV2.DatabaseObjectDDL,driver 接入
(Task-Dev-008)时一次调用 extractor 取得 base/compared 两个 result 后解构传入即可。

单测覆盖
--------
internal/differ/differ_test.go: 12 个 Test 函数 / 26 个 sub-case 全部 map case
结构,纯字符串断言(无 sqlmock / 无 sql.Open / 无网络 / 无文件 IO)。覆盖
design §17.1.4 全部 8 个测试函数命名 + impact_analysis.yml line 312「4 类对象 ×
3 种差异共 12 个分支」全量覆盖:

  - 8 个 design §17.1.4 必覆盖函数:
    TestDiffer_TableOnlyBase / TableOnlyCompared / TableBothDiff
    TestDiffer_ViewBothDiff(含 3 个 sub-case 覆盖 only_base/only_compared/both_diff)
    TestDiffer_FunctionBothDiff(同上 3 sub-case)
    TestDiffer_ProcedureBothDiff(同上 3 sub-case)
    TestDiffer_FunctionDropWithArgs(R-Q4-2:两条重载独立 DROP,sort.Strings 顺序无关断言)
    TestDiffer_WarningCommentInjection(R-Q4-1:byte-for-byte 字面值锁定 + 拒绝尾随空白/Go注释风格/!偏离)
  - 4 个增强覆盖:
    TestDiffer_TableBothSame_NoOutput(无变更不输出,3 sub-case 含 TABLE/VIEW/FUNCTION)
    TestDiffer_MixedObjectTypes(4 类对象混合一次性输入,顺序无关集合断言)
    TestDiffer_UnsupportedObjectTypeIgnored(INDEX/TRIGGER 等静默跳过不 panic)
    TestDiffer_NilAndEmptyDefensive(nil 条目 / nil DatabaseObject 边界)

中文 schema 名场景单测兜底(critical_boundaries SQL 注入防御):differ 直接复用
extractor 上游 $1/$2 参数化产出的字面值,不在本层做额外 escape。

map 迭代顺序无关断言:所有多元素期望用 sort.Strings(got) + sort.Strings(want)
模式(参考 semantic/sqlmock_rows_iteration_empty_and_order_20260521)。

验证
----
- go vet ./internal/differ/... ./internal/extractor/... ./internal/driver/... ./cmd/...  PASS
- go test -count=1 ./internal/differ/...        PASS(26 sub-case 全部通过)
- go test -count=1 ./internal/extractor/...     PASS(无回归)
- go build ./cmd/sqle-gaussdb-plugin            OK
- go build ./cmd/sqle-opengauss-plugin          OK
- 8 个 sibling 仓库(sqle / sqle-ee / sqle-pg-plugin / sqle-tbase-plugin /
  dms / dms-ee / dms-ui / dms-ui-ee)git status 全部 clean

不实现的场景(design §7.5 显式列出)
--------------------------------
分区 SPLIT/MERGE / 列存变更 / 分布键变更:按整表 DROP+CREATE 兜底;
INDEX / TRIGGER:本期不在抽取范围 → DDL 不出现 → diff 不出现。

driver 接入(替换 driver_gaussdb.go / driver_opengauss.go 中的 errNotImplemented)
留给 Task-Dev-008,本任务严格限定在 internal/differ/ 目录内。

Fixes #2905
补充 internal/differ/differ_test.go 单测文件(前一个 commit 误漏,本 commit 单独
补齐;语义上属于同一 Task-Dev-007,与 differ.go 共同满足 design §17.1.4 单测
矩阵 + impact_analysis.yml line 312 12 分支全量覆盖契约)。

覆盖
----
- 12 个 Test 函数 / 26 个 sub-case,全部 map case 结构 + 顺序无关断言
  (sort.Strings 模式 + map key 唯一性兜底)。
- design §17.1.4 line 873-881 全部 8 个测试函数命名 + 4 个增强覆盖
  (TableBothSame_NoOutput / MixedObjectTypes / UnsupportedObjectTypeIgnored /
  NilAndEmptyDefensive)。
- impact_analysis.yml R-Q4-1(DROP TABLE WARNING 注释 byte-for-byte 锁定)+
  R-Q4-2(DROP FUNCTION/PROCEDURE 带参数签名重载下两条独立 DROP)专项覆盖。
- 中文 schema 名场景(critical_boundaries SQL 注入防御)兜底。

约束
----
- 纯字符串断言,无 sqlmock / 无 sql.Open / 无网络 / 无文件 IO;
- 用本地 wantWarningComment 副本断言 WARNING 字面值(包内常量被无意修改时测
  试同步失效);
- 多元素期望全部用 sort.Strings(got) + sort.Strings(want) 顺序无关比较,
  避免 map 迭代顺序假设(参考 semantic/sqlmock_rows_iteration_empty_and_order_20260521)。

验证
----
- go test -count=1 ./internal/differ/...  PASS(26 sub-case 全通过)
- go test -count=1 ./internal/extractor/...  PASS(无回归)

Fixes #2905
…xtractor + differ (#2905)

shared impl on *Driver via embedding; variants no longer override (DRY)

Task-Dev-008 streams the previously placeholder GetDatabaseObjectDDL /
GetDatabaseDiffModifySQL through real wiring:

  - internal/driver/driver_common.go now hosts both methods on *Driver. The
    methods delegate to extractor.NewExtractor(d.DriverImpl.DB).Extract for
    DDL extraction; GetDatabaseDiffModifySQL additionally calls
    differ.GenerateModifySQLs after two Extract invocations (one per
    schema side) to produce the ModifySQLs slice. SchemaName on the
    returned DatabaseDiffModifySQLResult follows design §7 by surfacing
    info.BaseSchemaName; compared schema names are already encoded into
    DROP statements inside differ output. calibratedDSN is retained on
    the signature (interface contract) and explicitly discarded for
    future cross-instance compare scenarios.

  - internal/driver/driver_gaussdb.go and internal/driver/driver_opengauss.go
    drop their per-variant errNotImplemented overrides; embedding makes
    both *GaussDBDriver and *OpenGaussDriver inherit the shared
    implementation. errNotImplemented sentinel is removed from
    driver_common.go since no caller relies on it anymore.

  - internal/driver/driver_test.go updates the two enabled-this-period
    sub-cases in TestDriver_CapabilityMatrix from "expect not-implemented
    sentinel" to "nil objInfos -> empty slice + nil err" probes, swaps
    the errNotImplemented reference in TestWrapPGError for a local
    sentinel, and adds two new map-case Test functions
    (TestDriver_GetDatabaseObjectDDL_RealChain and
    TestDriver_GetDatabaseDiffModifySQL_RealChain), 3 sub-cases each
    (6 new sub-cases total). The diff sub-case exercises end-to-end
    DROP+CREATE wiring with the WARNING comment from design §7.3 / R-Q4-1.

Verification (docker golang:1.19.6 sandbox):
  go vet ./...               PASS
  go test ./internal/driver  PASS  (8 funcs, 42 sub-cases including new 6)
  go test ./internal/extractor PASS (no regression; 55 sub-cases)
  go test ./internal/differ    PASS (no regression; 26 sub-cases)
  go build ./cmd/sqle-gaussdb-plugin    OK
  go build ./cmd/sqle-opengauss-plugin  OK

Scope is limited to internal/driver/ (4 files, +461/-72 lines); 8 sibling
repos remain git-clean; no go.mod/go.sum churn.
…t-Fix-001)

sqle-ee execute_comparison main path passes baseInfos[i] with only
SchemaName set and DatabaseObjects=nil, expecting plugin to enumerate
the schema. Initial implementation iterated info.DatabaseObjects only,
returning empty DatabaseObjectDDLs and causing inconsistent_num=0 in
the UI for every schema pair (docs/test/case-2-1.md ~ case-2-4.md).

Add a per-Extract.entry fallback: when info.DatabaseObjects is empty,
list pg_class (relkind 'r'/'v') and pg_proc (prokind 'f'/'p') in the
target schema, GROUP BY proname for overload-aware dedup, then feed
the resulting []*DatabaseObject into the existing switch dispatch so
TABLE/VIEW/FUNCTION/PROCEDURE share their original extract helpers.

SQL templates pin schema via $1 binding (design §6.3 no-Go-escape
contract). Unit tests cover full-enumeration end-to-end, empty schema
(0 objects), and propagated permission error.
…sk-Test-Fix-001)

GaussDB kernel 505.2.1 + openGauss official Go driver returned the
combined SELECT pg_get_functiondef(oid), pg_get_function_arguments(oid)
result as a Postgres composite/record literal, e.g.
(4,"CREATE OR REPLACE FUNCTION ..."), which Scan() into two strings
cannot strip. Symptom: comparison_statements / modify_sql_statements
API output for FUNCTION and PROCEDURE wrapped the SQL text in
(N,"..."), making it impossible to copy-paste into psql
(docs/test/case-2-3.md / case-2-4.md).

Split each helper into two single-column queries (defs and args),
both keyed by (nspname, prokind, proname) and ordered by p.oid so
the i-th row of each query refers to the same overload. min(len)
alignment defends against rare concurrent overload mutation.

Unit tests rewritten to mock the two-query pattern via two helpers
(expectFuncDefsAndArgs / expectProcDefsAndArgs); two new regression
tests assert ObjectDDL does not start with '(' nor contain ',"' so
future driver regressions surface as test failures.
…t-Fix-001)

VIEW / FUNCTION / PROCEDURE CREATE branches previously emitted the
base side ObjectDDL verbatim, leaving qualifiers like
`CREATE OR REPLACE VIEW test_2905_base.v_user_summary` in the
generated modify SQL. Executing such SQL against the compared schema
would recreate the object in base namespace instead of converging
the compared side to the base form (docs/test/case-2-2.md /
case-2-3.md, design §7.2).

Extend genOnlyBaseSide / genBothDiff signatures with baseSchema and
add a rewriteSchemaQualifier helper that does a strings.ReplaceAll
on the `<baseSchema>.` prefix. TABLE branch keeps current behaviour
(pg_get_tabledef relies on search_path). Self-compare and empty
baseSchema short-circuit to no-op.

Also surfaces the existing same-DDL short-circuit (`baseDDL.ObjectDDL
== comparedDDL.ObjectDDL`) which doubles as P1.3 overload-redundant
CREATE filter once P1.1 stops wrapping pg_get_functiondef output in
record literals; new TestGenBothDiff_SameDDLOverload_NoOutput pins
that behaviour.
…d return (P1.1 amend)

Initial P1.1 fix split the double-column query into two single-column
queries, assuming the tuple wrapper `(N, "DDL")` was caused by the
driver packing multi-column rows into a record. After redeploy the
regression still produced `(4,"CREATE OR REPLACE FUNCTION ...")`.

Root cause confirmed by hitting the database directly with the
openGauss driver: GaussDB ships a `pg_get_functiondef(oid)` overload
that returns a composite record `(headerlines int, definition text)`
instead of upstream PostgreSQL's plain text. The driver serialises
the record-as-string and Scan() into one string keeps the wrapper.

Switch the SELECT projection to
`(pg_get_functiondef(p.oid)).definition` so the server-side strips
the record layer and the driver receives plain text. The split into
def/args two queries is kept since it aligns with overload ordering
(ORDER BY p.oid) and shields against future record-returning quirks
in pg_get_function_arguments.
…DDL (Fix-002 P3)

openGauss / GaussDB `pg_get_functiondef(oid)` returns PROCEDURE DDL with a
trailing standalone `/` line (PL/SQL terminator, gsql / SQL*Plus style).
The Go database/sql + openGauss-connector-go-pq driver — same driver used
by sqle-ee workflow exec — does not recognise `/` as a statement
terminator and reports `pq: syntax error at or near "/"` when the
modify_sql_statements output is fed back into the driver directly.

This commit adds extractor/procedure.go::stripPLSQLTerminator which
trims the trailing whitespace and the standalone `/` line (with a
multi-line anchored regex `\n\s*/\s*\z`). FUNCTION path (prokind='f')
is untouched — its output ends with `$function$;` and does not carry
the terminator. Mid-body `/` (theoretically impossible in a PL/SQL
block) is preserved by anchoring the regex to the very end of the
string.

Test coverage:
- TestExtractProcedure_TrimTrailingSlash with 4 sub-cases:
  slash_with_trailing_newline / slash_no_trailing_newline /
  slash_with_extra_blank_line_before / no_slash_unchanged.

Defect source:
- docs/test/case-3-4.md (initial reproduction in openGauss PROCEDURE
  main path)
- expertise_docs/semantic/opengauss_procedure_slash_terminator_driver_incompat_20260521.md
  (cross-channel background: gsql OK, JDBC / psycopg2 / Go all fail)
…a-replace short-circuit (Fix-002 P2-A/P4/P5)

Three closely-related differ fixes consolidated in one commit because
they share the same string-compare short-circuit code path inside
GenerateModifySQLs.

P2-A: TABLE schema rewrite gap (case-3-1.md / case-2-1.md)
  Task-Test-Fix-001 P1.2 introduced rewriteSchemaQualifier to rewrite
  base→compared schema qualifier for VIEW / FUNCTION / PROCEDURE, but
  TABLE was missed. pg_get_tabledef emits the TABLE DDL with a
  `SET search_path = baseSchema;` header line followed by a bare-name
  CREATE TABLE — so when the variant SQL runs against the compared
  instance, CREATE TABLE lands inside the BASE schema and collides
  with the existing same-name table (`relation "t_order" already
  exists in schema "test_2905_base"`). Fix:
  - new helper rewriteSetSearchPath(ddl, baseSchema, comparedSchema)
    using `(?m)^SET\s+search_path\s*=\s*<base>(\s*;?)` to anchor the
    rewrite to the first SET line, with explicit literal match on
    baseSchema to avoid mis-firing on unrelated SET lines;
  - genOnlyBaseSide TABLE branch and genBothDiff TABLE branch both
    invoke rewriteSetSearchPath on baseDDL.ObjectDDL before joining.
  - design §7.2 red line preserved: only base→compared, never reverse.

P4: USTORE/ustore case-only normalisation (case-2-1.md remnant)
  GaussDB pg_get_tabledef outputs storage_type=USTORE on initial
  CREATE TABLE but storage_type=ustore on rebuild; the case-only
  difference triggers a false-positive "inconsistent" diff. Fix:
  - new helper normalizeDDLForCompare(s) running
    `(?i)storage_type\s*=\s*ustore` → "storage_type=ustore" on a copy
    of the DDL strictly for the compare-side string compare; the
    actual modify SQL output is not mutated.

P5: schema-replace short-circuit before compare (case-2-3.md remnant)
  Overload functions whose body is identical but the SET / CREATE OR
  REPLACE schema qualifier differs (base.fn vs compared.fn) would
  still emit a redundant CREATE OR REPLACE. Fix:
  - GenerateModifySQLs now runs
    `rewriteSetSearchPath(rewriteSchemaQualifier(baseDDL, base, compared), base, compared)`
    + normalizeDDLForCompare on both sides before the equality check.
    When normalised equal, the diff is short-circuited.

Test coverage (5 new test functions, 12 sub-cases):
  - TestGenOnlyBaseSide_RewriteTableSearchPath
  - TestGenBothDiff_RewriteTableSearchPath
  - TestGenBothDiff_USTORENormalize_NoOutput
  - TestGenBothDiff_SameDDLDifferentSchema_NoOutput
  - TestRewriteSetSearchPath_GuardRails (5 sub-cases: empty base /
    same schema / normal rewrite with semicolon / without semicolon /
    non-matching base literal)

Defect source:
- docs/test/case-3-1.md (P2-A primary reproduction)
- docs/test/case-2-1.md (P2-A latent in GaussDB + P4 remnant)
- docs/test/case-2-3.md (P5 remnant — overload same body)
- expertise_docs/episodic/sqle_gaussdb_table_search_path_not_rewritten_20260521.md
… modify-SQL drawer (Fix-002 P2-B)

UI main path (dms-ui-ee EnvironmentComparison) ships the tree-node
display text (with parameter signature, e.g. `fn_calc(p integer)` /
`proc_alert(p TEXT)`) as ObjectName to plugin
GetDatabaseDiffModifySQL / GetDatabaseObjectDDL gRPC. Plugin's
internal extractor uses ObjectName as the `pg_proc.proname = $2`
bind parameter; pg_proc.proname is always a bare name (PG system
catalog convention), so the query matches zero rows and falls into
the "object not found → placeholder ObjectDDL=''" branch. Result:
differ sees a pair of empty placeholders, emits modify_sqls=[], the
UI drawer shows "暂无数据" for FUNCTION / PROCEDURE main path.

API bypass with bare ObjectName=`fn_calc` already worked (case-3-3.md
acceptance line 67-68), confirming differ + extractor + signature
matching are all sound; the gap is the ObjectName entry contract.

Fix strategy (option D from todo.md candidate set — equivalent to
recommended option C "fullName + bareName double-key" but with a
simpler implementation at the plugin entry):

  - new helper stripObjectNameSignature(s) using strings.IndexByte('(')
    + strings.TrimRight to peel the signature off; bare-name input
    passes through unchanged;
  - new helper normalizeObjectsForExtractor that maps a slice of
    *DatabaseObject, copying entries whose ObjectName needs stripping
    (the original proto messages are NOT mutated; TABLE/VIEW pointers
    are reused as-is since their ObjectName never contains '(');
  - GetDatabaseDiffModifySQL and GetDatabaseObjectDDL both run the
    normaliser on info.DatabaseObjects before constructing the
    extractor input; SchemaName / ComparedSchemaName / response
    structure are untouched.

The plugin extractor's pg_get_function_arguments-based ObjectName
rebuild (function.go / procedure.go) means BOTH base and compared
sides return the canonical `fn_calc(integer)` form after lookup —
the differ map (ObjectName, ObjectType) keys align naturally without
any double-key injection. R-Q4-2 overload semantics preserved
end-to-end.

Test coverage (3 new test functions):
  - TestStripObjectNameSignature: 8 sub-cases covering parameter name
    + type / type-only / multi-args / no-args / bare name / empty /
    PROCEDURE TEXT-typed / space-before-paren.
  - TestNormalizeObjectsForExtractor: empty input short-circuit /
    function stripped without mutating original / TABLE pointer
    reuse / nil element preservation.
  - TestDriver_GetDatabaseDiffModifySQL_StripsObjectNameSignature:
    end-to-end sqlmock — passes `fn_calc(p integer)` to plugin,
    asserts extractor receives bare `fn_calc` (via ExpectQuery
    WithArgs) and the resulting modify SQL contains
    `CREATE OR REPLACE FUNCTION compared.fn_calc` with `(p integer)`
    signature preserved.

Defect source:
- docs/test/case-3-3.md (FUNCTION main path "暂无数据")
- docs/test/case-3-4.md (PROCEDURE main path "暂无数据" — same root)
- expertise_docs/episodic/sqle_gaussdb_modify_sql_object_name_signature_mismatch_20260521.md
- expertise_docs/semantic/sqle_plugin_overload_objectname_signature_20260521.md
  (overload identification contract R-Q4-2)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant