mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
fix(core): Advance Postgres IDENTITY sequences after entity import (#29762)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a270f2f35
commit
ca33060e0b
|
|
@ -1106,4 +1106,57 @@ describe('ImportService', () => {
|
|||
expect(mockDataTableDDLService.dropTable).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('advanceIdentitySequences', () => {
|
||||
it('should run setval for each identity column on Postgres', async () => {
|
||||
// @ts-expect-error overriding for the test
|
||||
mockDataSource.options = { type: 'postgres' };
|
||||
|
||||
mockEntityManager.query = jest
|
||||
.fn()
|
||||
// information_schema lookup for "workflow_dependency"
|
||||
.mockResolvedValueOnce([{ column_name: 'id' }])
|
||||
// setval call returns nothing meaningful
|
||||
.mockResolvedValueOnce(undefined)
|
||||
// information_schema lookup for "insights_metadata" (has 'metaId')
|
||||
.mockResolvedValueOnce([{ column_name: 'metaId' }])
|
||||
.mockResolvedValueOnce(undefined)
|
||||
// information_schema lookup for "user" (no identity columns)
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
await importService.advanceIdentitySequences(mockEntityManager, [
|
||||
'workflow_dependency',
|
||||
'insights_metadata',
|
||||
'user',
|
||||
]);
|
||||
|
||||
const setvalCalls = (mockEntityManager.query as jest.Mock).mock.calls.filter(
|
||||
([sql]) => typeof sql === 'string' && sql.includes('setval('),
|
||||
);
|
||||
expect(setvalCalls).toHaveLength(2);
|
||||
// Quoted table identifier passed through to pg_get_serial_sequence
|
||||
// (regclass folds unquoted names to lowercase, so the quoted form is required).
|
||||
expect(setvalCalls[0][1]).toEqual(['"workflow_dependency"', 'id']);
|
||||
expect(setvalCalls[1][1]).toEqual(['"insights_metadata"', 'metaId']);
|
||||
});
|
||||
|
||||
it('should be a no-op on SQLite', async () => {
|
||||
// SQLite is the default in beforeEach.
|
||||
mockEntityManager.query = jest.fn();
|
||||
|
||||
await importService.advanceIdentitySequences(mockEntityManager, ['workflow_dependency']);
|
||||
|
||||
expect(mockEntityManager.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should be a no-op when no tables are provided', async () => {
|
||||
// @ts-expect-error overriding for the test
|
||||
mockDataSource.options = { type: 'postgres' };
|
||||
mockEntityManager.query = jest.fn();
|
||||
|
||||
await importService.advanceIdentitySequences(mockEntityManager, []);
|
||||
|
||||
expect(mockEntityManager.query).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -500,6 +500,11 @@ export class ImportService {
|
|||
customEncryptionKey,
|
||||
);
|
||||
|
||||
// Postgres IDENTITY sequences don't auto-advance when explicit ids are
|
||||
// inserted, so subsequent implicit inserts would collide. Reset each
|
||||
// imported table's sequence to MAX(col).
|
||||
await this.advanceIdentitySequences(transactionManager, tableNames);
|
||||
|
||||
// After the data_table / data_table_column registry rows are imported,
|
||||
// recreate the dynamic backing tables (empty) so the imported tables work.
|
||||
await this.recreateDataTableUserTablesFromRegistry(transactionManager);
|
||||
|
|
@ -768,6 +773,50 @@ export class ImportService {
|
|||
this.logger.info(`✅ Recreated ${dataTables.length} data-table backing table(s)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance Postgres IDENTITY sequences to MAX(col) for every identity column
|
||||
* in the given tables. No-op on SQLite, where INTEGER PRIMARY KEY auto-bumps
|
||||
* its rowid sequence on inserts with explicit ids.
|
||||
*/
|
||||
async advanceIdentitySequences(
|
||||
transactionManager: EntityManager,
|
||||
tableNames: string[],
|
||||
): Promise<void> {
|
||||
if (this.dataSource.options.type !== 'postgres') return;
|
||||
if (tableNames.length === 0) return;
|
||||
|
||||
this.logger.info('\n🔢 Advancing Postgres IDENTITY sequences...');
|
||||
let advanced = 0;
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
const identityColumns: Array<{ column_name: string }> = await transactionManager.query(
|
||||
`SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = $1
|
||||
AND identity_generation IS NOT NULL`,
|
||||
[tableName],
|
||||
);
|
||||
|
||||
if (identityColumns.length === 0) continue;
|
||||
|
||||
const escapedTable = this.dataSource.driver.escape(tableName);
|
||||
for (const { column_name: columnName } of identityColumns) {
|
||||
const escapedCol = this.dataSource.driver.escape(columnName);
|
||||
await transactionManager.query(
|
||||
`SELECT setval(
|
||||
pg_get_serial_sequence($1, $2),
|
||||
COALESCE((SELECT MAX(${escapedCol}) FROM ${escapedTable}), 1),
|
||||
(SELECT MAX(${escapedCol}) FROM ${escapedTable}) IS NOT NULL
|
||||
)`,
|
||||
[escapedTable, columnName],
|
||||
);
|
||||
advanced++;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`✅ Advanced ${advanced} sequence(s) across ${tableNames.length} table(s)`);
|
||||
}
|
||||
|
||||
async disableForeignKeyConstraints(transactionManager: EntityManager) {
|
||||
const disableCommand = this.foreignKeyCommands.disable[this.dataSource.options.type];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user