From d509dad4756f58d264cb4649a8252dfb04d383e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 21 Nov 2025 13:44:48 +0100 Subject: [PATCH] refactor(core): Set up binary data table (#22137) --- .../1763716655000-CreateBinaryDataTable.ts | 26 +++++++++++++ packages/@n8n/db/src/migrations/dsl/column.ts | 16 +++++++- packages/@n8n/db/src/migrations/dsl/table.ts | 37 ++++++++++++++++++- .../@n8n/db/src/migrations/mysqldb/index.ts | 2 + .../db/src/migrations/postgresdb/index.ts | 2 + .../@n8n/db/src/migrations/sqlite/index.ts | 2 + 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 packages/@n8n/db/src/migrations/common/1763716655000-CreateBinaryDataTable.ts diff --git a/packages/@n8n/db/src/migrations/common/1763716655000-CreateBinaryDataTable.ts b/packages/@n8n/db/src/migrations/common/1763716655000-CreateBinaryDataTable.ts new file mode 100644 index 00000000000..380caf450a3 --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1763716655000-CreateBinaryDataTable.ts @@ -0,0 +1,26 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +const tableName = 'binary_data'; + +export class CreateBinaryDataTable1763716655000 implements ReversibleMigration { + async up({ schemaBuilder: { createTable, column } }: MigrationContext) { + await createTable(tableName) + .withColumns( + column('fileId').uuid.primary.notNull, + column('sourceType') + .varchar(50) + .notNull.comment("Source the file belongs to, e.g. 'execution'"), + column('sourceId').varchar(255).notNull.comment('ID of the source, e.g. execution ID'), + column('data').binary.notNull.comment('Raw, not base64 encoded'), + column('mimeType').varchar(255), + column('fileName').varchar(255), + column('fileSize').int.notNull.comment('In bytes'), + ) + .withEnumCheck('sourceType', ['execution', 'chat_message_attachment']) + .withIndexOn(['sourceType', 'sourceId']).withTimestamps; + } + + async down({ schemaBuilder: { dropTable } }: MigrationContext) { + await dropTable(tableName); + } +} diff --git a/packages/@n8n/db/src/migrations/dsl/column.ts b/packages/@n8n/db/src/migrations/dsl/column.ts index 6c09bc868cb..9aef4e433e5 100644 --- a/packages/@n8n/db/src/migrations/dsl/column.ts +++ b/packages/@n8n/db/src/migrations/dsl/column.ts @@ -12,7 +12,8 @@ export class Column { | 'uuid' | 'double' | 'bigint' - | 'smallint'; + | 'smallint' + | 'binary'; private isGenerated = false; @@ -99,6 +100,11 @@ export class Column { return this; } + get binary() { + this.type = 'binary'; + return this; + } + get primary() { this.isPrimary = true; return this; @@ -190,6 +196,14 @@ export class Column { } } else if (type === 'bigint') { options.type = 'bigint'; + } else if (type === 'binary') { + if (isPostgres) { + options.type = 'bytea'; + } else if (isMysql) { + options.type = 'longblob'; + } else if (isSqlite) { + options.type = 'blob'; + } } if ( diff --git a/packages/@n8n/db/src/migrations/dsl/table.ts b/packages/@n8n/db/src/migrations/dsl/table.ts index 06d9f790dfe..fc9f7a98b6e 100644 --- a/packages/@n8n/db/src/migrations/dsl/table.ts +++ b/packages/@n8n/db/src/migrations/dsl/table.ts @@ -1,5 +1,5 @@ import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from '@n8n/typeorm'; -import { Table, TableColumn, TableForeignKey, TableUnique } from '@n8n/typeorm'; +import { Table, TableCheck, TableColumn, TableForeignKey, TableUnique } from '@n8n/typeorm'; import { UnexpectedError } from 'n8n-workflow'; import LazyPromise from 'p-lazy'; @@ -28,6 +28,10 @@ export class CreateTable extends TableOperation { private foreignKeys = new Set(); + private checks = new Set(); + + private enumChecks: Array<{ columnName: string; values: string[] }> = []; + withColumns(...columns: Column[]) { this.columns.push(...columns); return this; @@ -58,6 +62,16 @@ export class CreateTable extends TableOperation { return this; } + withCheck(name: string, expression: string) { + this.checks.add(new TableCheck({ name, expression })); + return this; + } + + withEnumCheck(columnName: string, values: string[]) { + this.enumChecks.push({ columnName, values }); + return this; + } + withForeignKey( columnName: string, ref: { @@ -82,7 +96,25 @@ export class CreateTable extends TableOperation { async execute(queryRunner: QueryRunner) { const { driver } = queryRunner.connection; - const { columns, tableName: name, prefix, indices, uniqueConstraints, foreignKeys } = this; + const { + columns, + tableName: name, + prefix, + indices, + uniqueConstraints, + foreignKeys, + checks, + enumChecks, + } = this; + + for (const { columnName, values } of enumChecks) { + const checkName = `CHK_${prefix}${name}_${columnName}`; + const escapedColumnName = driver.escape(columnName); + const escapedValues = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(', '); + const expression = `${escapedColumnName} IN (${escapedValues})`; + checks.add(new TableCheck({ name: checkName, expression })); + } + return await queryRunner.createTable( new Table({ name: `${prefix}${name}`, @@ -90,6 +122,7 @@ export class CreateTable extends TableOperation { ...(indices.size ? { indices: [...indices] } : {}), ...(uniqueConstraints.size ? { uniques: [...uniqueConstraints] } : {}), ...(foreignKeys.size ? { foreignKeys: [...foreignKeys] } : {}), + ...(checks.size ? { checks: [...checks] } : {}), ...('mysql' in driver ? { engine: 'InnoDB' } : {}), }), true, diff --git a/packages/@n8n/db/src/migrations/mysqldb/index.ts b/packages/@n8n/db/src/migrations/mysqldb/index.ts index 333d7816a18..f33829037a9 100644 --- a/packages/@n8n/db/src/migrations/mysqldb/index.ts +++ b/packages/@n8n/db/src/migrations/mysqldb/index.ts @@ -117,6 +117,7 @@ import { AddIsGlobalColumnToCredentialsTable1762771954619 } from '../common/1762 import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields'; import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn'; import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar'; +import { CreateBinaryDataTable1763716655000 } from '../common/1763716655000-CreateBinaryDataTable'; import type { Migration } from '../migration-types'; export const mysqlMigrations: Migration[] = [ @@ -239,4 +240,5 @@ export const mysqlMigrations: Migration[] = [ ChangeOAuthStateColumnToUnboundedVarchar1763572724000, AddAttachmentsToChatHubMessages1761773155024, AddActiveVersionIdColumn1763047800000, + CreateBinaryDataTable1763716655000, ]; diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index 476402e79bf..7b88fa65b01 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -117,6 +117,7 @@ import { AddIsGlobalColumnToCredentialsTable1762771954619 } from '../common/1762 import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields'; import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn'; import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar'; +import { CreateBinaryDataTable1763716655000 } from '../common/1763716655000-CreateBinaryDataTable'; import type { Migration } from '../migration-types'; export const postgresMigrations: Migration[] = [ @@ -239,4 +240,5 @@ export const postgresMigrations: Migration[] = [ ChangeOAuthStateColumnToUnboundedVarchar1763572724000, AddAttachmentsToChatHubMessages1761773155024, AddActiveVersionIdColumn1763047800000, + CreateBinaryDataTable1763716655000, ]; diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts index aff67ae6b44..9f618624567 100644 --- a/packages/@n8n/db/src/migrations/sqlite/index.ts +++ b/packages/@n8n/db/src/migrations/sqlite/index.ts @@ -113,6 +113,7 @@ import { AddIsGlobalColumnToCredentialsTable1762771954619 } from '../common/1762 import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields'; import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn'; import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar'; +import { CreateBinaryDataTable1763716655000 } from '../common/1763716655000-CreateBinaryDataTable'; import type { Migration } from '../migration-types'; const sqliteMigrations: Migration[] = [ @@ -231,6 +232,7 @@ const sqliteMigrations: Migration[] = [ ChangeOAuthStateColumnToUnboundedVarchar1763572724000, AddAttachmentsToChatHubMessages1761773155024, AddActiveVersionIdColumn1763047800000, + CreateBinaryDataTable1763716655000, ]; export { sqliteMigrations };