From dcf4379ad37c54a3422fe0a50bdef439197b663e Mon Sep 17 00:00:00 2001 From: huyphan Date: Sat, 11 Jul 2026 19:19:29 -0600 Subject: [PATCH] fix(mariadb): emit NOCYCLE/NOCACHE on ALTER SEQUENCE (was invalid "NO CYCLE") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ALTER SEQUENCE path built its statement directly and never routed through MariaDB's wrapCreateSequence normalizer, so a MODIFIED sequence emitted `... NO CYCLE CACHE 1000`. MariaDB requires the single tokens NOCYCLE/NOCACHE and rejects the spaced form ("syntax error near 'CYCLE CACHE 1000'"), which aborted and rolled back the whole migration. Add a `wrapAlterSequence` dialect hook (mirroring wrapCreateSequence) and have the MariaDB dialect apply the same NO CYCLE→NOCYCLE / NO CACHE→NOCACHE normalization, factored into a shared helper so the CREATE and ALTER paths stay in sync. Default is a no-op, so Postgres/DB2/SQL Server/Oracle are unaffected. Found by the web E2E suite (mariadb went from FAIL to 10/10; full suite 6/6 green). Co-Authored-By: Claude Opus 4.8 --- .../core/src/modules/sql-dialect.interface.ts | 10 +++++++++ .../src/modules/sql-generator.module.test.ts | 14 +++++++++++++ .../core/src/modules/sql-generator.module.ts | 3 ++- .../src/providers/mysql/mysql.sql-dialect.ts | 21 +++++++++++++++---- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/core/src/modules/sql-dialect.interface.ts b/packages/core/src/modules/sql-dialect.interface.ts index 3199c9c..905108d 100644 --- a/packages/core/src/modules/sql-dialect.interface.ts +++ b/packages/core/src/modules/sql-dialect.interface.ts @@ -213,6 +213,16 @@ export interface SqlDialect { */ wrapCreateSequence?(qualifiedName: string, createSql: string): string; + /** + * Wrap the standard `ALTER SEQUENCE name ...;` into a dialect-safe form. + * Called with the qualified name and the full `ALTER SEQUENCE name ...;` string. + * MariaDB needs the CYCLE/CACHE negatives collapsed to the single tokens + * `NOCYCLE`/`NOCACHE` (the generic renderer emits `NO CYCLE`/`NO CACHE`, which + * MariaDB rejects with a syntax error) — the same normalization wrapCreateSequence + * applies on the CREATE path. Default: returns the statement unchanged. + */ + wrapAlterSequence?(qualifiedName: string, alterSql: string): string; + // ── Version-aware DROP hooks ──────────────────────────────────────────────── // Each hook receives the server version string detected at connect time. // When the version is undefined the hook should fall back to the modern syntax. diff --git a/packages/core/src/modules/sql-generator.module.test.ts b/packages/core/src/modules/sql-generator.module.test.ts index f465335..7842c18 100644 --- a/packages/core/src/modules/sql-generator.module.test.ts +++ b/packages/core/src/modules/sql-generator.module.test.ts @@ -554,6 +554,20 @@ describe('SqlGeneratorModule.generateMigrationPlan', () => { expect(ora.some((s) => /RESTART/i.test(s))).toBe(false); }); + it('renders a MODIFIED MariaDB sequence ALTER with single-token NOCYCLE/NOCACHE (MariaDB rejects "NO CYCLE"/"NO CACHE")', () => { + const diff: TableDiff = { + tableName: 'ORDER_SEQ', objectType: 'SEQUENCE', status: 'MODIFIED', + columnDiffs: [], indexDiffs: [], foreignKeyDiffs: [], + sourceTable: tableSchema({ name: 'ORDER_SEQ', objectType: 'SEQUENCE', sequence: { start: '1000', increment: '1', minValue: '1', maxValue: '9223372036854775806', cycle: false, cache: 0 } }), + targetTable: tableSchema({ name: 'ORDER_SEQ', objectType: 'SEQUENCE', sequence: { start: '1', increment: '1', cycle: true, cache: 1000 } }), + }; + const stmts = gen.generateMigrationPlan([diff], 'mariadb', { sourceSchema: 'demo_a', targetSchema: 'demo_b' }).flatMap((s) => s.statements); + const alter = stmts.find((s) => /ALTER SEQUENCE/.test(s))!; + expect(alter).toContain('NOCYCLE'); + expect(alter).toContain('NOCACHE'); + expect(alter).not.toMatch(/NO CYCLE|NO CACHE/); + }); + it('SQL Server changes a column default by dropping the named DF constraint then re-adding', () => { const diff: TableDiff = { tableName: 'ORDER_ITEMS', objectType: 'TABLE', status: 'MODIFIED', diff --git a/packages/core/src/modules/sql-generator.module.ts b/packages/core/src/modules/sql-generator.module.ts index eb0a445..7d276c6 100644 --- a/packages/core/src/modules/sql-generator.module.ts +++ b/packages/core/src/modules/sql-generator.module.ts @@ -586,7 +586,8 @@ export class SqlGeneratorModule { if (s.maxValue !== undefined) alter += ` MAXVALUE ${s.maxValue}`; alter += s.cycle ? ` CYCLE` : ` NO CYCLE`; if (s.cache !== undefined) alter += s.cache > 0 ? ` CACHE ${s.cache}` : ` NO CACHE`; - statements.push(alter + `;`); + const alterSql = alter + `;`; + statements.push(dialect.wrapAlterSequence?.(tableName, alterSql) ?? alterSql); } else if (obj.objectType === 'TYPE' && obj.sourceTable) { statements.push(`DROP TYPE ${tableName};`); statements.push(this.renderCreateType({ ...obj.sourceTable, name: tableName }, dialect)); diff --git a/packages/core/src/providers/mysql/mysql.sql-dialect.ts b/packages/core/src/providers/mysql/mysql.sql-dialect.ts index 5f2f614..01ffb80 100644 --- a/packages/core/src/providers/mysql/mysql.sql-dialect.ts +++ b/packages/core/src/providers/mysql/mysql.sql-dialect.ts @@ -67,6 +67,15 @@ const types = makeDialectTypeFns({ }, }); +/** + * MariaDB's sequence grammar wants NOCYCLE/NOCACHE as single tokens — the generic + * renderer's `NO CYCLE`/`NO CACHE` (with a space) is a syntax error. Shared by the + * CREATE and ALTER SEQUENCE wrappers so both paths stay in sync. + */ +function normalizeSequenceTokens(sql: string): string { + return sql.replace(/\bNO CYCLE\b/g, 'NOCYCLE').replace(/\bNO CACHE\b/g, 'NOCACHE'); +} + const mysqlDialect: SqlDialect = { identityClause(c: ColumnSpec): string { return c.identity ? ` AUTO_INCREMENT` : ''; @@ -131,10 +140,14 @@ const mysqlDialect: SqlDialect = { // generic renderer's default spacing) is a syntax error — and supports // IF NOT EXISTS directly (unlike SQL Server's OBJECT_ID-guard workaround). wrapCreateSequence(_qualifiedName: string, createSql: string): string { - return createSql - .replace(/\bNO CYCLE\b/g, 'NOCYCLE') - .replace(/\bNO CACHE\b/g, 'NOCACHE') - .replace(/^CREATE SEQUENCE /, 'CREATE SEQUENCE IF NOT EXISTS '); + return normalizeSequenceTokens(createSql).replace(/^CREATE SEQUENCE /, 'CREATE SEQUENCE IF NOT EXISTS '); + }, + + // The ALTER SEQUENCE path in the generator builds its statement directly (it + // doesn't route through wrapCreateSequence), so it needs the same NOCYCLE/NOCACHE + // normalization or MariaDB rejects `NO CYCLE`/`NO CACHE` with a syntax error. + wrapAlterSequence(_qualifiedName: string, alterSql: string): string { + return normalizeSequenceTokens(alterSql); }, ...types,