chore(core): Require SQLite migration recreate acknowledgements (#31202)

This commit is contained in:
Tomi Turtiainen 2026-06-04 16:42:27 +03:00 committed by GitHub
parent 2993afb31d
commit bca1e08ea8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 1238 additions and 481 deletions

View File

@ -117,6 +117,7 @@ Run through this before requesting review. Each item is a real, recurring review
- [ ] **Sparse-unique columns:** use a partial index `WHERE col IS NOT NULL`. — [Index Management](#index-management)
- [ ] **Composite index column order** matches your actual `WHERE` / `ORDER BY` usage. — [Index Management](#index-management)
- [ ] **Entity ↔ migration parity**: column types, `notNull`, defaults, FKs, `@Index` decorators all match. — [Schema/Entity Drift](#schemaentity-drift)
- [ ] **If using `addColumns`, `dropColumns`, `addNotNull`, `dropNotNull`, `addEnumCheck`, or `dropEnumCheck`:** verified whether the target table has incoming FKs. If so, either set `withFKsDisabled = true as const` (in a `sqlite/` subclass if this is a `common/` migration) or use raw `ALTER TABLE ADD COLUMN` for nullable/defaulted columns. — [SQLite table recreation risk](#sqlite-table-recreation-risk)
- [ ] **No live-app value imports** in the migration body. Inline types/utility code locally. — [Never import entities as values](#never-import-entities-as-values)
- [ ] **`async down()` was tested locally**: `pnpm start && pnpm start -- db:revert && pnpm start` on **both** SQLite and Postgres. — [Reversibility](#reversibility)
- [ ] **One logical change per migration**; split unrelated table changes into separate files. — [Don't combine independent schema changes](#dont-combine-independent-schema-changes)
@ -218,7 +219,7 @@ export class MigrateThing1234567890000 implements IrreversibleMigration {
const { schemaBuilder: { addColumns, column, createIndex } } = ctx;
// One-liner DSL calls stay inline — naming them adds no information.
await addColumns('my_table', [column('slug').varchar(255)]);
await addColumns('my_table', [column('slug').varchar(255)], { recreatesOnSqlite: true });
// The non-trivial step gets a named method.
await this.backfillSlugs(ctx);
@ -350,6 +351,51 @@ export class CreateMyTable1234567890000 implements ReversibleMigration {
}
```
### SQLite table recreation risk
Six DSL methods trigger **full table recreation** on SQLite — TypeORM internally creates a temp copy, drops the original, and renames:
| Method | TypeORM internal call |
|---|---|
| `addColumns()` | `queryRunner.addColumns()` |
| `dropColumns()` | `queryRunner.dropColumns()` |
| `addNotNull()` | `queryRunner.changeColumn()` |
| `dropNotNull()` | `queryRunner.changeColumn()` |
| `addEnumCheck()` | `queryRunner.changeColumn()` |
| `dropEnumCheck()` | `queryRunner.changeColumn()` |
All six require a final options parameter with `recreatesOnSqlite: true` — TypeScript rejects calls that omit it.
**The danger:** If the target table has incoming FK constraints with `CASCADE` from other tables, the `DROP TABLE` during recreation fires cascading deletes and **wipes rows from those referencing tables**.
**Decision tree:**
1. Does the target table have incoming FK constraints from other tables?
- **No** → Safe to use the DSL method directly (with the ack parameter).
- **Yes** → Continue to step 2.
2. Is this an `addColumns` call where every new column is nullable or has a default?
- **Yes** → Use raw `ALTER TABLE ADD COLUMN` instead (avoids table recreation entirely):
```typescript
await runQuery(
`ALTER TABLE ${escape.tableName('my_table')} ADD COLUMN ${escape.columnName('col')} TEXT`,
);
```
See `1733133775640-AddMockedNodesColumnToTestDefinition.ts` for a real example.
- **No** → Continue to step 3.
3. Set `withFKsDisabled = true as const` on the migration class. For common migrations, create a SQLite subclass in `sqlite/` that extends the common migration and adds the flag:
```typescript
// sqlite/1234567890000-MyMigration.ts
import { MyMigration1234567890000 as BaseMigration } from '../common/1234567890000-MyMigration';
export class MyMigration1234567890000 extends BaseMigration {
withFKsDisabled = true as const;
}
```
**How `withFKsDisabled` works:** The migration wrapper calls `PRAGMA foreign_keys=OFF` before `up()`/`down()`, runs the migration inside a manual transaction, then re-enables foreign keys. This prevents CASCADE from firing during the internal table drop. It also sets `transaction = false` to avoid TypeORM's default transaction (since SQLite can't nest transactions with PRAGMA changes).
> **Note:** On Postgres, these methods use `ALTER TABLE` directly and don't recreate the table. The risk is SQLite-specific.
### Column types
**Match column type to value semantics.** Never `varchar` as a catch-all for non-string values — storing numbers as strings loses sort order, range queries, and SUM/AVG aggregations.
@ -570,7 +616,11 @@ When a migration both adds a column and backfills data, structure it clearly wit
```typescript
export class AddAndBackfillColumn1234567890000 implements IrreversibleMigration {
async up(ctx: MigrationContext) {
await ctx.schemaBuilder.addColumns('my_table', [ctx.schemaBuilder.column('newCol').text]);
await ctx.schemaBuilder.addColumns(
'my_table',
[ctx.schemaBuilder.column('newCol').text],
{ recreatesOnSqlite: true },
);
await this.backfillNewCol(ctx);
}
@ -633,7 +683,7 @@ Some migrations override with `transaction = false as const` for big DDL on engi
- **Small differences** (a single statement, a CHECK constraint, slightly different syntax): keep one migration in `common/` and branch on `isSqlite` / `isPostgres`.
- **Large differences** (different table recreation strategies, different intermediate steps, fundamentally different SQL): write **separate files** in `postgresdb/` and `sqlite/`. A common migration full of `if (isSqlite) { ... }` blocks is harder to read and review than two focused files.
If only Postgres needs the change, just put the file in `postgresdb/`; don't write a no-op SQLite migration with `if (isPostgres)`. SQLite no longer needs separate migrations for column adds (the recreate-table path was fixed) — verify before duplicating.
If only Postgres needs the change, just put the file in `postgresdb/`; don't write a no-op SQLite migration with `if (isPostgres)`. For SQLite column adds, follow the [SQLite table recreation risk](#sqlite-table-recreation-risk) decision tree before deciding whether a common migration is enough or a SQLite subclass/raw `ALTER TABLE` path is needed.
### SQLite supports modern syntax

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,16 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class RemoveResetPasswordColumns1690000000030 implements ReversibleMigration {
async up({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('user', ['resetPasswordToken', 'resetPasswordTokenExpiration']);
await dropColumns('user', ['resetPasswordToken', 'resetPasswordTokenExpiration'], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('user', [
column('resetPasswordToken').varchar(),
column('resetPasswordTokenExpiration').int,
]);
await addColumns(
'user',
[column('resetPasswordToken').varchar(), column('resetPasswordTokenExpiration').int],
{ recreatesOnSqlite: true },
);
}
}

View File

@ -2,14 +2,20 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddMfaColumns1690000000030 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('user', [
column('mfaEnabled').bool.notNull.default(false),
column('mfaSecret').text,
column('mfaRecoveryCodes').text,
]);
await addColumns(
'user',
[
column('mfaEnabled').bool.notNull.default(false),
column('mfaSecret').text,
column('mfaRecoveryCodes').text,
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('user', ['mfaEnabled', 'mfaSecret', 'mfaRecoveryCodes']);
await dropColumns('user', ['mfaEnabled', 'mfaSecret', 'mfaRecoveryCodes'], {
recreatesOnSqlite: true,
});
}
}

View File

@ -6,7 +6,9 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
*/
export class ExecutionSoftDelete1693491613982 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column, createIndex } }: MigrationContext) {
await addColumns('execution_entity', [column('deletedAt').timestamp()]);
await addColumns('execution_entity', [column('deletedAt').timestamp()], {
recreatesOnSqlite: true,
});
await createIndex('execution_entity', ['deletedAt']);
await createIndex('execution_entity', ['stoppedAt']);
}
@ -14,6 +16,6 @@ export class ExecutionSoftDelete1693491613982 implements ReversibleMigration {
async down({ schemaBuilder: { dropColumns, dropIndex } }: MigrationContext) {
await dropIndex('execution_entity', ['stoppedAt']);
await dropIndex('execution_entity', ['deletedAt']);
await dropColumns('execution_entity', ['deletedAt']);
await dropColumns('execution_entity', ['deletedAt'], { recreatesOnSqlite: true });
}
}

View File

@ -10,13 +10,13 @@ export class DisallowOrphanExecutions1693554410387 implements ReversibleMigratio
await runQuery(`DELETE FROM ${executionEntity} WHERE ${workflowId} IS NULL;`);
await addNotNull('execution_entity', 'workflowId');
await addNotNull('execution_entity', 'workflowId', { recreatesOnSqlite: true });
}
/**
* Reversal excludes restoring deleted rows.
*/
async down({ schemaBuilder: { dropNotNull } }: MigrationContext) {
await dropNotNull('execution_entity', 'workflowId');
await dropNotNull('execution_entity', 'workflowId', { recreatesOnSqlite: true });
}
}

View File

@ -2,10 +2,10 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddWorkflowMetadata1695128658538 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('workflow_entity', [column('meta').json]);
await addColumns('workflow_entity', [column('meta').json], { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('workflow_entity', ['meta']);
await dropColumns('workflow_entity', ['meta'], { recreatesOnSqlite: true });
}
}

View File

@ -4,12 +4,20 @@ const tableName = 'workflow_history';
export class ModifyWorkflowHistoryNodesAndConnections1695829275184 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, dropColumns, column } }: MigrationContext) {
await dropColumns(tableName, ['nodes', 'connections']);
await addColumns(tableName, [column('nodes').json.notNull, column('connections').json.notNull]);
await dropColumns(tableName, ['nodes', 'connections'], { recreatesOnSqlite: true });
await addColumns(
tableName,
[column('nodes').json.notNull, column('connections').json.notNull],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns, addColumns, column } }: MigrationContext) {
await dropColumns(tableName, ['nodes', 'connections']);
await addColumns(tableName, [column('nodes').text.notNull, column('connections').text.notNull]);
await dropColumns(tableName, ['nodes', 'connections'], { recreatesOnSqlite: true });
await addColumns(
tableName,
[column('nodes').text.notNull, column('connections').text.notNull],
{ recreatesOnSqlite: true },
);
}
}

View File

@ -48,7 +48,7 @@ export class DropRoleMapping1705429061930 implements ReversibleMigration {
tablePrefix,
}: MigrationContext,
) {
await addColumns(table, [column('role').text]);
await addColumns(table, [column('role').text], { recreatesOnSqlite: true });
const roleTable = escape.tableName('role');
const tableName = escape.tableName(table);
@ -74,7 +74,7 @@ export class DropRoleMapping1705429061930 implements ReversibleMigration {
${where}`;
await runQuery(swQuery);
await addNotNull(table, 'role');
await addNotNull(table, 'role', { recreatesOnSqlite: true });
await dropForeignKey(
table,
@ -82,7 +82,7 @@ export class DropRoleMapping1705429061930 implements ReversibleMigration {
['role', 'id'],
`FK_${tablePrefix}${foreignKeySuffixes[table]}`,
);
await dropColumns(table, [roleColumnName]);
await dropColumns(table, [roleColumnName], { recreatesOnSqlite: true });
}
private async migrateDown(
@ -95,7 +95,7 @@ export class DropRoleMapping1705429061930 implements ReversibleMigration {
}: MigrationContext,
) {
const roleColumnName = table === 'user' ? 'globalRoleId' : 'roleId';
await addColumns(table, [column(roleColumnName).int]);
await addColumns(table, [column(roleColumnName).int], { recreatesOnSqlite: true });
const roleTable = escape.tableName('role');
const tableName = escape.tableName(table);
@ -118,7 +118,7 @@ export class DropRoleMapping1705429061930 implements ReversibleMigration {
${where}`;
await runQuery(query);
await addNotNull(table, roleColumnName);
await addNotNull(table, roleColumnName, { recreatesOnSqlite: true });
await addForeignKey(
table,
roleColumnName,
@ -126,6 +126,6 @@ export class DropRoleMapping1705429061930 implements ReversibleMigration {
`FK_${tablePrefix}${foreignKeySuffixes[table]}`,
);
await dropColumns(table, ['role']);
await dropColumns(table, ['role'], { recreatesOnSqlite: true });
}
}

View File

@ -2,6 +2,6 @@ import type { IrreversibleMigration, MigrationContext } from '../migration-types
export class RemoveNodesAccess1712044305787 implements IrreversibleMigration {
async up({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('credentials_entity', ['nodesAccess']);
await dropColumns('credentials_entity', ['nodesAccess'], { recreatesOnSqlite: true });
}
}

View File

@ -80,7 +80,7 @@ export class CreateProject1714133768519 implements ReversibleMigration {
}: MigrationContext,
) {
const projectIdColumn = column('projectId').varchar(36).default('NULL');
await addColumns(relationTableName, [projectIdColumn]);
await addColumns(relationTableName, [projectIdColumn], { recreatesOnSqlite: true });
const relationTable = escape.tableName(relationTableName);
const { t, c } = escapeNames(escape);
@ -104,7 +104,7 @@ export class CreateProject1714133768519 implements ReversibleMigration {
await addForeignKey(relationTableName, 'projectId', ['project', 'id']);
await addNotNull(relationTableName, 'projectId');
await addNotNull(relationTableName, 'projectId', { recreatesOnSqlite: true });
// Index the new projectId column
await createIndex(relationTableName, ['projectId']);

View File

@ -17,6 +17,8 @@ export class MakeExecutionStatusNonNullable1714133768521 implements Irreversible
await runQuery(query);
await schemaBuilder.addNotNull('execution_entity', 'status');
await schemaBuilder.addNotNull('execution_entity', 'status', {
recreatesOnSqlite: true,
});
}
}

View File

@ -70,7 +70,7 @@ export class AddApiKeysTable1724951148974 implements ReversibleMigration {
const idColumn = escape.columnName('id');
const createdAtColumn = escape.columnName('createdAt');
await addColumns('user', [column('apiKey').varchar()]);
await addColumns('user', [column('apiKey').varchar()], { recreatesOnSqlite: true });
await createIndex('user', ['apiKey'], true);

View File

@ -6,11 +6,13 @@ export class SeparateExecutionCreationFromStart1727427440136 implements Reversib
runQuery,
escape,
}: MigrationContext) {
await addColumns('execution_entity', [
column('createdAt').notNull.timestamp().default('NOW()'),
]);
await addColumns(
'execution_entity',
[column('createdAt').notNull.timestamp().default('NOW()')],
{ recreatesOnSqlite: true },
);
await dropNotNull('execution_entity', 'startedAt');
await dropNotNull('execution_entity', 'startedAt', { recreatesOnSqlite: true });
const executionEntity = escape.tableName('execution_entity');
const createdAt = escape.columnName('createdAt');
@ -21,7 +23,7 @@ export class SeparateExecutionCreationFromStart1727427440136 implements Reversib
}
async down({ schemaBuilder: { dropColumns, addNotNull } }: MigrationContext) {
await dropColumns('execution_entity', ['createdAt']);
await addNotNull('execution_entity', 'startedAt');
await dropColumns('execution_entity', ['createdAt'], { recreatesOnSqlite: true });
await addNotNull('execution_entity', 'startedAt', { recreatesOnSqlite: true });
}
}

View File

@ -9,7 +9,7 @@ export class UpdateProcessedDataValueColumnToText1729607673464 implements Revers
await runQuery(`ALTER TABLE ${prefixedTableName} DROP COLUMN value;`);
await runQuery(`ALTER TABLE ${prefixedTableName} RENAME COLUMN value_temp TO value`);
await addNotNull(processedDataTableName, 'value');
await addNotNull(processedDataTableName, 'value', { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { addNotNull }, runQuery, tablePrefix }: MigrationContext) {
@ -19,6 +19,6 @@ export class UpdateProcessedDataValueColumnToText1729607673464 implements Revers
await runQuery(`ALTER TABLE ${prefixedTableName} DROP COLUMN value;`);
await runQuery(`ALTER TABLE ${prefixedTableName} RENAME COLUMN value_temp TO value`);
await addNotNull(processedDataTableName, 'value');
await addNotNull(processedDataTableName, 'value', { recreatesOnSqlite: true });
}
}

View File

@ -1,10 +1,10 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddProjectIcons1729607673469 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('project', [column('icon').json]);
await addColumns('project', [column('icon').json], { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('project', ['icon']);
await dropColumns('project', ['icon'], { recreatesOnSqlite: true });
}
}

View File

@ -2,10 +2,12 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddDescriptionToTestDefinition1731404028106 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('test_definition', [column('description').text]);
await addColumns('test_definition', [column('description').text], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('test_definition', ['description']);
await dropColumns('test_definition', ['description'], { recreatesOnSqlite: true });
}
}

View File

@ -14,7 +14,7 @@ export class AddScopesColumnToApiKeys1742918400000 implements ReversibleMigratio
queryRunner,
schemaBuilder: { addColumns, column },
}: MigrationContext) {
await addColumns('user_api_keys', [column('scopes').json]);
await addColumns('user_api_keys', [column('scopes').json], { recreatesOnSqlite: true });
const userApiKeysTable = escape.tableName('user_api_keys');
const userTable = escape.tableName('user');
@ -36,6 +36,6 @@ export class AddScopesColumnToApiKeys1742918400000 implements ReversibleMigratio
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('user_api_keys', ['scopes']);
await dropColumns('user_api_keys', ['scopes'], { recreatesOnSqlite: true });
}
}

View File

@ -36,7 +36,9 @@ export class LinkRoleToUserTable1750252139168 implements ReversibleMigration {
});
}
await addColumns('user', [column('roleSlug').varchar(128).default("'global:member'").notNull]);
await addColumns('user', [column('roleSlug').varchar(128).default("'global:member'").notNull], {
recreatesOnSqlite: true,
});
await runQuery(
`UPDATE ${userTableName} SET ${roleSlugColumn} = ${roleColumn} WHERE ${roleColumn} != ${roleSlugColumn}`,
@ -54,6 +56,6 @@ export class LinkRoleToUserTable1750252139168 implements ReversibleMigration {
async down({ schemaBuilder: { dropForeignKey, dropColumns } }: MigrationContext) {
await dropForeignKey('user', 'roleSlug', ['role', 'slug']);
await dropColumns('user', ['roleSlug']);
await dropColumns('user', ['roleSlug'], { recreatesOnSqlite: true });
}
}

View File

@ -26,7 +26,7 @@ export class RemoveOldRoleColumn1750252139170 implements ReversibleMigration {
`UPDATE ${userTableName} SET ${roleSlugColumn} = ${roleColumn} WHERE ${roleColumn} != ${roleSlugColumn}`,
);
await dropColumns('user', ['role']);
await dropColumns('user', ['role'], { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { addColumns, column }, escape, runQuery }: MigrationContext) {
@ -34,7 +34,9 @@ export class RemoveOldRoleColumn1750252139170 implements ReversibleMigration {
const roleColumn = escape.columnName('role');
const roleSlugColumn = escape.columnName('roleSlug');
await addColumns('user', [column('role').varchar(128).default("'global:member'").notNull]);
await addColumns('user', [column('role').varchar(128).default("'global:member'").notNull], {
recreatesOnSqlite: true,
});
await runQuery(
`UPDATE ${userTableName} SET ${roleColumn} = ${roleSlugColumn} WHERE ${roleSlugColumn} != ${roleColumn}`,

View File

@ -2,10 +2,14 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddInputsOutputsToTestCaseExecution1752669793000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('test_case_execution', [column('inputs').json, column('outputs').json]);
await addColumns('test_case_execution', [column('inputs').json, column('outputs').json], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('test_case_execution', ['inputs', 'outputs']);
await dropColumns('test_case_execution', ['inputs', 'outputs'], {
recreatesOnSqlite: true,
});
}
}

View File

@ -16,10 +16,14 @@ export class AddTimestampsToRoleAndRoleIndexes1756906557570 implements Irreversi
// add the columns twice in the same statement.
await queryRunner.getTable(`${tablePrefix}${USER_TABLE_NAME}`);
await schemaBuilder.addColumns(ROLE_TABLE_NAME, [
new Column('createdAt').timestampTimezone().notNull.default('NOW()'),
new Column('updatedAt').timestampTimezone().notNull.default('NOW()'),
]);
await schemaBuilder.addColumns(
ROLE_TABLE_NAME,
[
new Column('createdAt').timestampTimezone().notNull.default('NOW()'),
new Column('updatedAt').timestampTimezone().notNull.default('NOW()'),
],
{ recreatesOnSqlite: true },
);
// This index should allow us to efficiently query project relations by their role
// This will be used for counting how many users have a specific project role

View File

@ -2,12 +2,14 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddAudienceColumnToApiKeys1758731786132 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('user_api_keys', [
column('audience').varchar().notNull.default("'public-api'"),
]);
await addColumns(
'user_api_keys',
[column('audience').varchar().notNull.default("'public-api'")],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('user_api_keys', ['audience']);
await dropColumns('user_api_keys', ['audience'], { recreatesOnSqlite: true });
}
}

View File

@ -38,26 +38,34 @@ export class CreateChatHubAgentTable1760020000000 implements ReversibleMigration
}).withTimestamps;
// Add agentId and agentName to chat_hub_sessions
await addColumns(table.sessions, [
column('agentId')
.varchar(36)
.comment('ID of the custom agent (if provider is "custom-agent")'),
column('agentName')
.varchar(128)
.comment('Cached name of the custom agent (if provider is "custom-agent")'),
]);
await addColumns(
table.sessions,
[
column('agentId')
.varchar(36)
.comment('ID of the custom agent (if provider is "custom-agent")'),
column('agentName')
.varchar(128)
.comment('Cached name of the custom agent (if provider is "custom-agent")'),
],
{ recreatesOnSqlite: true },
);
// Add agentId to chat_hub_messages
await addColumns(table.messages, [
column('agentId')
.varchar(36)
.comment('ID of the custom agent (if provider is "custom-agent")'),
]);
await addColumns(
table.messages,
[
column('agentId')
.varchar(36)
.comment('ID of the custom agent (if provider is "custom-agent")'),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropTable, dropColumns } }: MigrationContext) {
await dropColumns(table.messages, ['agentId']);
await dropColumns(table.sessions, ['agentId', 'agentName']);
await dropColumns(table.messages, ['agentId'], { recreatesOnSqlite: true });
await dropColumns(table.sessions, ['agentId', 'agentName'], { recreatesOnSqlite: true });
await dropTable(table.agents);
}
}

View File

@ -6,31 +6,41 @@ const table = {
export class DropUnusedChatHubColumns1760965142113 implements ReversibleMigration {
async up({ schemaBuilder: { dropColumns, addColumns, column } }: MigrationContext) {
await dropColumns(table.messages, ['turnId', 'runIndex', 'state']);
await addColumns(table.messages, [
column('status')
.varchar(16)
.default("'success'")
.notNull.comment(
'ChatHubMessageStatus enum, eg. "success", "error", "running", "cancelled"',
),
]);
await dropColumns(table.messages, ['turnId', 'runIndex', 'state'], {
recreatesOnSqlite: true,
});
await addColumns(
table.messages,
[
column('status')
.varchar(16)
.default("'success'")
.notNull.comment(
'ChatHubMessageStatus enum, eg. "success", "error", "running", "cancelled"',
),
],
{ recreatesOnSqlite: true },
);
}
async down({
schemaBuilder: { dropColumns, addColumns, column, addForeignKey },
}: MigrationContext) {
await dropColumns(table.messages, ['status']);
await addColumns(table.messages, [
column('turnId').uuid,
column('runIndex')
.int.notNull.default(0)
.comment('The nth attempt this message has been generated/retried this turn'),
column('state')
.varchar(16)
.default("'active'")
.notNull.comment('ChatHubMessageState enum: "active", "superseded", "hidden", "deleted"'),
]);
await dropColumns(table.messages, ['status'], { recreatesOnSqlite: true });
await addColumns(
table.messages,
[
column('turnId').uuid,
column('runIndex')
.int.notNull.default(0)
.comment('The nth attempt this message has been generated/retried this turn'),
column('state')
.varchar(16)
.default("'active'")
.notNull.comment('ChatHubMessageState enum: "active", "superseded", "hidden", "deleted"'),
],
{ recreatesOnSqlite: true },
);
await addForeignKey(table.messages, 'turnId', [table.messages, 'id'], undefined, 'CASCADE');
}
}

View File

@ -6,14 +6,18 @@ const table = {
export class AddAttachmentsToChatHubMessages1761773155024 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(table.messages, [
column('attachments').json.comment(
'File attachments for the message (if any), stored as JSON. Files are stored as base64-encoded data URLs.',
),
]);
await addColumns(
table.messages,
[
column('attachments').json.comment(
'File attachments for the message (if any), stored as JSON. Files are stored as base64-encoded data URLs.',
),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(table.messages, ['attachments']);
await dropColumns(table.messages, ['attachments'], { recreatesOnSqlite: true });
}
}

View File

@ -7,20 +7,28 @@ const table = {
export class AddToolsColumnToChatHubTables1761830340990 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(table.sessions, [
column('tools')
.json.notNull.default("'[]'")
.comment('Tools available to the agent as JSON node definitions'),
]);
await addColumns(table.agents, [
column('tools')
.json.notNull.default("'[]'")
.comment('Tools available to the agent as JSON node definitions'),
]);
await addColumns(
table.sessions,
[
column('tools')
.json.notNull.default("'[]'")
.comment('Tools available to the agent as JSON node definitions'),
],
{ recreatesOnSqlite: true },
);
await addColumns(
table.agents,
[
column('tools')
.json.notNull.default("'[]'")
.comment('Tools available to the agent as JSON node definitions'),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(table.sessions, ['tools']);
await dropColumns(table.agents, ['tools']);
await dropColumns(table.sessions, ['tools'], { recreatesOnSqlite: true });
await dropColumns(table.agents, ['tools'], { recreatesOnSqlite: true });
}
}

View File

@ -2,10 +2,12 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddWorkflowDescriptionColumn1762177736257 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('workflow_entity', [column('description').text]);
await addColumns('workflow_entity', [column('description').text], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('workflow_entity', ['description']);
await dropColumns('workflow_entity', ['description'], { recreatesOnSqlite: true });
}
}

View File

@ -94,6 +94,8 @@ export class BackfillMissingWorkflowHistoryRecords1762763704614 implements Irrev
);
// Step 3: Make versionId NOT NULL
await schemaBuilder.addNotNull('workflow_entity', 'versionId');
await schemaBuilder.addNotNull('workflow_entity', 'versionId', {
recreatesOnSqlite: true,
});
}
}

View File

@ -7,14 +7,20 @@ const description = 'description';
export class AddWorkflowHistoryAutoSaveFields1762847206508 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(tableName, [
column(name).varchar(128),
column(autosaved).bool.notNull.default(false),
column(description).text,
]);
await addColumns(
tableName,
[
column(name).varchar(128),
column(autosaved).bool.notNull.default(false),
column(description).text,
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(tableName, [name, autosaved, description]);
await dropColumns(tableName, [name, autosaved, description], {
recreatesOnSqlite: true,
});
}
}

View File

@ -12,7 +12,9 @@ export class AddActiveVersionIdColumn1763047800000 implements ReversibleMigratio
}: MigrationContext) {
const workflowsTableName = escape.tableName(WORKFLOWS_TABLE_NAME);
await addColumns(WORKFLOWS_TABLE_NAME, [column('activeVersionId').varchar(36)]);
await addColumns(WORKFLOWS_TABLE_NAME, [column('activeVersionId').varchar(36)], {
recreatesOnSqlite: true,
});
await addForeignKey(
WORKFLOWS_TABLE_NAME,
@ -43,7 +45,9 @@ export class AddActiveVersionIdColumn1763047800000 implements ReversibleMigratio
WORKFLOW_HISTORY_TABLE_NAME,
'versionId',
]);
await dropColumns(WORKFLOWS_TABLE_NAME, ['activeVersionId']);
await dropColumns(WORKFLOWS_TABLE_NAME, ['activeVersionId'], {
recreatesOnSqlite: true,
});
}
// Create workflow_history records for workflows missing them

View File

@ -13,9 +13,11 @@ export class AddCreatorIdToProjectTable1764276827837 implements ReversibleMigrat
schemaBuilder: { addColumns, addForeignKey, column },
queryRunner,
}: MigrationContext) {
await addColumns(table.project, [
column('creatorId').uuid.comment('ID of the user who created the project'),
]);
await addColumns(
table.project,
[column('creatorId').uuid.comment('ID of the user who created the project')],
{ recreatesOnSqlite: true },
);
await addForeignKey(table.project, 'creatorId', ['user', 'id'], FOREIGN_KEY_NAME, 'SET NULL');
@ -37,6 +39,6 @@ export class AddCreatorIdToProjectTable1764276827837 implements ReversibleMigrat
async down({ schemaBuilder: { dropColumns, dropForeignKey } }: MigrationContext) {
await dropForeignKey(table.project, 'creatorId', ['user', 'id'], FOREIGN_KEY_NAME);
await dropColumns(table.project, ['creatorId']);
await dropColumns(table.project, ['creatorId'], { recreatesOnSqlite: true });
}
}

View File

@ -6,11 +6,15 @@ const FOREIGN_KEY_NAME = 'credentials_entity_resolverId_foreign';
export class AddResolvableFieldsToCredentials1765459448000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, addForeignKey, column } }: MigrationContext) {
await addColumns(credentialsTableName, [
column('isResolvable').bool.notNull.default(false),
column('resolvableAllowFallback').bool.notNull.default(false),
column('resolverId').varchar(16),
]);
await addColumns(
credentialsTableName,
[
column('isResolvable').bool.notNull.default(false),
column('resolvableAllowFallback').bool.notNull.default(false),
column('resolverId').varchar(16),
],
{ recreatesOnSqlite: true },
);
await addForeignKey(
credentialsTableName,
@ -29,10 +33,10 @@ export class AddResolvableFieldsToCredentials1765459448000 implements Reversible
FOREIGN_KEY_NAME,
);
await dropColumns(credentialsTableName, [
'isResolvable',
'resolvableAllowFallback',
'resolverId',
]);
await dropColumns(
credentialsTableName,
['isResolvable', 'resolvableAllowFallback', 'resolverId'],
{ recreatesOnSqlite: true },
);
}
}

View File

@ -5,11 +5,11 @@ const table = 'chat_hub_agents';
export class AddIconToAgentTable1765788427674 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
// Add icon column to agents table (nullable)
await addColumns(table, [column('icon').json]);
await addColumns(table, [column('icon').json], { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
// Drop icon column
await dropColumns(table, ['icon']);
await dropColumns(table, ['icon'], { recreatesOnSqlite: true });
}
}

View File

@ -18,7 +18,7 @@ export class AddChatMessageIndices1766068346315 implements ReversibleMigration {
WHERE ${lastMessageAtColumn} IS NULL`,
);
await addNotNull('chat_hub_sessions', 'lastMessageAt');
await addNotNull('chat_hub_sessions', 'lastMessageAt', { recreatesOnSqlite: true });
// Index intended for faster sessionRepository.getManyByUserId queries
await runQuery(
@ -39,6 +39,6 @@ export class AddChatMessageIndices1766068346315 implements ReversibleMigration {
);
await runQuery(`DROP INDEX IF EXISTS ${escape.indexName('chat_hub_messages_sessionId')}`);
await dropNotNull('chat_hub_sessions', 'lastMessageAt');
await dropNotNull('chat_hub_sessions', 'lastMessageAt', { recreatesOnSqlite: true });
}
}

View File

@ -2,12 +2,16 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddPublishedVersionIdToWorkflowDependency1769000000000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column, createIndex } }: MigrationContext) {
await addColumns('workflow_dependency', [column('publishedVersionId').varchar(36)]);
await addColumns('workflow_dependency', [column('publishedVersionId').varchar(36)], {
recreatesOnSqlite: true,
});
await createIndex('workflow_dependency', ['publishedVersionId']);
}
async down({ schemaBuilder: { dropColumns, dropIndex } }: MigrationContext) {
await dropIndex('workflow_dependency', ['publishedVersionId']);
await dropColumns('workflow_dependency', ['publishedVersionId']);
await dropColumns('workflow_dependency', ['publishedVersionId'], {
recreatesOnSqlite: true,
});
}
}

View File

@ -198,8 +198,8 @@ export class CreateChatHubToolsTable1770000000000 implements ReversibleMigration
);
// Drop the tools columns from chat hub sessions and agents
await dropColumns(table.sessions, ['tools']);
await dropColumns(table.agents, ['tools']);
await dropColumns(table.sessions, ['tools'], { recreatesOnSqlite: true });
await dropColumns(table.agents, ['tools'], { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { addColumns, column, dropTable } }: MigrationContext) {
@ -210,7 +210,11 @@ export class CreateChatHubToolsTable1770000000000 implements ReversibleMigration
// This loses data, but we can't really restore it.
// Impact of losing the configured tools should be fairly minimal, as credentials remain intact
// and users can easily re-add the search tools to chat hub sessions and agents after the rollback if needed.
await addColumns(table.sessions, [column('tools').json.notNull.default("'[]'")]);
await addColumns(table.agents, [column('tools').json.notNull.default("'[]'")]);
await addColumns(table.sessions, [column('tools').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
await addColumns(table.agents, [column('tools').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
}
}

View File

@ -4,10 +4,12 @@ const table = 'chat_hub_agents';
export class AddFilesColumnToChatHubAgents1771500000002 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(table, [column('files').json.notNull.default("'[]'")]);
await addColumns(table, [column('files').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(table, ['files']);
await dropColumns(table, ['files'], { recreatesOnSqlite: true });
}
}

View File

@ -4,10 +4,12 @@ const table = 'chat_hub_agents';
export class AddSuggestedPromptsToAgentTable1772000000000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(table, [column('suggestedPrompts').json.notNull.default("'[]'")]);
await addColumns(table, [column('suggestedPrompts').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(table, ['suggestedPrompts']);
await dropColumns(table, ['suggestedPrompts'], { recreatesOnSqlite: true });
}
}

View File

@ -6,18 +6,22 @@ export class AddRoleColumnToProjectSecretsProviderAccess1772619247761
implements ReversibleMigration
{
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(table, [
column('role')
.varchar(128)
.notNull.default("'secretsProviderConnection:user'")
.withEnumCheck(['secretsProviderConnection:owner', 'secretsProviderConnection:user']),
]);
await addColumns(
table,
[
column('role')
.varchar(128)
.notNull.default("'secretsProviderConnection:user'")
.withEnumCheck(['secretsProviderConnection:owner', 'secretsProviderConnection:user']),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns }, tablePrefix, queryRunner }: MigrationContext) {
const fullTableName = `${tablePrefix}${table}`;
const checkName = `CHK_${tablePrefix}${table}_role`;
await queryRunner.dropCheckConstraint(fullTableName, checkName);
await dropColumns(table, ['role']);
await dropColumns(table, ['role'], { recreatesOnSqlite: true });
}
}

View File

@ -2,15 +2,19 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddTypeToChatHubSessions1772700000000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('chat_hub_sessions', [
column('type')
.varchar(16)
.notNull.default("'production'")
.withEnumCheck(['production', 'manual']),
]);
await addColumns(
'chat_hub_sessions',
[
column('type')
.varchar(16)
.notNull.default("'production'")
.withEnumCheck(['production', 'manual']),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('chat_hub_sessions', ['type']);
await dropColumns('chat_hub_sessions', ['type'], { recreatesOnSqlite: true });
}
}

View File

@ -2,16 +2,21 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddRestoreFieldsToWorkflowBuilderSession1774280963551 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('workflow_builder_session', [
column('activeVersionCardId').varchar(255),
column('resumeAfterRestoreMessageId').varchar(255),
]);
await addColumns(
'workflow_builder_session',
[
column('activeVersionCardId').varchar(255),
column('resumeAfterRestoreMessageId').varchar(255),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('workflow_builder_session', [
'activeVersionCardId',
'resumeAfterRestoreMessageId',
]);
await dropColumns(
'workflow_builder_session',
['activeVersionCardId', 'resumeAfterRestoreMessageId'],
{ recreatesOnSqlite: true },
);
}
}

View File

@ -15,7 +15,7 @@ export class ChangeWorkflowPublishHistoryVersionIdToSetNull1775740765000
{
async up({ schemaBuilder: { dropForeignKey, addForeignKey, dropNotNull } }: MigrationContext) {
await dropForeignKey(tableName, columnName, reference);
await dropNotNull(tableName, columnName);
await dropNotNull(tableName, columnName, { recreatesOnSqlite: true });
await addForeignKey(tableName, columnName, reference, undefined, 'SET NULL');
}
@ -26,7 +26,7 @@ export class ChangeWorkflowPublishHistoryVersionIdToSetNull1775740765000
await dropForeignKey(tableName, columnName, reference);
await mc.runQuery(`DELETE FROM ${tableName} WHERE ${columnName} IS NULL`);
await addNotNull(tableName, columnName);
await addNotNull(tableName, columnName, { recreatesOnSqlite: true });
await addForeignKey(tableName, columnName, reference, undefined, 'CASCADE');
}
}

View File

@ -2,10 +2,12 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddTracingContextToExecution1777045000000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('execution_entity', [column('tracingContext').json]);
await addColumns('execution_entity', [column('tracingContext').json], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('execution_entity', ['tracingContext']);
await dropColumns('execution_entity', ['tracingContext'], { recreatesOnSqlite: true });
}
}

View File

@ -2,17 +2,23 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddLangsmithIdsToInstanceAiRunSnapshots1777100000000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('instance_ai_run_snapshots', [
column('langsmithRunId')
.varchar(36)
.comment('LangSmith run ID (UUID v4, e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479").'),
column('langsmithTraceId')
.varchar(36)
.comment('LangSmith trace ID (UUID v4, e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479").'),
]);
await addColumns(
'instance_ai_run_snapshots',
[
column('langsmithRunId')
.varchar(36)
.comment('LangSmith run ID (UUID v4, e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479").'),
column('langsmithTraceId')
.varchar(36)
.comment('LangSmith trace ID (UUID v4, e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479").'),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('instance_ai_run_snapshots', ['langsmithRunId', 'langsmithTraceId']);
await dropColumns('instance_ai_run_snapshots', ['langsmithRunId', 'langsmithTraceId'], {
recreatesOnSqlite: true,
});
}
}

View File

@ -13,7 +13,9 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
*/
export class AddExecutionDeduplicationKey1778000000000 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column, createIndex } }: MigrationContext) {
await addColumns('execution_entity', [column('deduplicationKey').varchar(255)]);
await addColumns('execution_entity', [column('deduplicationKey').varchar(255)], {
recreatesOnSqlite: true,
});
// NOTE: partial unique index. Two inserts with the same non-null key race and the
// second fails, so duplicates can be detected. Null rows are excluded from the index
// entirely — executions without a dedupe key don't occupy index space and aren't
@ -29,6 +31,6 @@ export class AddExecutionDeduplicationKey1778000000000 implements ReversibleMigr
async down({ schemaBuilder: { dropIndex, dropColumns } }: MigrationContext) {
await dropIndex('execution_entity', ['deduplicationKey']);
await dropColumns('execution_entity', ['deduplicationKey']);
await dropColumns('execution_entity', ['deduplicationKey'], { recreatesOnSqlite: true });
}
}

View File

@ -11,7 +11,9 @@ export class AddEvaluationConfigColumnsToTestRun1778100002000 implements Reversi
runQuery,
dbType,
}: MigrationContext) {
await addColumns(TEST_RUN_TABLE, [column('evaluationConfigId').varchar(36)]);
await addColumns(TEST_RUN_TABLE, [column('evaluationConfigId').varchar(36)], {
recreatesOnSqlite: true,
});
await addForeignKey(
TEST_RUN_TABLE,
@ -31,7 +33,9 @@ export class AddEvaluationConfigColumnsToTestRun1778100002000 implements Reversi
}
async down({ schemaBuilder: { dropColumns, dropForeignKey, dropIndex } }: MigrationContext) {
await dropColumns(TEST_RUN_TABLE, ['evaluationConfigSnapshot']);
await dropColumns(TEST_RUN_TABLE, ['evaluationConfigSnapshot'], {
recreatesOnSqlite: true,
});
await dropIndex(TEST_RUN_TABLE, ['evaluationConfigId']);
await dropForeignKey(
TEST_RUN_TABLE,
@ -39,6 +43,6 @@ export class AddEvaluationConfigColumnsToTestRun1778100002000 implements Reversi
[EVALUATION_CONFIG_TABLE, 'id'],
FK_NAME,
);
await dropColumns(TEST_RUN_TABLE, ['evaluationConfigId']);
await dropColumns(TEST_RUN_TABLE, ['evaluationConfigId'], { recreatesOnSqlite: true });
}
}

View File

@ -56,7 +56,9 @@ export class CreateEvaluationCollection1778496086558 implements ReversibleMigrat
await queryRunner.getTable(TEST_RUN_TABLE);
}
await addColumns(TEST_RUN_TABLE, [column('collectionId').varchar(36)]);
await addColumns(TEST_RUN_TABLE, [column('collectionId').varchar(36)], {
recreatesOnSqlite: true,
});
await addForeignKey(
TEST_RUN_TABLE,
@ -79,7 +81,7 @@ export class CreateEvaluationCollection1778496086558 implements ReversibleMigrat
[EVALUATION_COLLECTION_TABLE, 'id'],
FK_TEST_RUN_COLLECTION,
);
await dropColumns(TEST_RUN_TABLE, ['collectionId']);
await dropColumns(TEST_RUN_TABLE, ['collectionId'], { recreatesOnSqlite: true });
await dropTable(EVALUATION_COLLECTION_TABLE);
}
}

View File

@ -2,10 +2,12 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class DropAgentExecutionWorkingMemory1784000000002 implements ReversibleMigration {
async up({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('agent_execution', ['workingMemory']);
await dropColumns('agent_execution', ['workingMemory'], { recreatesOnSqlite: true });
}
async down({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('agent_execution', [column('workingMemory').text]);
await addColumns('agent_execution', [column('workingMemory').text], {
recreatesOnSqlite: true,
});
}
}

View File

@ -2,12 +2,16 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddNodeGroupsColumnToWorkflowAndHistory1784000000006 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('workflow_entity', [column('nodeGroups').json.notNull.default("'[]'")]);
await addColumns('workflow_history', [column('nodeGroups').json.notNull.default("'[]'")]);
await addColumns('workflow_entity', [column('nodeGroups').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
await addColumns('workflow_history', [column('nodeGroups').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('workflow_entity', ['nodeGroups']);
await dropColumns('workflow_history', ['nodeGroups']);
await dropColumns('workflow_entity', ['nodeGroups'], { recreatesOnSqlite: true });
await dropColumns('workflow_history', ['nodeGroups'], { recreatesOnSqlite: true });
}
}

View File

@ -5,10 +5,16 @@ const runSnapshotsTable = 'instance_ai_run_snapshots';
export class CreateInstanceAiCheckpointTable1784000000007 implements ReversibleMigration {
async up({ schemaBuilder: { createTable, addColumns, column } }: MigrationContext) {
await addColumns(runSnapshotsTable, [
column('traceId').varchar(64).comment('OpenTelemetry trace ID for the root Instance AI run.'),
column('spanId').varchar(64).comment('OpenTelemetry span ID for the root Instance AI run.'),
]);
await addColumns(
runSnapshotsTable,
[
column('traceId')
.varchar(64)
.comment('OpenTelemetry trace ID for the root Instance AI run.'),
column('spanId').varchar(64).comment('OpenTelemetry span ID for the root Instance AI run.'),
],
{ recreatesOnSqlite: true },
);
await createTable(checkpointTable)
.withColumns(
@ -34,6 +40,6 @@ export class CreateInstanceAiCheckpointTable1784000000007 implements ReversibleM
async down({ schemaBuilder: { dropTable, dropColumns } }: MigrationContext) {
await dropTable(checkpointTable);
await dropColumns(runSnapshotsTable, ['traceId', 'spanId']);
await dropColumns(runSnapshotsTable, ['traceId', 'spanId'], { recreatesOnSqlite: true });
}
}

View File

@ -43,7 +43,9 @@ export class CreateAgentHistoryTable1784000000011 implements ReversibleMigration
onDelete: 'SET NULL',
}).withTimestamps;
await addColumns('agents', [column('activeVersionId').varchar(36)]);
await addColumns('agents', [column('activeVersionId').varchar(36)], {
recreatesOnSqlite: true,
});
await addForeignKey(
'agents',
'activeVersionId',
@ -59,7 +61,9 @@ export class CreateAgentHistoryTable1784000000011 implements ReversibleMigration
await dropTable('agent_published_version');
// These columns were left from previous refactors and never actually used in the live system
await dropColumns('agents', ['credentialId', 'provider', 'model']);
await dropColumns('agents', ['credentialId', 'provider', 'model'], {
recreatesOnSqlite: true,
});
}
async down(ctx: MigrationContext) {
@ -67,11 +71,15 @@ export class CreateAgentHistoryTable1784000000011 implements ReversibleMigration
const { createTable, addColumns, dropForeignKey, dropColumns, dropTable, column } =
schemaBuilder;
await addColumns('agents', [
column('credentialId').varchar(255),
column('provider').varchar(128),
column('model').varchar(128),
]);
await addColumns(
'agents',
[
column('credentialId').varchar(255),
column('provider').varchar(128),
column('model').varchar(128),
],
{ recreatesOnSqlite: true },
);
await createTable('agent_published_version')
.withColumns(
@ -99,7 +107,7 @@ export class CreateAgentHistoryTable1784000000011 implements ReversibleMigration
await this.restoreActiveHistoryToPublishedVersion(ctx);
await dropForeignKey('agents', 'activeVersionId', ['agent_history', 'versionId']);
await dropColumns('agents', ['activeVersionId']);
await dropColumns('agents', ['activeVersionId'], { recreatesOnSqlite: true });
await dropTable('agent_history');
}

View File

@ -34,16 +34,24 @@ export class PersistInstanceAiPendingConfirmations1784000000014 implements Rever
// Reshape `instance_ai_checkpoints` first, then create the table that
// FKs into it. SQLite's `dropNotNull` recreates the table; doing that
// while a FK already points at it would force a more fragile rebuild.
await schemaBuilder.addColumns(checkpointsTable, [
schemaBuilder
.column('expiredAt')
.timestampTimezone()
.comment('Soft-delete timestamp: null means live; non-null marks the row as a tombstone.'),
]);
await schemaBuilder.addColumns(
checkpointsTable,
[
schemaBuilder
.column('expiredAt')
.timestampTimezone()
.comment(
'Soft-delete timestamp: null means live; non-null marks the row as a tombstone.',
),
],
{ recreatesOnSqlite: true },
);
// Soft-delete sets `state = null` on consumed/pruned snapshots, so the
// column must be nullable. Created NOT NULL in `1784000000007`.
await schemaBuilder.dropNotNull(checkpointsTable, 'state');
await schemaBuilder.dropNotNull(checkpointsTable, 'state', {
recreatesOnSqlite: true,
});
// Enforce the soft-delete invariant at the DB level: a tombstoned row
// (`expiredAt` set) must have released its `state` blob.
@ -80,8 +88,8 @@ export class PersistInstanceAiPendingConfirmations1784000000014 implements Rever
const stateCol = escape.columnName('state');
await runQuery(`DELETE FROM ${table} WHERE ${stateCol} IS NULL`);
await addNotNull(checkpointsTable, 'state');
await dropColumns(checkpointsTable, ['expiredAt']);
await addNotNull(checkpointsTable, 'state', { recreatesOnSqlite: true });
await dropColumns(checkpointsTable, ['expiredAt'], { recreatesOnSqlite: true });
}
private async createPendingConfirmationsTable({

View File

@ -2,7 +2,9 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddSourceWorkflowIdToWorkflow1784000000015 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column, createIndex } }: MigrationContext) {
await addColumns('workflow_entity', [column('sourceWorkflowId').varchar()]);
await addColumns('workflow_entity', [column('sourceWorkflowId').varchar()], {
recreatesOnSqlite: true,
});
await createIndex(
'workflow_entity',
@ -15,6 +17,8 @@ export class AddSourceWorkflowIdToWorkflow1784000000015 implements ReversibleMig
async down({ schemaBuilder: { dropColumns, dropIndex } }: MigrationContext) {
await dropIndex('workflow_entity', ['sourceWorkflowId']);
await dropColumns('workflow_entity', ['sourceWorkflowId']);
await dropColumns('workflow_entity', ['sourceWorkflowId'], {
recreatesOnSqlite: true,
});
}
}

View File

@ -2,10 +2,12 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddLastUsedAtToApiKey1784000000017 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns('user_api_keys', [column('lastUsedAt').timestampTimezone()]);
await addColumns('user_api_keys', [column('lastUsedAt').timestampTimezone()], {
recreatesOnSqlite: true,
});
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns('user_api_keys', ['lastUsedAt']);
await dropColumns('user_api_keys', ['lastUsedAt'], { recreatesOnSqlite: true });
}
}

View File

@ -46,7 +46,9 @@ export class CreateAgentFilesTable1784000000018 implements ReversibleMigration {
{ schemaBuilder: { addEnumCheck, dropEnumCheck } }: MigrationContext,
sourceTypes: string[],
) {
await dropEnumCheck(binaryDataTableName, sourceTypeColumn);
await addEnumCheck(binaryDataTableName, sourceTypeColumn, sourceTypes);
await dropEnumCheck(binaryDataTableName, sourceTypeColumn, { recreatesOnSqlite: true });
await addEnumCheck(binaryDataTableName, sourceTypeColumn, sourceTypes, {
recreatesOnSqlite: true,
});
}
}

View File

@ -2,12 +2,14 @@ import type { MigrationContext, ReversibleMigration } from '../migration-types';
export class AddCustomTelemetryTagsToProject1784000000019 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column, createIndex } }: MigrationContext) {
await addColumns('project', [column('customTelemetryTags').json.notNull.default("'[]'")]);
await addColumns('project', [column('customTelemetryTags').json.notNull.default("'[]'")], {
recreatesOnSqlite: true,
});
await createIndex('shared_workflow', ['projectId']);
}
async down({ schemaBuilder: { dropColumns, dropIndex } }: MigrationContext) {
await dropIndex('shared_workflow', ['projectId']);
await dropColumns('project', ['customTelemetryTags']);
await dropColumns('project', ['customTelemetryTags'], { recreatesOnSqlite: true });
}
}

View File

@ -82,16 +82,20 @@ export class CreateAgentTaskDefinitionTable1784000000021 implements ReversibleMi
onDelete: 'CASCADE',
}).withTimestamps;
await addColumns('agent_execution_threads', [
column('taskId')
.varchar(32)
.comment(
'Published task ID that triggered this session; not an FK because published runs can outlive draft task definition rows',
),
column('taskVersionId')
.varchar(36)
.comment('Published agent_history version that supplied the task snapshot'),
]);
await addColumns(
'agent_execution_threads',
[
column('taskId')
.varchar(32)
.comment(
'Published task ID that triggered this session; not an FK because published runs can outlive draft task definition rows',
),
column('taskVersionId')
.varchar(36)
.comment('Published agent_history version that supplied the task snapshot'),
],
{ recreatesOnSqlite: true },
);
await addForeignKey(
'agent_execution_threads',
'taskVersionId',
@ -110,7 +114,9 @@ export class CreateAgentTaskDefinitionTable1784000000021 implements ReversibleMi
'agent_history',
'versionId',
]);
await dropColumns('agent_execution_threads', ['taskId', 'taskVersionId']);
await dropColumns('agent_execution_threads', ['taskId', 'taskVersionId'], {
recreatesOnSqlite: true,
});
await dropTable('agent_task_run_lock');
await dropTable('agent_task_snapshot');
await dropTable('agent_task_definition');

View File

@ -45,14 +45,18 @@ export class AddSubAgentLinkageToAgentExecutionThreads1784000000022
escape,
tablePrefix,
}: MigrationContext) {
await addColumns('agent_execution_threads', [
column('parentThreadId')
.varchar(128)
.comment('Parent session thread id that delegated this subagent run.'),
column('parentAgentId')
.varchar(36)
.comment('Saved agent id of the parent that delegated this subagent run.'),
]);
await addColumns(
'agent_execution_threads',
[
column('parentThreadId')
.varchar(128)
.comment('Parent session thread id that delegated this subagent run.'),
column('parentAgentId')
.varchar(36)
.comment('Saved agent id of the parent that delegated this subagent run.'),
],
{ recreatesOnSqlite: true },
);
if (isPostgres) {
for (const { table, column: columnName } of COLUMNS_TO_WIDEN) {

View File

@ -7,17 +7,21 @@ const COLUMN_NAME = 'resource';
// instance's canonical MCP resource URL when consuming the code.
export class AddResourceToOAuthAuthorizationCodes1784000000024 implements ReversibleMigration {
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
await addColumns(TABLE_NAME, [
column(COLUMN_NAME)
.varchar()
.comment(
'RFC 8707 resource indicator URI (e.g. https://n8n.example.com/mcp-server/http). ' +
'NULL = legacy flow predating resource indicator support; defaults to the instance canonical MCP resource URL.',
),
]);
await addColumns(
TABLE_NAME,
[
column(COLUMN_NAME)
.varchar()
.comment(
'RFC 8707 resource indicator URI (e.g. https://n8n.example.com/mcp-server/http). ' +
'NULL = legacy flow predating resource indicator support; defaults to the instance canonical MCP resource URL.',
),
],
{ recreatesOnSqlite: true },
);
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(TABLE_NAME, [COLUMN_NAME]);
await dropColumns(TABLE_NAME, [COLUMN_NAME], { recreatesOnSqlite: true });
}
}

View File

@ -15,6 +15,14 @@ import {
DropTable,
} from './table';
/**
* Marks an operation that recreates the entire table on SQLite.
* Adding this option is an explicit acknowledgment of that risk.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
type RecreatesOnSqliteAck = { recreatesOnSqlite: true };
export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunner) => ({
column: (name: string) => new Column(name),
/* eslint-disable @typescript-eslint/promise-function-async */
@ -23,9 +31,41 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne
dropTable: (tableName: string) => new DropTable(tableName, tablePrefix, queryRunner),
addColumns: (tableName: string, columns: Column[]) =>
/**
* Adds columns to an existing table.
*
* **WARNING SQLite table recreation:** On SQLite, TypeORM implements this by
* recreating the entire table (create temp copy data drop original rename).
* If other tables have incoming FK constraints with CASCADE on the target table,
* the DROP triggers cascading deletes and **wipes data from those tables**.
*
* **Mitigation:** On SQLite migrations, set `withFKsDisabled = true as const`
* when the target table has incoming FKs. For common migrations, add a
* SQLite-only subclass with the flag instead of setting it on the common class.
*
* **Safer alternative:** When all new columns are nullable or have defaults,
* raw `ALTER TABLE ADD COLUMN` avoids table recreation entirely.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
addColumns: (tableName: string, columns: Column[], _opts: RecreatesOnSqliteAck) =>
new AddColumns(tableName, columns, tablePrefix, queryRunner),
dropColumns: (tableName: string, columnNames: string[]) =>
/**
* Drops columns from an existing table.
*
* **WARNING SQLite table recreation:** On SQLite, TypeORM implements this by
* recreating the entire table. If other tables have incoming FK constraints with
* CASCADE on the target table, the DROP triggers cascading deletes and **wipes
* data from those tables**.
*
* **Mitigation:** On SQLite migrations, set `withFKsDisabled = true as const`
* when the target table has incoming FKs. For common migrations, add a
* SQLite-only subclass with the flag instead of setting it on the common class.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
dropColumns: (tableName: string, columnNames: string[], _opts: RecreatesOnSqliteAck) =>
new DropColumns(tableName, columnNames, tablePrefix, queryRunner),
/**
@ -91,16 +131,76 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne
customConstraintName,
),
addNotNull: (tableName: string, columnName: string) =>
/**
* Adds a NOT NULL constraint to an existing column.
*
* **WARNING SQLite table recreation:** On SQLite, TypeORM implements
* `changeColumn` by recreating the entire table. If other tables have incoming
* FK constraints with CASCADE on the target table, the DROP triggers cascading
* deletes and **wipes data from those tables**.
*
* **Mitigation:** On SQLite migrations, set `withFKsDisabled = true as const`
* when the target table has incoming FKs. For common migrations, add a
* SQLite-only subclass with the flag instead of setting it on the common class.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
addNotNull: (tableName: string, columnName: string, _opts: RecreatesOnSqliteAck) =>
new AddNotNull(tableName, columnName, tablePrefix, queryRunner),
dropNotNull: (tableName: string, columnName: string) =>
/**
* Drops the NOT NULL constraint from an existing column.
*
* **WARNING SQLite table recreation:** On SQLite, TypeORM implements
* `changeColumn` by recreating the entire table. If other tables have incoming
* FK constraints with CASCADE on the target table, the DROP triggers cascading
* deletes and **wipes data from those tables**.
*
* **Mitigation:** On SQLite migrations, set `withFKsDisabled = true as const`
* when the target table has incoming FKs. For common migrations, add a
* SQLite-only subclass with the flag instead of setting it on the common class.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
dropNotNull: (tableName: string, columnName: string, _opts: RecreatesOnSqliteAck) =>
new DropNotNull(tableName, columnName, tablePrefix, queryRunner),
/** WARNING: This recreates the entire table on SQLite. */
addEnumCheck: (tableName: string, columnName: string, values: string[]) =>
new AddEnumCheck(tableName, columnName, values, tablePrefix, queryRunner),
/** WARNING: This recreates the entire table on SQLite. */
dropEnumCheck: (tableName: string, columnName: string) =>
/**
* Adds a CHECK constraint that emulates an enum on the given column.
*
* **WARNING SQLite table recreation:** On SQLite, TypeORM implements this by
* recreating the entire table. If other tables have incoming FK constraints with
* CASCADE on the target table, the DROP triggers cascading deletes and **wipes
* data from those tables**.
*
* **Mitigation:** On SQLite migrations, set `withFKsDisabled = true as const`
* when the target table has incoming FKs. For common migrations, add a
* SQLite-only subclass with the flag instead of setting it on the common class.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
addEnumCheck: (
tableName: string,
columnName: string,
values: string[],
_opts: RecreatesOnSqliteAck,
) => new AddEnumCheck(tableName, columnName, values, tablePrefix, queryRunner),
/**
* Drops a CHECK constraint that emulates an enum on the given column.
*
* **WARNING SQLite table recreation:** On SQLite, TypeORM implements this by
* recreating the entire table. If other tables have incoming FK constraints with
* CASCADE on the target table, the DROP triggers cascading deletes and **wipes
* data from those tables**.
*
* **Mitigation:** On SQLite migrations, set `withFKsDisabled = true as const`
* when the target table has incoming FKs. For common migrations, add a
* SQLite-only subclass with the flag instead of setting it on the common class.
*
* @see {@link BaseMigration.withFKsDisabled}
*/
dropEnumCheck: (tableName: string, columnName: string, _opts: RecreatesOnSqliteAck) =>
new DropEnumCheck(tableName, columnName, tablePrefix, queryRunner),
/* eslint-enable */

View File

@ -152,6 +152,7 @@ export class DropTable extends TableOperation {
}
}
/** SQLite: TypeORM recreates the table internally — beware CASCADE FK data loss. */
export class AddColumns extends TableOperation {
constructor(
tableName: string,
@ -179,6 +180,7 @@ export class AddColumns extends TableOperation {
}
}
/** SQLite: TypeORM recreates the table internally — beware CASCADE FK data loss. */
export class DropColumns extends TableOperation {
constructor(
tableName: string,
@ -233,6 +235,7 @@ export class DropForeignKey extends ForeignKeyOperation {
}
}
/** SQLite: TypeORM recreates the table internally via changeColumn — beware CASCADE FK data loss. */
class ModifyNotNull extends TableOperation {
constructor(
tableName: string,

View File

@ -26,7 +26,9 @@ export class AddProjectIdToVariableTable1758794506893 implements ReversibleMigra
`ALTER TABLE ${variablesTableName} DROP CONSTRAINT ${tablePrefix}variables_key_key;`,
);
await addColumns(VARIABLES_TABLE_NAME, [column('projectId').varchar(36)]);
await addColumns(VARIABLES_TABLE_NAME, [column('projectId').varchar(36)], {
recreatesOnSqlite: true,
});
await addForeignKey(VARIABLES_TABLE_NAME, 'projectId', ['project', 'id'], undefined, 'CASCADE');
// Create index for unique project key (projectId not null)
@ -65,7 +67,7 @@ export class AddProjectIdToVariableTable1758794506893 implements ReversibleMigra
// Remove foreign key constraints and drop the projectId column
await dropForeignKey(VARIABLES_TABLE_NAME, 'projectId', ['project', 'id']);
await dropColumns(VARIABLES_TABLE_NAME, ['projectId']);
await dropColumns(VARIABLES_TABLE_NAME, ['projectId'], { recreatesOnSqlite: true });
// Recreate the original unique index on key
await queryRunner.query(`

View File

@ -15,9 +15,11 @@ export class AddCreatorIdToProjectTable1764276827837 implements ReversibleMigrat
schemaBuilder: { addColumns, addForeignKey, column },
queryRunner,
}: MigrationContext) {
await addColumns(table.project, [
column('creatorId').uuid.comment('ID of the user who created the project'),
]);
await addColumns(
table.project,
[column('creatorId').uuid.comment('ID of the user who created the project')],
{ recreatesOnSqlite: true },
);
await addForeignKey(table.project, 'creatorId', ['user', 'id'], FOREIGN_KEY_NAME, 'SET NULL');
@ -38,6 +40,6 @@ export class AddCreatorIdToProjectTable1764276827837 implements ReversibleMigrat
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(table.project, ['creatorId']);
await dropColumns(table.project, ['creatorId'], { recreatesOnSqlite: true });
}
}

View File

@ -8,11 +8,15 @@ export class AddResolvableFieldsToCredentials1764689448000 implements Reversible
withFKsDisabled = true as const;
async up({ schemaBuilder: { addColumns, addForeignKey, column } }: MigrationContext) {
await addColumns(credentialsTableName, [
column('isResolvable').bool.notNull.default(false),
column('resolvableAllowFallback').bool.notNull.default(false),
column('resolverId').varchar(16),
]);
await addColumns(
credentialsTableName,
[
column('isResolvable').bool.notNull.default(false),
column('resolvableAllowFallback').bool.notNull.default(false),
column('resolverId').varchar(16),
],
{ recreatesOnSqlite: true },
);
await addForeignKey(
credentialsTableName,
@ -24,10 +28,10 @@ export class AddResolvableFieldsToCredentials1764689448000 implements Reversible
}
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
await dropColumns(credentialsTableName, [
'isResolvable',
'resolvableAllowFallback',
'resolverId',
]);
await dropColumns(
credentialsTableName,
['isResolvable', 'resolvableAllowFallback', 'resolverId'],
{ recreatesOnSqlite: true },
);
}
}