Skip to content

feat(odc): ODC workbench DB2 LUW support — plugin/db-browser/odc-core/service/migrate (Fixes actiontech/dms-ee#839)#24

Open
actiontech-bot wants to merge 19 commits into
dev/4.3.4from
feat-839
Open

feat(odc): ODC workbench DB2 LUW support — plugin/db-browser/odc-core/service/migrate (Fixes actiontech/dms-ee#839)#24
actiontech-bot wants to merge 19 commits into
dev/4.3.4from
feat-839

Conversation

@actiontech-bot
Copy link
Copy Markdown
Member

Summary

Add end-to-end DB2 LUW datasource support to ODC workbench.

  • odc-core: add DialectType.DB2 + ConnectType.DB2 + DB2_DRIVER_CLASS_NAME; route DB2 character LOB columns through Clob#length() to avoid jcc -4461.
  • odc-plugins: add connect-plugin-db2 (jcc driver, JDBC URL with catalog fallback, Db2DataSourceFactory) + schema-plugin-db2 (Db2SchemaExtension / Db2TableExtension / Db2ViewExtension / Db2SessionExtension); normalize DB2 internal version code; filter system schemas (NULLID/SYSTOOLS/SQLJ); fallback catalogName→defaultSchema.
  • db-browser: Db2SchemaAccessor (table/index/constraint/view detail), Db2StatsAccessor, Db2ObjectOperator, Db2TableEditor/Db2ColumnEditor/Db2IndexEditor/Db2ConstraintEditor, Db2NoOpPartitionEditor, Db2DMLBuilder (double-quoted identifier ANSI DML), Db2SqlBuilder; back-fill index/constraint columnNames + 1-based ordinalPosition; defend Db2IndexEditor against null columnNames; back-fill MON_GET_CONNECTION unqualified for DB2 session list.
  • odc-service: introduce DB2 dialect branches in ConnectConsoleService.queryTableOrViewData + DataConverters; promote DB2 defaultSchema to catalogName so plugin fallback has a value.
  • odc-migrate: seed column_data_type / support_view / support_kill_session / support_kill_query for DB2 in R_2_0_0__initialize_version_diff_config.sql.

Test plan

  • Unit tests: Db2SchemaAccessorTest 15/15, Db2StatsAccessorTest 9/9, Db2IndexEditorTest 4/4, GeneralLobMapperTest 14/14, Db2*Test overall 49+/49+ PASS
  • odc-service compile + executable.jar rebuild PASS
  • Browser end-to-end (playwright-cli):
    • case 1.x DB2 datasource sync 200 (catalog fallback + database_name → defaultSchema)
    • case 2.x resource tree (schema / table / view / constraint nodes)
    • case 3.x table detail + view list + queryData 200 (DB2 ANSI double-quoted identifiers + FETCH FIRST)
    • case 4.x table designer create / update with enableAutoIncrement=false + caseSensitivity=true
    • case 5.x LOB queryData (CLOB/DBCLOB/BLOB) returns rows + status=SUCCESS
    • case 6.x DB2 session list + Kill / Kill Query buttons

Fixes actiontech/dms-ee#839
Related: actiontech/odc-client PR (feat-839), actiontech/dms PR (feat-839)

…ASS_NAME

- DialectType: add DB2 enum + isDb2() predicate (B-01 / B-S1)
- ConnectType: add DB2(DialectType.DB2) (B-02, literal "DB2" pinned per compat-RISK-2)
- OdcConstants: add DB2_DRIVER_CLASS_NAME + DB2_DEFAULT_SCHEMA placeholder (B-03)
- DBBrowserFactory: add DB2 string constant (B-04)

Refs: actiontech/dms-ee#839
…r + Db2StatsAccessor + Db2ObjectOperator

- AbstractDBBrowserFactory: add buildForDB2() abstract method + DB2 case in create() (B-05)
- Db2SchemaAccessor: real impl backed by SYSCAT.* (B-06/B-07)
- Db2StatsAccessor: real impl backed by MON_GET_CONNECTION + SYSCAT.TABLES (B-08)
- Db2ObjectOperator: real impl for drop/showCount (B-09 real)
- 15 placeholder buildForDB2() across 8 editor + 7 template factories (B-09 placeholder + B-10)
- Unit tests: Db2SchemaAccessorTest + Db2StatsAccessorTest (mock JDBC, no real connection)

Refs: actiontech/dms-ee#839
…ce-layer DB2 branches

- server/plugins/pom.xml: register connect-plugin-db2 + schema-plugin-db2 modules (B-14)
- connect-plugin-db2: Db2ConnectionPlugin + Db2ConnectionExtension (getDriverClassName / generateJdbcUrl / getConnectionInitializers / test) + Db2SessionExtension (getCurrentSchema / getConnectionId 3-level fallback / getKillSessionSql FORCE APPLICATION / killQuery / setClientInfo=false) + Db2InformationExtension (B-11 / B-12 / B-13)
- IBM JDBC dep: com.ibm.db2:jcc:11.5.9.0 scope=provided in connect-plugin-db2/pom.xml only (design §2.7, IPLA via scope=provided)
- schema-plugin-db2: Db2SchemaPlugin + Db2DatabaseExtension + Db2TableExtension + utils/DBAccessorUtil routing DBSchemaAccessorFactory.setType(DialectType.DB2.name()) (B-15 / B-16)
- odc-service: ConnectTypeUtil (B-18) / ConnectionTesting (B-19) / ConnectionConfig.getDefaultSchema=username.toUpperCase (B-20) / OBConsoleDataSourceFactory (B-21) / TableDataService reuses MySQLDMLBuilder (B-22, design §2.5) / DruidDataSourceFactory validationQuery=select 1 from SYSIBM.SYSDUMMY1 (B-24) / DefaultDBSessionManage explicit DB2 branch (B-S2)
- odc-core: SqlCommentProcessor split via addLineMysql for DB2 (B-23)
- Unit tests (mock-only, no real JDBC): Db2ConnectionExtensionTest + Db2SessionExtensionTest (getConnectionId 3-level map case) + ConnectTypeUtilTest / DruidDataSourceFactoryTest / SqlCommentProcessorTest DB2 cases appended

Refs: actiontech/dms-ee#839
…BC URL

Root cause: Db2ConnectionExtension.generateJdbcUrl required a non-empty
catalogName via Validate.notEmpty, but the upstream DMS-EE
buildDatasourceBaseInfo contract (compat-RISK-5 D-02) only carries the DB2
database name via the defaultSchema field of ConnectionConfig — not via
catalogName. This caused every DB2 datasource sync to fail with
"catalog name can not be null", leaving status=INACTIVE and blocking all
schema / table / view / column metadata reads (batch-3 cases 2.1-2.4 plus
batch-4+ DB2 cases).

Fix (selected option A of A/B/C tradeoff, see fix-001.md):
- generateJdbcUrl: when catalogName is blank, fall back to defaultSchema
  (DB2 catalog = database, defaultSchema carries the DB name end-to-end
  from DMS-EE → odc-service → plugin)
- when the resolved catalog equals defaultSchema, omit the redundant
  ":currentSchema=" segment (DB2 driver defaults to user.toUpperCase(),
  which is exactly what ConnectionConfig.getDefaultSchema returns for DB2
  per B-20)
- improve the fail-fast error message to mention both catalogName and
  defaultSchema so future regressions surface the actual missing piece
- Db2ConnectionExtensionTest: add 4 regression cases (null fallback / empty
  fallback / catalog==schema case-insensitive / explicit catalog+distinct
  schema) and split the existing blank-catalog throws case into NPE-for-null
  and IAE-for-empty to keep Validate.notEmpty semantics explicit

Test: cd server/plugins/connect-plugin-db2 && mvn test → 18 PASS / 0 FAIL.

Refs: actiontech/dms-ee#839
…fallback has a value

Root cause (companion to fix(odc-plugin-db2) above): even with the plugin
layer fallback (catalogName ← defaultSchema), the actual value never reaches
the plugin because ConnectionService.create / update has a generic branch:

    if (!connection.getType().isDefaultSchemaRequired()) {
        connection.setDefaultSchema(null);
    }

For DB2 isDefaultSchemaRequired() is false (DB2 catalog ≠ schema in the
ODC model), so the DMS-EE-supplied database name carried via defaultSchema
gets silently nulled before JDBC URL build time. With both fields blank the
plugin-layer fallback has nothing to fall back to and we get the original
"catalog name can not be null" again.

Fix:
- ConnectionService.adaptDb2DatabaseToCatalog(): runs in both create() and
  update() right after environmentAdapter / connectionSSLAdaptor and BEFORE
  the isDefaultSchemaRequired branch. For DB2 only, if catalogName is blank
  and a raw defaultSchema was provided by the caller, copy that raw value
  into catalogName. Explicitly never overwrites an explicit catalogName.
- ConnectionService.clearDb2DefaultSchemaOnEntity(): after modelToEntity()
  the ConnectionConfig.getDefaultSchema() DB2 fallback would have synthesised
  user.toUpperCase() into the entity; that's fine for JDBC URL build but
  would persist a misleading value in the t_connection.default_schema column.
  Wipe it on the entity right before saveAndFlush so persistence stays
  honest (raw null) while runtime resolution keeps using getDefaultSchema().
- ConnectionConfig.getRawDefaultSchema(): @JsonIgnore helper that returns the
  persisted field as-is, bypassing the dialect-specific resolution in
  getDefaultSchema(). Required by adaptDb2DatabaseToCatalog so we can tell
  "the user provided no schema" from "fallback synthesised user.toUpperCase()".

Together with the plugin-layer fallback this delivers the compat-RISK-7
decision B (compatibility shim): DMS-EE buildDatasourceBaseInfo contract
stays unchanged, the DB2 catalog/database name flows defaultSchema →
catalogName → JDBC URL transparently, and explicit catalogName overrides
keep working.

Refs: actiontech/dms-ee#839
…ix-F)

Adds ConnectionServiceDb2AdapterTest covering the new adaptDb2DatabaseToCatalog
helper added in the previous commit:
- DB2 + raw defaultSchema=testdb → catalogName=testdb (DMS-EE D-02 contract)
- DB2 + explicit catalogName is preserved (no overwrite)
- MYSQL + defaultSchema=testdb is a no-op (adapter only acts on DB2)
- DB2 + blank defaultSchema is a no-op (no false positives)
- null connection guard
- getRawDefaultSchema vs getDefaultSchema: raw stays null while the DB2
  dialect-fallback synthesises user.toUpperCase()

Pure POJO checks; no JDBC, no Spring context. Six assertions cover the
adapter's invariants required by case 2.1 / 2.2 / 2.3 retest.

Refs: actiontech/dms-ee#839
…ils stops throwing

Bug A in fix-G: the IBM Data Server Driver for JDBC returns the DB2
product version as the IBM internal version code (for v11.5.8.0:
"SQL110580") rather than as a dotted decimal. ODC core consumes the
return value via VersionUtils.compareVersions which calls
Integer.parseInt on every dot-separated segment, so passing the raw
"SQL110580" raises NumberFormatException and propagates HTTP 400 out of
POST /api/v2/datasource/databases/{id}/sessions
(createSessionByDatabase). With session creation broken, the entire DB2
workbench downstream stack (resource-tree table/view/index/constraint
sub-nodes, SQL Console execution, "数据研发" submenu, batch-3 cases
2.2 / 2.3 / 2.4 / 3.x / 4.x / 5.x and 6.x) is blocked.

This change keeps the fix inside the DB2 plugin (no impact to other
dialects) and normalizes whatever the driver returns into a dotted
decimal that VersionUtils can parse:
  1. Already-dotted decimals pass through unchanged.
  2. Free-form text containing a dotted decimal (e.g. the
     "DB2 v11.5.9.0" returned by SYSIBMADM.ENV_INST_INFO.SERVICE_LEVEL)
     gets the dotted run extracted.
  3. IBM internal codes "SQL" + 6/7/8 digits get positionally decoded
     into V.R.M.F (e.g. SQL110580 → 11.5.8.0).
  4. Anything unrecognized falls back to the sentinel "0.0.0" so the
     caller still gets a parseable string and a WARN is logged.

Mock-only unit tests pin the bug A repro ("SQL110580" → "11.5.8.0"),
exercise the map of formats the IBM driver is observed to emit, and
defensively round-trip every output through VersionUtils.compareVersions
to prove no input can re-introduce a throw.

Refs: actiontech/dms-ee#839
…STOOLS/SQLJ are hidden

Bug C in fix-G: design.md §6 prescribes an 11-entry system-schema
blacklist for DB2 (SYSCAT / SYSIBM / SYSIBMADM / SYSIBMINTERNAL /
SYSIBMTS / SYSFUN / SYSPROC / SYSSTAT / SYSTOOLS / SYSPUBLIC / NULLID).
The original implementation in Db2SchemaAccessor.showDatabases filtered
SYSCAT.SCHEMATA by DEFINER NOT IN(...), but on DB2 11.5 several system
schemas (NULLID, SQLJ, SYSTOOLS) are created by the instance owner
(e.g. db2inst1) and therefore have DEFINER = db2inst1, not SYSIBM. The
DEFINER predicate let those rows slip into the user schema list — see
the JDBC truth table in
docs/test/screenshots/case-2-1-round2-schema-definer.txt and the
batch-3 round-2 failure analysis in docs/test/case-2-1.md.

The fix switches the filter dimension to SCHEMANAME, which is what the
design intended, and adds SQLJ to the blacklist (DB2 JDBC stored
procedures schema, meaningless to end users; previously slipped through
the DEFINER filter too).

A new mock-only test (showDatabases_sqlFiltersBySchemaNameWithFullBlacklist)
captures the SQL string with ArgumentCaptor and pins both shape
("SCHEMANAME NOT IN", not "DEFINER NOT IN") and the full
12-entry blacklist so a future refactor can't silently regress to the
original DEFINER-based filtering.

Refs: actiontech/dms-ee#839
…sor surface

fix-H for dms-ee#839 — round-fix-G regression test exposed bug D: ODC
`DBTableController#getTable` (5-tab table-detail page) and
`DBMetadataController#listIdentities?type=VIEW` (resource-tree view node)
collapsed with HTTP 500 because `Db2SchemaAccessor` shipped with 52
methods that still threw `UnsupportedOperationException("Not supported
yet")`. Any one of those methods on the aggregator path
(`OBMySQLTableExtension.getDetail`) is enough to crash the whole tab.

Two-tier implementation, both in the same commit because they sit on
one class and share helper state (the 12-entry SYSTEM_SCHEMA_BLACKLIST
constant introduced for the view list is also reused by the schema
list):

A. View surface (case 2.4 — DB2INST1 → "视图" subnode + SQL autocomplete)
   - listAllUserViews(viewNameLike): SELECT VIEWSCHEMA, VIEWNAME FROM
     SYSCAT.VIEWS WHERE VIEWSCHEMA NOT IN (<12-entry blacklist>)
   - listAllSystemViews(viewNameLike): inverse (VIEWSCHEMA IN (...))
   - listAllViews(): union of user + system
   - listViews(schema): per-schema variant for tree expansion
   - showSystemViews(schema): per-schema list for autocomplete
   - getView(schema, name): null (controller maps to 404, not 500)

B. Table-detail aggregator surface (case 2.3 — TEST_ORDERS detail page)
   - getDatabase(schema): minimal POJO so OBMySQLTableExtension.getDetail
     can proceed (DB2 ≈ Oracle/PG where schema is the user-facing DB id)
   - listDatabases(): full DBDatabase list (was missing wrapper)
   - listTableColumns(schema, List<table>): batch variant — loops the
     per-table SYSCAT.COLUMNS query (the existing per-table variant
     already worked); avoids throwing on every detail-page render
   - listTableConstraints(schema, table): SYSCAT.TABCONST with P/U/F/K
     -> DBConstraintType mapping (PRIMARY_KEY/UNIQUE_KEY/FOREIGN_KEY/
     CHECK/UNKNOWN)
   - listTableIndexes(schema, table): SYSCAT.INDEXES with UNIQUERULE
     P/U -> unique=true, D -> unique=false
   - switchDatabase / syncExternalTableFiles: void / false (no-op
     because DB2 schema is fixed at JDBC connect time)

C. Safe-degradation surface (remaining unsupported methods)
   Every other previously-throwing method now returns empty List/Map /
   null / false. Out-of-scope for this release (design.md §6 explicitly
   covers only schema/table/view/column/index/constraint):
   - PL objects: listFunctions/Procedures/Packages/PackageBodies/
     Triggers/Types, listSequences/Synonyms — empty list
   - MView: listMViews / listAllMViewsLike / refreshMVData /
     listMView{Constraints,RefreshRecords,Indexes} / getMView — empty/null/false
   - Variables / charset / collation — empty list (DB2 surfaces via
     SYSPROC.* but ODC variables page not wired here)
   - Basic{Table,View,ExternalTable,MView,ColumnsInfo}Columns batch +
     per-table — empty (autocomplete optimization; falls back to
     heavier per-table listTableColumns path)
   - Schema-wide batch: listTable{Indexes,Constraints,Options,
     Partitions} / listTableRangePartitionInfo / listSubpartitions /
     listPartitionTables — empty
   - getTableDDL: empty string (db2look / DB2LK_GENERATE_DDL out of
     scope; ".contains(...)" safe on "")
   - getTableOptions / getPartition / getTableColumnGroups / single-
     object getX (View/Function/Procedure/Package/Trigger/Type/Sequence/
     Synonym) — null (controller maps null -> 404, not 500)
   - listUsers — empty (no DB2 user picker in this release)
   - getTables(schema, list) — empty Map (DB2 plugin's Db2TableExtension
     orchestrates columns+indexes+constraints individually; this batch
     path is not used)

D. Tests (Db2SchemaAccessorTest, +5 new mock-only cases on top of the
   8 baseline from fix-G):
   - listViews_returnsViewIdentities (view path)
   - listAllUserViews_filtersSystemSchemasAndSupportsLikePredicate
     (view + blacklist single-source-of-truth)
   - listAllSystemViews_inverseOfUserViews (regression guard against
     accidentally swapping VIEWSCHEMA NOT IN <-> IN)
   - listTableColumnsBatch_loopsPerTableAndKeysByName
   - listTableColumnsBatch_emptyInputReturnsEmptyMap (with explicit
     List<String> cast to disambiguate from String overload)
   - listTableIndexes_uniqueRuleMapping (P/D mapping)
   - listTableConstraints_typeMapping (P/U/F constraint type mapping)
   - getDatabase_returnsMinimalIdEqualsName (aggregator path
     prerequisite)
   - unsupportedPlaceholders_degradeToEmptyInsteadOfThrowing
     (regression guard for the 18 spot-checked methods that were
     previously throwing — pins them to empty / false / null)

   13 / 13 PASS; baseline already-known
   SqlServerTriggerTemplateTest.setTableName compile failure is
   pre-existing and unrelated (see
   expertise_docs/episodic/odc_baseline_test_failures_2026-05-19.md).

E. Compatibility
   - No new RISK in docs/spec/compat_risks.md: the
     DBSchemaAccessor contract was already published; bug D is "plugin
     never honored the contract". Non-DB2 plugins untouched.
   - vendor / go.mod / pnpm-lock not touched.
   - No new startup hook / no AutoMigrate / no schema migration.

Refs: docs/test/case-2-3.md round-fix-G; docs/test/case-2-4.md
round-fix-G; expertise_docs/episodic/
odc_db2_fixG_round_bugD_unsupported_methods_2026-05-19.md
Fixes: dms-ee#839
… page stops 500

fix-H bug D-2 (table-detail page 5 tabs collapsed with HTTP 500). The
fix-H accessor commit (b9a03fc) implemented the column / constraint /
index / DDL accessor methods but the table-detail aggregator path still
crashed because `Db2TableExtension extends OBMySQLTableExtension` and
inherits two OB-MySQL-specific code paths:

1. `getStatsAccessor()` → `DBAccessorUtil#getStatsAccessor` (OB-MySQL
   plugin's util) → `getDbVersion()` → `new OBMySQLInformationExtension()
   .getDBVersion(connection)` → `OBUtils.getObVersion(connection)` which
   runs `show variables like 'version_comment'`. DB2 jcc treats this as
   a non-query (DB2 has no `SHOW VARIABLES` syntax) and throws
   `ERRORCODE=-4476 (executeQuery used for update)` — the whole
   table-detail page returns 500.

2. `getDetail()` builds an `OBMySQLGetDBTableByParser(ddl)` over the
   string returned by `schemaAccessor.getTableDDL()`. For DB2 the
   accessor returns "" by design (db2look / DB2LK_GENERATE_DDL are
   out-of-scope per design.md §6), and the MySQL DDL parser would also
   be invoked on a non-MySQL grammar. The inherited body also calls
   `schemaAccessor.listTableColumnGroups()` which doesn't apply to DB2.

Fix: override both methods in `Db2TableExtension`:

- `getStatsAccessor(connection)` returns a fresh `Db2StatsAccessor`
  bound to the live JdbcOperations. `Db2StatsAccessor` (added in
  feat-839 commit d36349c, `libs/db-browser/.../stats/db2/`) doesn't
  need a version probe — it queries SYSCAT.TABLES `CARD` / `NPAGES`
  directly, so `OBUtils.getObVersion` is bypassed entirely.

- `getDetail(connection, schema, table)` re-assembles the `DBTable`
  payload from four DB2-native accessor calls (columns / constraints /
  indexes / DDL / options) plus `getDb2TableStats(...)` (defensive
  wrapper around `Db2StatsAccessor.getTableStats`). Partition is set
  to null (DB2 partitioned tables are out of scope) and
  listTableColumnGroups is skipped (DB2 has no OB-style column groups).
  This is the same data shape OB-MySQL produces, just sourced via SYSCAT
  instead of the MySQL DDL parser.

- `syncExternalTableFiles()` no longer throws — DB2 has no external
  tables in this release, so returning false matches the accessor
  contract that fix-H established in commit b9a03fc.

Self-test (fix agent):
- bug D-1 (case 2.4): listAllUserViews via
  `/odc_query/api/v2/connect/sessions/<sid>/metadata/identities?type=
  VIEW&currentOrganizationId=1` returns HTTP 200 with 2 user views
  (DB2INST1.TEST_VIEW_001, DB2INST1.V_TEST_ORDERS_SUMMARY) — was 500
  before fix-H accessor commit; PASS.
- bug D-2 (case 2.3): getTable for TEST_ORDERS via
  `/odc_query/api/v2/connect/sessions/<sid>/databases/DB2INST1/tables/
  VEVTVF9PUkRFUlM%3D?currentOrganizationId=1` — verified after this
  commit + restart.

Touched files: schema-plugin-db2 only (1 file, ~110 lines net). No
db-browser change. No vendor / go.mod change. CE/EE: this commit is
DB2-plugin-specific code; ce-ee-split applies in code_review phase.

Refs: docs/test/case-2-3.md (fix-G round), docs/dev/fix_reports/
fix-G-db2-tree-meta.md §7, OBUtils.java:307. Companion to commit
b9a03fc (db-browser Db2SchemaAccessor implementation).
Fixes: dms-ee#839
fix-I bug E: the v1 view controller (GET /api/v1/view/list/{sid}) was 400-ing
with "Feature extension point is not supported for DB2" because schema-plugin-db2
shipped a Db2DatabaseExtension and a Db2TableExtension but no Db2ViewExtension.
The pf4j manifest (META-INF/extensions.idx, auto-generated from @extension) was
therefore missing the ViewExtensionPoint binding for DB2, and the front-end
resource tree silently omitted the "视图" category node under each DB2 schema
(case 2.4 view-list regression).

Changes:
* server/plugins/schema-plugin-db2/Db2ViewExtension — extends OBMySQLViewExtension
  and overrides three protected hooks:
  - getSchemaAccessor → DBAccessorUtil.getSchemaAccessor (routes to Db2SchemaAccessor)
  - getOperator → Db2ObjectOperator (DB2 double-quoted identifiers, not MySQL backticks)
  - getTemplate → OB-MySQL view template factory (DBViewTemplateFactory.buildForDB2()
    throws UnsupportedOperationException; the OB-MySQL template emits a generic
    "select * from ..." scaffold that's DB2-compatible for the wizard skeleton)
* libs/db-browser Db2SchemaAccessor.listViews — switch the SQL from
  "SELECT VIEWSCHEMA, TABNAME FROM SYSCAT.VIEWS" to
  "SELECT VIEWSCHEMA, VIEWNAME FROM SYSCAT.VIEWS". SYSCAT.VIEWS does not surface
  a TABNAME column (that's on SYSCAT.TABLES); the inherited skeleton produced
  SQLCODE=-206 (SQLERRMC=TABNAME) as soon as fix-I wired the v1 controller in.
* server/odc-migrate ... R_2_0_0__initialize_version_diff_config.sql — append a
  support_view='true' row for DB2 so VersionDiffConfigService#getSupportFeatures
  includes view in the front-end supports[] array; without it, even with the
  ViewExtensionPoint registered, the resource tree still hides "视图".
* Db2ViewExtensionTest — 9 mock-only unit tests pin the list/listSystemViews/
  getDetail delegation, the drop operator routing, and the @extension annotation
  + OBMySQLViewExtension inheritance contract.
* Db2SchemaAccessorTest.listViews_returnsViewIdentities — pin VIEWNAME (not
  TABNAME) in the executed SQL via ArgumentCaptor.

Issue: dms-ee#839
Fixes case 2.4 view-list regression exposed by fix-H round complementary test.
…ta (and peer)

fix-I bug F: ConnectConsoleService.queryTableOrViewData threw
IllegalArgumentException("Unsupported dialect type, DB2") on the very first
call because its dialect-routing if/else chain covered isMysql / isOracle /
isDm / isDoris / isTidb / isSqlServer but not isDb2. The HTTP request 400-ed
and the UI data tab spinner never resolved, blocking case 3.1 (table data
pagination + type echo) and case 3.2 (CLOB/BLOB cell view).

Changes:
* ConnectConsoleService.queryTableOrViewData L175 — append an isDb2 branch
  that selects OracleSqlBuilder (not MySQLSqlBuilder). Both DB2 and Oracle
  quote identifiers with ANSI double quotes; the MySQL builder emits backticks
  which DB2 jcc rejects with SQLCODE=-104 SQLSTATE=42601.
* ConnectConsoleService.queryTableOrViewData L208 — append an isDb2 branch on
  the row-limit clause. DB2 uses ANSI "FETCH FIRST n ROWS ONLY", not MySQL-style
  "LIMIT n". Although DB2 11.x has a sql_compat MYSQL mode that accepts LIMIT,
  the default DB2 grammar (which our test instance runs) rejects it; FETCH FIRST
  is portable across DB2 versions and matches our 11.5 test bed.
* DataConverters.java — peer of the same dialect grep: TableDataService's data-
  edit path (case 4.x) routes DB2 through MySQLDMLBuilder.toSQLString, which
  delegates to DataConvertUtil → DataConverters. DataConverters threw
  "Illegal DialectType DB2" for any dialect other than Oracle/MySQL/Doris/TiDB,
  which would 500 the row-edit save flow once case 4.x runs. Reuse initForMysqlMode
  for DB2 — per design.md §2.5, DB2 and MySQL agree on basic string/numeric
  literal grammar for the editor MVP.

Verified by global grep of dialectType.is{Mysql,Oracle,Dm,Doris,Tidb,SqlServer}
across odc-service; remaining hits are single-point checks where DB2 safely
falls into the else branch (e.g. OdcStatementCallBack only enables Oracle-
specific dbms_output cleanup, ConnectionSessionFactory only routes Oracle
schemaName logic). Other dialect-routing if/else chains already had DB2
branches added in earlier feat-839 commits (DruidDataSourceFactory L135,
TableDataService L101, ConnectionTesting L166).

Issue: dms-ee#839
Fixes case 3.1 / 3.2 (and unblocks case 4.x edit path) exposed by fix-H round
complementary test.
…jcc -4461

DB2 jcc rejects ResultSet#getBinaryStream() on CLOB / DBCLOB / NCLOB columns
with ERRORCODE=-4461 / SQLSTATE=42815 ("data conversion invalid: result
column type wrong"). After fix-I unblocked the queryTableOrViewData HTTP
path (400 -> 200), this jcc error became the next bug along the recursive
chain: queryData now returns 200 + status=FAILED + rows=[], the data tab
spinner never resolves and the user sees no rows for any DB2 table that has
a LOB column.

Root cause: GeneralLobMapper.mapCell always calls CellData#getBinaryStream
even when the column is a character LOB. For OB / MySQL / Oracle this happens
to work because their drivers happily expose the BLOB-style stream for CLOBs;
DB2 jcc is strict and reports the type error.

Fix (odc-core layer, not plugin-specific):

* GeneralLobMapper now classifies columns by typeName. CLOB / NCLOB / DBCLOB
  go through Clob#length() (jcc-safe); other LOB types continue to use
  Blob#length() when available and fall back to InputStream#available() so
  existing OB / MySQL behaviour is preserved.
* GeneralLobMapper now recognises DBCLOB so the DB2 double-byte CLOB column
  is treated as a LOB instead of being rendered via getString.
* ResultSetCachedElementFactory.isCharacterType now covers DB2 dialect
  (CLOB / DBCLOB / NCLOB) so the "view large field" path uses
  getCharacterStream rather than getBinaryStream.
* DataTypeUtil.BINARY_DATA_TYPES adds "dbclob" so the cached virtual element
  factory routes DB2 DBCLOB columns through the binary/character branch
  instead of falling through to resultSet.getObject.

Tests:

* GeneralLobMapperTest gains four new cases covering Blob handle preferred
  path, Clob length path, DBCLOB Clob length path, and zero-length Clob.
* Added TestBlob / TestClob stubs and extended LobCellData with a flag for
  driver-provided Blob handles; existing two cases for binary LOB
  fall-through still pass.

All 14 GeneralLobMapperTest cases pass locally. OracleBinaryNumberMapperTest
and OracleTimeStampMapperTest remain on the baseline failure list (Oracle
driver ClassDef missing, unrelated to this fix, verified via git stash
double-run).

Refs actiontech/dms-ee#839
…COLUSE

fix-L commit-1 (bug N1).

Db2SchemaAccessor.listTableConstraints previously only read SYSCAT.TABCONST and
set schemaName/tableName/name/type, leaving columnNames=null. This caused
BaseDMLBuilder.getPrimaryConstraint to NPE at
`for (String col : constraint.getColumnNames())` for every DB2 table that has a
real PK/UK constraint — i.e. every editable table — and HTTP 500 the
batchGetModifySql endpoint that backs cell-edit submission in the workbench.

Fix: after pulling the constraint list from SYSCAT.TABCONST, query
SYSCAT.KEYCOLUSE per constraint (ORDER BY COLSEQ) to back-fill columnNames for
PK / UK / FK, and additionally join SYSCAT.REFERENCES + KEYCOLUSE for FK to fill
referenceSchemaName / referenceTableName / referenceColumnNames. CHECK
constraints have no rows in KEYCOLUSE and keep columnNames as an empty list
(non-null) so callers iterating it are NPE-safe.

Unit test: Db2SchemaAccessorTest#listTableConstraints_backFillsColumnNamesFromKeyColUse
verifies that a PK constraint returned from listTableConstraints has a non-null,
ordered columnNames list (ID, ORDER_NO). 14/14 tests pass.

Refs actiontech/dms-ee#839
…ted identifiers

fix-L commit-2 (bug N2).

Before this change TableDataService routed DB2 sessions through MySQLDMLBuilder,
which emits MySQL backticks for identifiers — e.g.
`insert into `DB2INST1`.`TEST_ORDERS`(`ID`,...) values (100,...)` — that
DB2 rejects with SQLCODE=-7 / SQLSTATE=42601 in the parser. This blocks cell
edit and row insert end-to-end on every DB2 table in the workbench.

Fix:
 - new Db2SqlBuilder in libs/db-browser that quotes identifiers with ANSI double
   quotes (DB2 native) and values with single quotes;
 - new Db2DMLBuilder in odc-service that extends BaseDMLBuilder and wires the
   above SqlBuilder, with DB2-shaped LOB type lists (clob/blob/dbclob/nclob);
 - TableDataService.batchGetModifySql now picks Db2DMLBuilder for DialectType.isDb2()
   instead of falling through to MySQLDMLBuilder.

Unit tests: Db2DMLBuilderTest exercises InsertGenerator / UpdateGenerator /
DeleteGenerator with a mocked DMLBuilder backed by Db2SqlBuilder and asserts:
  - no backtick character appears anywhere in the generated SQL;
  - schema/table/columns are wrapped in ANSI double quotes ("DB2INST1"."TEST_ORDERS", "ID", ...);
  - statement keywords are lowercase 'insert into' / 'update ' / 'delete from '.
3/3 tests pass.

Stack: this commit depends on fix-L commit-1 (55fb552) which back-fills
SYSCAT.KEYCOLUSE columnNames so BaseDMLBuilder.getPrimaryConstraint no longer
NPEs before this builder gets a chance to run.

Refs actiontech/dms-ee#839
…on list

Db2StatsAccessor#listAllSessions and #currentSession previously qualified
MON_GET_CONNECTION with the SYSIBMADM schema, which raised SQLCODE=-440
SQLSTATE=42884 (function not found) on DB2 v11.5 because the table
function actually lives in SYSPROC; SYSIBMADM only exposes MON_* views.
This blocked /api/v1/dbsession/list/<sid> with HTTP 500 and made the
ODC session-management page show an empty body (Test-014/-015 cases
6.1 / 6.2 / 7.1 P1 evidence in docs/test/case-6-1.md and case-6-2.md).

Three concrete corrections:

1. Drop the SYSIBMADM. qualifier; rely on DB2's name resolution to find
   the function under SYSPROC.
2. Replace APPL_STATUS with WORKLOAD_OCCURRENCE_STATE for the session
   state column. APPL_STATUS does not exist in MON_GET_CONNECTION
   (jcc raised SQLCODE=-206); WORKLOAD_OCCURRENCE_STATE is the canonical
   per-connection state field in DB2 11.5 MON_GET_CONNECTION schema.
3. Replace CONNECTION_HANDLE() with MON_GET_APPLICATION_HANDLE() in
   currentSession. CONNECTION_HANDLE() does not exist in DB2 11.5
   (jcc raised SQLCODE=-440). MON_GET_APPLICATION_HANDLE() returns the
   current application's handle, which is exactly what we need to pass
   into MON_GET_CONNECTION for the current session row.

Additionally, COALESCE CLIENT_HOSTNAME with CLIENT_IPADDR for the host
column because CLIENT_HOSTNAME is null on default DB2 client setups.

Unit tests (Db2StatsAccessorTest) extended:
- listAllSessions_sqlShape: regression guard, asserts SQL no longer
  contains SYSIBMADM.MON_GET_CONNECTION / APPL_STATUS and uses
  WORKLOAD_OCCURRENCE_STATE + COALESCE host.
- currentSession_sqlShape: asserts no SYSIBMADM. / no CONNECTION_HANDLE()
  / uses MON_GET_APPLICATION_HANDLE() / FETCH FIRST 1 ROWS ONLY.
- currentSession_mapsRow and currentSession_returnsEmptyOnException:
  RowMapper field mapping + ms-to-s conversion + empty-fallback on
  JDBC exception.

All 9 tests pass (mvn -pl libs/db-browser test -Dtest=Db2StatsAccessorTest
-Dmaven.compiler.failOnError=false). jcc-direct probe before the patch
confirms the three corrected SQL forms execute against the DB2 11.5
target (22 active sessions returned for listAllSessions; current session
row returned for currentSession).

Refs actiontech/dms-ee#839
Without these two rows in odc_version_diff_config the front-end
supportFeature.enableKillSession / enableKillQuery stay false for any
DB2 datasource, so the session management panel hides the Kill button
even though fix-M has already wired Db2StatsAccessor + the FORCE
APPLICATION pathway against DB2 11.5 LUW (case 6.2 / O2).

Pattern mirrors the SQL_SERVER / DORIS always-on rows (min_version='0').

This is the third leg of the dialect feature tripartite contract
documented in expertise_docs/procedural/odc_dialect_view_node_tripartite_contract_2026-05-19.md:

  1. SchemaAccessor SQL    -> Db2StatsAccessor (fix-M)
  2. ExtensionPoint        -> Db2SessionExtension (existing)
  3. MetaDB seed           -> this commit

Fixes oceanbase#839
Issue: dms-ee#839
DB2 table-designer 列类型下拉为空 / 保存表结构 500 两个 bug 同源:
odc_version_diff_config 缺 DB2 的 column_data_type 行,且
DBTableEditorFactory.buildForDB2() 等仍 throw UnsupportedOperationException。

变更:
1. odc-migrate: 追加 DB2 的 column_data_type seed(同 SQL_SERVER 模式,
   min_version='9.7' 覆盖所有目标版本)。
2. db-browser: 新增 db2/Db2ColumnEditor、Db2IndexEditor、Db2ConstraintEditor、
   Db2TableEditor、Db2NoOpPartitionEditor,使用 DB2 LUW 语法(per-attribute
   ALTER COLUMN SET DATA TYPE/SET NOT NULL/SET DEFAULT;schema 级 CREATE/DROP
   INDEX;COMMENT ON TABLE/COLUMN)。
3. 将 DBTableEditorFactory / DBTableColumnEditorFactory / DBTableIndexEditorFactory
   / DBTableConstraintEditorFactory / DBTablePartitionEditorFactory 的
   buildForDB2() 由 throw 改为返回真实实例。
4. schema-plugin-db2 DBAccessorUtil 增 getTableEditor(connection),固定
   dbVersion='11.5' 绕开 OB-MySQL 的 "show variables like 'version_comment'"
   探活语句(DB2 jcc 报 -4476)。
5. Db2TableExtension override generateCreateDDL / generateUpdateDDL 改走
   DB2 native editor,不再落入 OB-MySQL 路径。

Issue: dms-ee#839
…+ defend Db2IndexEditor against null columnNames

Fixes the workbench's "POST generateUpdateTableDDL HTTP 400/500 message=null"
error that blocked every "edit a column" operation on DB2 tables carrying any
index (Issue dms-ee#839, fix_report_20260601_031142 P0-2).

Root cause: Db2SchemaAccessor.listTableIndexes only queried SYSCAT.INDEXES and
never populated columnNames / ordinalPosition. DBTableIndexEditor.
generateUpdateObjectListDDL then treated every existing index as "new" (because
ordinalPosition was null) and dispatched it into Db2IndexEditor.
generateCreateObjectDDL, which NPE'd on `index.getColumnNames().stream()`.

Three coordinated changes:
* Db2SchemaAccessor.listTableIndexes: JOIN SYSCAT.INDEXES with SYSCAT.INDEXCOLUSE
  (mirroring SqlServerSchemaAccessor.listTableIndexes), aggregate by INDNAME,
  assign a 1-based ordinalPosition via AtomicInteger, always store columnNames
  as a List (never null). UNIQUERULE='P' → primary=true + unique=true + UNIQUE,
  'U' → UNIQUE, 'D' → NORMAL.
* Db2SchemaAccessor.listTableConstraints: also assign 1-based ordinalPosition so
  DBTableConstraintEditor.generateUpdateObjectListDDL does not mistake every
  existing PK/UK/FK/CHECK for "new" during column edits.
* Db2IndexEditor.generateCreateObjectDDL: defensive short-circuit returns ""
  when columnNames is null/empty instead of throwing NPE, matching the
  upstream contract (an empty DDL string is concatenated as no-op).

Tests: Db2SchemaAccessorTest gains a 3-row regression test pinning
columnNames + ordinalPosition; listTableIndexes_uniqueRuleMapping updated to
the JOIN row shape; listTableConstraints_typeMapping asserts ordinalPosition
1/2/3; new Db2IndexEditorTest covers null/empty/populated CREATE INDEX paths.
Db2*Test 28 PASS, odc-service / odc-core compile.

Related-Issue: actiontech/dms-ee#839
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants