diff --git a/apps/web/src/frontend/lib/identity-insert-sql.test.ts b/apps/web/src/frontend/lib/identity-insert-sql.test.ts index 32b519ea..365758a2 100644 --- a/apps/web/src/frontend/lib/identity-insert-sql.test.ts +++ b/apps/web/src/frontend/lib/identity-insert-sql.test.ts @@ -13,9 +13,12 @@ const build = (dialect: string, generation?: string) => { describe('explicit identity INSERT is shaped per dialect', () => { it('adds OVERRIDING SYSTEM VALUE where the engine demands it', () => { - // Postgres/Db2 reject an explicit GENERATED ALWAYS value without this - // (SQLSTATE 428C9 / SQL0798N). - for (const d of ['postgres', 'cockroachdb', 'yugabytedb', 'db2']) { + // The Postgres family rejects an explicit GENERATED ALWAYS value without + // this (SQLSTATE 428C9). Db2 is NOT one of them despite a similar error: + // verified on Db2 LUW 11.5, the clause itself is SQL0104N there, so a Db2 + // GENERATED ALWAYS column is simply not writable — see the `unsupported` + // entry in dialect-identity-insert. + for (const d of ['postgres', 'cockroachdb', 'yugabytedb']) { expect(build(d, 'ALWAYS'), d).toContain('OVERRIDING SYSTEM VALUE'); // It must sit between the column list and VALUES, not anywhere else. expect(build(d, 'ALWAYS'), d).toMatch(/\)\s+OVERRIDING SYSTEM VALUE\s+VALUES/); diff --git a/apps/web/src/frontend/lib/rowDml.ts b/apps/web/src/frontend/lib/rowDml.ts index 5f63e85c..8c4d9b5b 100644 --- a/apps/web/src/frontend/lib/rowDml.ts +++ b/apps/web/src/frontend/lib/rowDml.ts @@ -303,8 +303,10 @@ export function buildPeekInsert(opts: { } if (Object.keys(row).length === 0) return { error: 'Nothing to insert.' }; - // Postgres and Db2 refuse an explicit value for a GENERATED ALWAYS identity - // column unless the statement says so (SQLSTATE 428C9 / SQL0798N). The + // Postgres refuses an explicit value for a GENERATED ALWAYS identity column + // unless the statement says so (SQLSTATE 428C9). Db2 is *not* in this group + // despite the similar error: it has no overriding clause at all (SQL0798N, + // and SQL0104N if you try one), so its support entry is `unsupported`. The // clause comes from a fixed capability table, never from user input. const support = writeIdentityGeneration ? identityInsertFor(dialect, writeIdentityGeneration) diff --git a/packages/sql/src/modules/dialect-identity-insert.test.ts b/packages/sql/src/modules/dialect-identity-insert.test.ts index b742a791..0236be77 100644 --- a/packages/sql/src/modules/dialect-identity-insert.test.ts +++ b/packages/sql/src/modules/dialect-identity-insert.test.ts @@ -19,10 +19,15 @@ describe('identityInsertSupport — every registered dialect has an answer', () for (const d of ['mysql', 'mariadb', 'tidb', 'sqlite']) { expect(identityInsertSupport(d).kind, d).toBe('native'); } - for (const d of ['postgres', 'cockroachdb', 'yugabytedb', 'db2']) { + for (const d of ['postgres', 'cockroachdb', 'yugabytedb']) { expect(identityInsertSupport(d).kind, d).toBe('overriding'); expect(identityInsertSupport(d).clause, d).toBe('OVERRIDING SYSTEM VALUE'); } + // Db2 is deliberately not in that list. It looks like Postgres from the + // error message alone (both complain about GENERATED ALWAYS), but verified + // on Db2 LUW 11.5: OVERRIDING SYSTEM VALUE is SQL0104N in every position, + // so there is no ceremony that makes the write legal. + expect(identityInsertSupport('db2').kind).toBe('unsupported'); for (const d of ['sqlserver', 'azuresql']) { expect(identityInsertSupport(d).kind, d).toBe('toggle'); } @@ -72,7 +77,16 @@ describe('identityInsertFor — how the column was declared', () => { it('keeps the overriding clause for ALWAYS', () => { expect(identityInsertFor('postgres', 'ALWAYS').kind).toBe('overriding'); - expect(identityInsertFor('db2', 'ALWAYS').kind).toBe('overriding'); + }); + + it('refuses a Db2 GENERATED ALWAYS identity outright', () => { + // Letting this through as `overriding` started the copy and then failed it + // mid-run with SQL0104N — the outcome this table exists to prevent. + const always = identityInsertFor('db2', 'ALWAYS'); + expect(always.kind).toBe('unsupported'); + expect(always.reason).toMatch(/GENERATED BY DEFAULT/i); + // BY DEFAULT is genuinely writable — confirmed against the live server. + expect(identityInsertFor('db2', 'BY DEFAULT').kind).toBe('native'); }); it('lets Oracle BY DEFAULT through', () => { diff --git a/packages/sql/src/modules/dialect-identity-insert.ts b/packages/sql/src/modules/dialect-identity-insert.ts index 6cbbe5ca..63d0c61b 100644 --- a/packages/sql/src/modules/dialect-identity-insert.ts +++ b/packages/sql/src/modules/dialect-identity-insert.ts @@ -91,7 +91,21 @@ const SUPPORT: Record = { postgres: OVERRIDING, cockroachdb: OVERRIDING, yugabytedb: OVERRIDING, - db2: OVERRIDING, + + // Not OVERRIDING: that clause is Postgres (and DB2 for i / z/OS). Verified on + // DB2 LUW 11.5 — `OVERRIDING SYSTEM VALUE` is SQL0104N in every position + // (before the column list, after it, and on INSERT … SELECT), and a plain + // insert into GENERATED ALWAYS is SQL0798N. There is no override, so listing + // DB2 as `overriding` let the copy start and then fail mid-run, which is the + // one outcome this table exists to prevent. + db2: { + kind: 'unsupported', + resyncSequence: true, + reason: + 'Db2 has no overriding clause for a GENERATED ALWAYS identity column ' + + '(SQL0798N, and SQL0104N for OVERRIDING SYSTEM VALUE). Turn off Include ' + + 'identity, or change the column to GENERATED BY DEFAULT.', + }, sqlserver: TOGGLE, azuresql: TOGGLE,