From 9e48ae305248b483cc0b3fd7409a8d337b303024 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 11 Jul 2025 11:35:10 -0400 Subject: [PATCH] feat: naming strategy (#19848) * feat: naming strategy * feat: detect renames --- server/src/bin/migrations.ts | 25 +++++- server/src/schema/tables/stack.table.ts | 6 +- .../comparers/column.comparer.spec.ts | 1 + .../sql-tools/comparers/column.comparer.ts | 31 +++++++- .../comparers/constraint.comparer.ts | 33 +++++++- .../src/sql-tools/comparers/index.comparer.ts | 18 ++++- server/src/sql-tools/contexts/base-context.ts | 30 +++++++ .../sql-tools/contexts/processor-context.ts | 17 +--- server/src/sql-tools/decorators/index.ts | 22 ------ server/src/sql-tools/helpers.ts | 79 ++++++++++++++----- server/src/sql-tools/naming/default.naming.ts | 50 ++++++++++++ server/src/sql-tools/naming/hash.naming.ts | 51 ++++++++++++ .../src/sql-tools/naming/naming.interface.ts | 59 ++++++++++++++ .../processors/check-constraint.processor.ts | 5 +- .../sql-tools/processors/column.processor.ts | 2 +- .../processors/database.processor.ts | 3 +- .../foreign-key-column.processor.ts | 18 +++-- .../foreign-key-constraint.processor.ts | 27 +++++-- .../sql-tools/processors/index.processor.ts | 19 ++++- .../primary-key-contraint.processor.ts | 11 ++- .../sql-tools/processors/table.processor.ts | 3 +- .../sql-tools/processors/trigger.processor.ts | 18 +++-- .../processors/unique-constraint.processor.ts | 15 ++-- server/src/sql-tools/public_api.ts | 26 +++++- server/src/sql-tools/readers/column.reader.ts | 2 + server/src/sql-tools/schema-diff.spec.ts | 6 ++ server/src/sql-tools/schema-diff.ts | 6 ++ server/src/sql-tools/schema-from-code.ts | 3 +- .../transformers/column.transformer.spec.ts | 4 + .../transformers/column.transformer.ts | 10 +-- .../transformers/constraint.transformer.ts | 16 ++-- .../transformers/index.transformer.ts | 10 +-- .../transformers/table.transformer.spec.ts | 5 ++ server/src/sql-tools/types.ts | 11 ++- ...foreign-key-with-unique-constraint.stub.ts | 2 +- 35 files changed, 517 insertions(+), 127 deletions(-) delete mode 100644 server/src/sql-tools/decorators/index.ts create mode 100644 server/src/sql-tools/naming/default.naming.ts create mode 100644 server/src/sql-tools/naming/hash.naming.ts create mode 100644 server/src/sql-tools/naming/naming.interface.ts diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 81f5c7e0ab..a9832a14f7 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -9,7 +9,13 @@ import { ConfigRepository } from 'src/repositories/config.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import 'src/schema'; -import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; +import { + DefaultNamingStrategy, + HashNamingStrategy, + schemaDiff, + schemaFromCode, + schemaFromDatabase, +} from 'src/sql-tools'; import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; const main = async () => { @@ -107,7 +113,22 @@ const compare = async () => { const { database } = configRepository.getEnv(); const db = postgres(asPostgresConnectionConfig(database.config)); - const source = schemaFromCode({ overrides: true }); + const tables = new Set(); + const preferred = new DefaultNamingStrategy(); + const fallback = new HashNamingStrategy(); + + const source = schemaFromCode({ + overrides: true, + namingStrategy: { + getName(item) { + if ('tableName' in item && tables.has(item.tableName)) { + return preferred.getName(item); + } + + return fallback.getName(item); + }, + }, + }); const target = await schemaFromDatabase(db, {}); console.log(source.warnings.join('\n')); diff --git a/server/src/schema/tables/stack.table.ts b/server/src/schema/tables/stack.table.ts index fa206f24f2..02afc2ed50 100644 --- a/server/src/schema/tables/stack.table.ts +++ b/server/src/schema/tables/stack.table.ts @@ -35,7 +35,11 @@ export class StackTable { updateId!: Generated; //TODO: Add constraint to ensure primary asset exists in the assets array - @ForeignKeyColumn(() => AssetTable, { nullable: false, unique: true }) + @ForeignKeyColumn(() => AssetTable, { + nullable: false, + unique: true, + uniqueConstraintName: 'REL_91704e101438fd0653f582426d', + }) primaryAssetId!: string; @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) diff --git a/server/src/sql-tools/comparers/column.comparer.spec.ts b/server/src/sql-tools/comparers/column.comparer.spec.ts index 25ef8543a8..fde237ad7b 100644 --- a/server/src/sql-tools/comparers/column.comparer.spec.ts +++ b/server/src/sql-tools/comparers/column.comparer.spec.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'; const testColumn: DatabaseColumn = { name: 'test', tableName: 'table1', + primary: false, nullable: false, isArray: false, type: 'character varying', diff --git a/server/src/sql-tools/comparers/column.comparer.ts b/server/src/sql-tools/comparers/column.comparer.ts index 5cc3f7a930..035fd6fc98 100644 --- a/server/src/sql-tools/comparers/column.comparer.ts +++ b/server/src/sql-tools/comparers/column.comparer.ts @@ -1,7 +1,32 @@ -import { getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; +import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types'; -export const compareColumns: Comparer = { +export const compareColumns = { + getRenameKey: (column) => { + return asRenameKey([ + column.tableName, + column.type, + column.nullable, + column.default, + column.storage, + column.primary, + column.isArray, + column.length, + column.identity, + column.enumName, + column.numericPrecision, + column.numericScale, + ]); + }, + onRename: (source, target) => [ + { + type: 'ColumnRename', + tableName: source.tableName, + oldName: target.name, + newName: source.name, + reason: Reason.Rename, + }, + ], onMissing: (source) => [ { type: 'ColumnAdd', @@ -67,7 +92,7 @@ export const compareColumns: Comparer = { return items; }, -}; +} satisfies Comparer; const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { return [ diff --git a/server/src/sql-tools/comparers/constraint.comparer.ts b/server/src/sql-tools/comparers/constraint.comparer.ts index 0ff6fbe131..0df670ecf3 100644 --- a/server/src/sql-tools/comparers/constraint.comparer.ts +++ b/server/src/sql-tools/comparers/constraint.comparer.ts @@ -1,4 +1,4 @@ -import { haveEqualColumns } from 'src/sql-tools/helpers'; +import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; import { CompareFunction, Comparer, @@ -13,6 +13,37 @@ import { } from 'src/sql-tools/types'; export const compareConstraints: Comparer = { + getRenameKey: (constraint) => { + switch (constraint.type) { + case ConstraintType.PRIMARY_KEY: + case ConstraintType.UNIQUE: { + return asRenameKey([constraint.type, constraint.tableName, ...constraint.columnNames.toSorted()]); + } + + case ConstraintType.FOREIGN_KEY: { + return asRenameKey([ + constraint.type, + constraint.tableName, + ...constraint.columnNames.toSorted(), + constraint.referenceTableName, + ...constraint.referenceColumnNames.toSorted(), + ]); + } + + case ConstraintType.CHECK: { + return asRenameKey([constraint.type, constraint.tableName, constraint.expression]); + } + } + }, + onRename: (source, target) => [ + { + type: 'ConstraintRename', + tableName: target.tableName, + oldName: target.name, + newName: source.name, + reason: Reason.Rename, + }, + ], onMissing: (source) => [ { type: 'ConstraintAdd', diff --git a/server/src/sql-tools/comparers/index.comparer.ts b/server/src/sql-tools/comparers/index.comparer.ts index 99571cf61a..5625560df7 100644 --- a/server/src/sql-tools/comparers/index.comparer.ts +++ b/server/src/sql-tools/comparers/index.comparer.ts @@ -1,7 +1,23 @@ -import { haveEqualColumns } from 'src/sql-tools/helpers'; +import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types'; export const compareIndexes: Comparer = { + getRenameKey: (index) => { + if (index.override) { + return index.override.value.sql.replace(index.name, 'INDEX_NAME'); + } + + return asRenameKey([index.tableName, ...(index.columnNames || []).toSorted(), index.unique]); + }, + onRename: (source, target) => [ + { + type: 'IndexRename', + tableName: source.tableName, + oldName: target.name, + newName: source.name, + reason: Reason.Rename, + }, + ], onMissing: (source) => [ { type: 'IndexCreate', diff --git a/server/src/sql-tools/contexts/base-context.ts b/server/src/sql-tools/contexts/base-context.ts index 620286cae3..0fa7230a00 100644 --- a/server/src/sql-tools/contexts/base-context.ts +++ b/server/src/sql-tools/contexts/base-context.ts @@ -1,3 +1,6 @@ +import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming'; +import { HashNamingStrategy } from 'src/sql-tools/naming/hash.naming'; +import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface'; import { BaseContextOptions, DatabaseEnum, @@ -11,6 +14,26 @@ import { const asOverrideKey = (type: string, name: string) => `${type}:${name}`; +const isNamingInterface = (strategy: any): strategy is NamingInterface => { + return typeof strategy === 'object' && typeof strategy.getName === 'function'; +}; + +const asNamingStrategy = (strategy: 'hash' | 'default' | NamingInterface): NamingInterface => { + if (isNamingInterface(strategy)) { + return strategy; + } + + switch (strategy) { + case 'hash': { + return new HashNamingStrategy(); + } + + default: { + return new DefaultNamingStrategy(); + } + } +}; + export class BaseContext { databaseName: string; schemaName: string; @@ -24,10 +47,17 @@ export class BaseContext { overrides: DatabaseOverride[] = []; warnings: string[] = []; + private namingStrategy: NamingInterface; + constructor(options: BaseContextOptions) { this.databaseName = options.databaseName ?? 'postgres'; this.schemaName = options.schemaName ?? 'public'; this.overrideTableName = options.overrideTableName ?? 'migration_overrides'; + this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash'); + } + + getNameFor(item: NamingItem) { + return this.namingStrategy.getName(item); } getTableByName(name: string) { diff --git a/server/src/sql-tools/contexts/processor-context.ts b/server/src/sql-tools/contexts/processor-context.ts index 562880a925..3ab196b0af 100644 --- a/server/src/sql-tools/contexts/processor-context.ts +++ b/server/src/sql-tools/contexts/processor-context.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { ColumnOptions, TableOptions } from 'src/sql-tools/decorators'; -import { asKey } from 'src/sql-tools/helpers'; +import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; +import { TableOptions } from 'src/sql-tools/decorators/table.decorator'; import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types'; type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map }; @@ -59,19 +59,6 @@ export class ProcessorContext extends BaseContext { tableMetadata.methodToColumn.set(propertyName, column); } - asIndexName(table: string, columns?: string[], where?: string) { - const items: string[] = []; - for (const columnName of columns ?? []) { - items.push(columnName); - } - - if (where) { - items.push(where); - } - - return asKey('IDX_', table, items); - } - warnMissingTable(context: string, object: object, propertyName?: symbol | string) { const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); this.warn(context, `Unable to find table (${label})`); diff --git a/server/src/sql-tools/decorators/index.ts b/server/src/sql-tools/decorators/index.ts deleted file mode 100644 index 86affe5002..0000000000 --- a/server/src/sql-tools/decorators/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -export * from 'src/sql-tools/decorators/after-delete.decorator'; -export * from 'src/sql-tools/decorators/after-insert.decorator'; -export * from 'src/sql-tools/decorators/before-update.decorator'; -export * from 'src/sql-tools/decorators/check.decorator'; -export * from 'src/sql-tools/decorators/column.decorator'; -export * from 'src/sql-tools/decorators/configuration-parameter.decorator'; -export * from 'src/sql-tools/decorators/create-date-column.decorator'; -export * from 'src/sql-tools/decorators/database.decorator'; -export * from 'src/sql-tools/decorators/delete-date-column.decorator'; -export * from 'src/sql-tools/decorators/extension.decorator'; -export * from 'src/sql-tools/decorators/extensions.decorator'; -export * from 'src/sql-tools/decorators/foreign-key-column.decorator'; -export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; -export * from 'src/sql-tools/decorators/generated-column.decorator'; -export * from 'src/sql-tools/decorators/index.decorator'; -export * from 'src/sql-tools/decorators/primary-column.decorator'; -export * from 'src/sql-tools/decorators/primary-generated-column.decorator'; -export * from 'src/sql-tools/decorators/table.decorator'; -export * from 'src/sql-tools/decorators/trigger-function.decorator'; -export * from 'src/sql-tools/decorators/trigger.decorator'; -export * from 'src/sql-tools/decorators/unique.decorator'; -export * from 'src/sql-tools/decorators/update-date-column.decorator'; diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts index afa735e282..8131f0350b 100644 --- a/server/src/sql-tools/helpers.ts +++ b/server/src/sql-tools/helpers.ts @@ -2,13 +2,6 @@ import { createHash } from 'node:crypto'; import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types'; -export const asMetadataKey = (name: string) => `sql-tools:${name}`; - -export const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); -// match TypeORM -export const asKey = (prefix: string, tableName: string, values: string[]) => - (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); - export const asOptions = (options: string | T): T => { if (typeof options === 'string') { return { name: options } as T; @@ -79,6 +72,10 @@ export const compare = ( const items: SchemaDiff[] = []; const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); + const missingKeys = new Set(); + const extraKeys = new Set(); + + // common keys for (const key of keys) { const source = sourceMap[key]; const target = targetMap[key]; @@ -92,22 +89,63 @@ export const compare = ( } if (source && !target) { - items.push(...comparer.onMissing(source)); - } else if (!source && target) { - items.push(...comparer.onExtra(target)); - } else { - if ( - haveEqualOverrides( - source as unknown as { override?: DatabaseOverride }, - target as unknown as { override?: DatabaseOverride }, - ) - ) { + missingKeys.add(key); + continue; + } + + if (!source && target) { + extraKeys.add(key); + continue; + } + + if ( + haveEqualOverrides( + source as unknown as { override?: DatabaseOverride }, + target as unknown as { override?: DatabaseOverride }, + ) + ) { + continue; + } + + items.push(...comparer.onCompare(source, target)); + } + + // renames + if (comparer.getRenameKey && comparer.onRename) { + const renameMap: Record = {}; + for (const sourceKey of missingKeys) { + const source = sourceMap[sourceKey]; + const renameKey = comparer.getRenameKey(source); + renameMap[renameKey] = sourceKey; + } + + for (const targetKey of extraKeys) { + const target = targetMap[targetKey]; + const renameKey = comparer.getRenameKey(target); + const sourceKey = renameMap[renameKey]; + if (!sourceKey) { continue; } - items.push(...comparer.onCompare(source, target)); + + const source = sourceMap[sourceKey]; + + items.push(...comparer.onRename(source, target)); + + missingKeys.delete(sourceKey); + extraKeys.delete(targetKey); } } + // missing + for (const key of missingKeys) { + items.push(...comparer.onMissing(sourceMap[key])); + } + + // extra + for (const key of extraKeys) { + items.push(...comparer.onExtra(targetMap[key])); + } + return items; }; @@ -186,8 +224,6 @@ export const asColumnComment = (tableName: string, columnName: string, comment: export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', '); -export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, [...columns]); - export const asJsonString = (value: unknown): string => { return `'${escape(JSON.stringify(value))}'::jsonb`; }; @@ -202,3 +238,6 @@ const escape = (value: string) => { .replaceAll(/[\r]/g, String.raw`\r`) .replaceAll(/[\t]/g, String.raw`\t`); }; + +export const asRenameKey = (values: Array) => + values.map((value) => value ?? '').join('|'); diff --git a/server/src/sql-tools/naming/default.naming.ts b/server/src/sql-tools/naming/default.naming.ts new file mode 100644 index 0000000000..807580169d --- /dev/null +++ b/server/src/sql-tools/naming/default.naming.ts @@ -0,0 +1,50 @@ +import { sha1 } from 'src/sql-tools/helpers'; +import { NamingItem } from 'src/sql-tools/naming/naming.interface'; + +const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); + +export class DefaultNamingStrategy { + getName(item: NamingItem): string { + switch (item.type) { + case 'database': { + return asSnakeCase(item.name); + } + + case 'table': { + return asSnakeCase(item.name); + } + + case 'column': { + return item.name; + } + + case 'primaryKey': { + return `${item.tableName}_pkey`; + } + + case 'foreignKey': { + return `${item.tableName}_${item.columnNames.join('_')}_fkey`; + } + + case 'check': { + return `${item.tableName}_${sha1(item.expression).slice(0, 8)}_chk`; + } + + case 'unique': { + return `${item.tableName}_${item.columnNames.join('_')}_uq`; + } + + case 'index': { + if (item.columnNames) { + return `${item.tableName}_${item.columnNames.join('_')}_idx`; + } + + return `${item.tableName}_${sha1(item.expression || item.where || '').slice(0, 8)}_idx`; + } + + case 'trigger': { + return `${item.tableName}_${item.functionName}`; + } + } + } +} diff --git a/server/src/sql-tools/naming/hash.naming.ts b/server/src/sql-tools/naming/hash.naming.ts new file mode 100644 index 0000000000..575d0f1239 --- /dev/null +++ b/server/src/sql-tools/naming/hash.naming.ts @@ -0,0 +1,51 @@ +import { sha1 } from 'src/sql-tools/helpers'; +import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming'; +import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface'; + +const fallback = new DefaultNamingStrategy(); + +const asKey = (prefix: string, tableName: string, values: string[]) => + (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); + +export class HashNamingStrategy implements NamingInterface { + getName(item: NamingItem): string { + switch (item.type) { + case 'primaryKey': { + return asKey('PK_', item.tableName, item.columnNames); + } + + case 'foreignKey': { + return asKey('FK_', item.tableName, item.columnNames); + } + + case 'check': { + return asKey('CHK_', item.tableName, [item.expression]); + } + + case 'unique': { + return asKey('UQ_', item.tableName, item.columnNames); + } + + case 'index': { + const items: string[] = []; + for (const columnName of item.columnNames ?? []) { + items.push(columnName); + } + + if (item.where) { + items.push(item.where); + } + + return asKey('IDX_', item.tableName, items); + } + + case 'trigger': { + return asKey('TR_', item.tableName, [...item.actions, item.scope, item.timing, item.functionName]); + } + + default: { + return fallback.getName(item); + } + } + } +} diff --git a/server/src/sql-tools/naming/naming.interface.ts b/server/src/sql-tools/naming/naming.interface.ts new file mode 100644 index 0000000000..f331a22c46 --- /dev/null +++ b/server/src/sql-tools/naming/naming.interface.ts @@ -0,0 +1,59 @@ +import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; + +export type NamingItem = + | { + type: 'database'; + name: string; + } + | { + type: 'table'; + name: string; + } + | { + type: 'column'; + name: string; + } + | { + type: 'primaryKey'; + tableName: string; + columnNames: string[]; + } + | { + type: 'foreignKey'; + tableName: string; + columnNames: string[]; + referenceTableName: string; + referenceColumnNames: string[]; + } + | { + type: 'check'; + tableName: string; + expression: string; + } + | { + type: 'unique'; + tableName: string; + columnNames: string[]; + } + | { + type: 'index'; + tableName: string; + columnNames?: string[]; + expression?: string; + where?: string; + } + | { + type: 'trigger'; + tableName: string; + functionName: string; + actions: TriggerAction[]; + scope: TriggerScope; + timing: TriggerTiming; + columnNames?: string[]; + expression?: string; + where?: string; + }; + +export interface NamingInterface { + getName(item: NamingItem): string; +} diff --git a/server/src/sql-tools/processors/check-constraint.processor.ts b/server/src/sql-tools/processors/check-constraint.processor.ts index 21ee47ccc6..5eba1015bf 100644 --- a/server/src/sql-tools/processors/check-constraint.processor.ts +++ b/server/src/sql-tools/processors/check-constraint.processor.ts @@ -1,4 +1,3 @@ -import { asKey } from 'src/sql-tools/helpers'; import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processCheckConstraints: Processor = (ctx, items) => { @@ -15,12 +14,10 @@ export const processCheckConstraints: Processor = (ctx, items) => { table.constraints.push({ type: ConstraintType.CHECK, - name: options.name || asCheckConstraintName(tableName, options.expression), + name: options.name || ctx.getNameFor({ type: 'check', tableName, expression: options.expression }), tableName, expression: options.expression, synchronize: options.synchronize ?? true, }); } }; - -const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); diff --git a/server/src/sql-tools/processors/column.processor.ts b/server/src/sql-tools/processors/column.processor.ts index 84c158a234..9b499b380b 100644 --- a/server/src/sql-tools/processors/column.processor.ts +++ b/server/src/sql-tools/processors/column.processor.ts @@ -13,7 +13,7 @@ export const processColumns: Processor = (ctx, items) => { continue; } - const columnName = options.name ?? String(propertyName); + const columnName = options.name ?? ctx.getNameFor({ type: 'column', name: String(propertyName) }); const existingColumn = table.columns.find((column) => column.name === columnName); if (existingColumn) { // TODO log warnings if column name is not unique diff --git a/server/src/sql-tools/processors/database.processor.ts b/server/src/sql-tools/processors/database.processor.ts index 7158cd9494..9f2e847fd6 100644 --- a/server/src/sql-tools/processors/database.processor.ts +++ b/server/src/sql-tools/processors/database.processor.ts @@ -1,10 +1,9 @@ -import { asSnakeCase } from 'src/sql-tools/helpers'; import { Processor } from 'src/sql-tools/types'; export const processDatabases: Processor = (ctx, items) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'database')) { - ctx.databaseName = options.name || asSnakeCase(object.name); + ctx.databaseName = options.name || ctx.getNameFor({ type: 'database', name: object.name }); } }; diff --git a/server/src/sql-tools/processors/foreign-key-column.processor.ts b/server/src/sql-tools/processors/foreign-key-column.processor.ts index e37b6e3f0b..6d147a78eb 100644 --- a/server/src/sql-tools/processors/foreign-key-column.processor.ts +++ b/server/src/sql-tools/processors/foreign-key-column.processor.ts @@ -1,4 +1,3 @@ -import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers'; import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; export const processForeignKeyColumns: Processor = (ctx, items) => { @@ -31,15 +30,24 @@ export const processForeignKeyColumns: Processor = (ctx, items) => { column.type = referenceColumns[0].type; } + const referenceTableName = referenceTable.name; const referenceColumnNames = referenceColumns.map((column) => column.name); - const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames); + const name = + options.constraintName || + ctx.getNameFor({ + type: 'foreignKey', + tableName: table.name, + columnNames, + referenceTableName, + referenceColumnNames, + }); table.constraints.push({ name, tableName: table.name, columnNames, type: ConstraintType.FOREIGN_KEY, - referenceTableName: referenceTable.name, + referenceTableName, referenceColumnNames, onUpdate: options.onUpdate as ActionType, onDelete: options.onDelete as ActionType, @@ -48,7 +56,7 @@ export const processForeignKeyColumns: Processor = (ctx, items) => { if (options.unique || options.uniqueConstraintName) { table.constraints.push({ - name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames), + name: options.uniqueConstraintName || ctx.getNameFor({ type: 'unique', tableName: table.name, columnNames }), tableName: table.name, columnNames, type: ConstraintType.UNIQUE, @@ -57,5 +65,3 @@ export const processForeignKeyColumns: Processor = (ctx, items) => { } } }; - -const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); diff --git a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts index 7c44ab2694..39d7508d11 100644 --- a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts +++ b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts @@ -1,4 +1,3 @@ -import { asForeignKeyConstraintName } from 'src/sql-tools/helpers'; import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; export const processForeignKeyConstraints: Processor = (ctx, items) => { @@ -46,18 +45,27 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => { continue; } - const referenceColumns = + const referenceTableName = referenceTable.name; + const referenceColumnNames = options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name); - const name = options.name || asForeignKeyConstraintName(table.name, options.columns); + const name = + options.name || + ctx.getNameFor({ + type: 'foreignKey', + tableName: table.name, + columnNames: options.columns, + referenceTableName, + referenceColumnNames, + }); table.constraints.push({ type: ConstraintType.FOREIGN_KEY, name, tableName: table.name, columnNames: options.columns, - referenceTableName: referenceTable.name, - referenceColumnNames: referenceColumns, + referenceTableName, + referenceColumnNames, onUpdate: options.onUpdate as ActionType, onDelete: options.onDelete as ActionType, synchronize: options.synchronize ?? true, @@ -68,8 +76,15 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => { } if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) { + const indexName = + options.indexName || + ctx.getNameFor({ + type: 'index', + tableName: table.name, + columnNames: options.columns, + }); table.indexes.push({ - name: options.indexName || ctx.asIndexName(table.name, options.columns), + name: indexName, tableName: table.name, columnNames: options.columns, unique: false, diff --git a/server/src/sql-tools/processors/index.processor.ts b/server/src/sql-tools/processors/index.processor.ts index cd2ee1507e..766e83fe8b 100644 --- a/server/src/sql-tools/processors/index.processor.ts +++ b/server/src/sql-tools/processors/index.processor.ts @@ -10,8 +10,17 @@ export const processIndexes: Processor = (ctx, items) => { continue; } + const indexName = + options.name || + ctx.getNameFor({ + type: 'index', + tableName: table.name, + columnNames: options.columns, + where: options.where, + }); + table.indexes.push({ - name: options.name || ctx.asIndexName(table.name, options.columns, options.where), + name: indexName, tableName: table.name, unique: options.unique ?? false, expression: options.expression, @@ -50,7 +59,13 @@ export const processIndexes: Processor = (ctx, items) => { continue; } - const indexName = options.indexName || ctx.asIndexName(table.name, [column.name]); + const indexName = + options.indexName || + ctx.getNameFor({ + type: 'index', + tableName: table.name, + columnNames: [column.name], + }); const isIndexPresent = table.indexes.some((index) => index.name === indexName); if (isIndexPresent) { diff --git a/server/src/sql-tools/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/processors/primary-key-contraint.processor.ts index 12fc27db7e..0971bfc337 100644 --- a/server/src/sql-tools/processors/primary-key-contraint.processor.ts +++ b/server/src/sql-tools/processors/primary-key-contraint.processor.ts @@ -1,4 +1,3 @@ -import { asKey } from 'src/sql-tools/helpers'; import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processPrimaryKeyConstraints: Processor = (ctx) => { @@ -15,7 +14,13 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => { const tableMetadata = ctx.getTableMetadata(table); table.constraints.push({ type: ConstraintType.PRIMARY_KEY, - name: tableMetadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames), + name: + tableMetadata.options.primaryConstraintName || + ctx.getNameFor({ + type: 'primaryKey', + tableName: table.name, + columnNames, + }), tableName: table.name, columnNames, synchronize: tableMetadata.options.synchronize ?? true, @@ -23,5 +28,3 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => { } } }; - -const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); diff --git a/server/src/sql-tools/processors/table.processor.ts b/server/src/sql-tools/processors/table.processor.ts index 7fec4eb317..993c9ec45d 100644 --- a/server/src/sql-tools/processors/table.processor.ts +++ b/server/src/sql-tools/processors/table.processor.ts @@ -1,4 +1,3 @@ -import { asSnakeCase } from 'src/sql-tools/helpers'; import { Processor } from 'src/sql-tools/types'; export const processTables: Processor = (ctx, items) => { @@ -14,7 +13,7 @@ export const processTables: Processor = (ctx, items) => { ctx.addTable( { - name: options.name || asSnakeCase(object.name), + name: options.name || ctx.getNameFor({ type: 'table', name: object.name }), columns: [], constraints: [], indexes: [], diff --git a/server/src/sql-tools/processors/trigger.processor.ts b/server/src/sql-tools/processors/trigger.processor.ts index 10b07a466a..b50b42cc49 100644 --- a/server/src/sql-tools/processors/trigger.processor.ts +++ b/server/src/sql-tools/processors/trigger.processor.ts @@ -1,5 +1,3 @@ -import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; -import { asKey } from 'src/sql-tools/helpers'; import { Processor } from 'src/sql-tools/types'; export const processTriggers: Processor = (ctx, items) => { @@ -12,8 +10,19 @@ export const processTriggers: Processor = (ctx, items) => { continue; } + const triggerName = + options.name || + ctx.getNameFor({ + type: 'trigger', + tableName: table.name, + actions: options.actions, + scope: options.scope, + timing: options.timing, + functionName: options.functionName, + }); + table.triggers.push({ - name: options.name || asTriggerName(table.name, options), + name: triggerName, tableName: table.name, timing: options.timing, actions: options.actions, @@ -26,6 +35,3 @@ export const processTriggers: Processor = (ctx, items) => { }); } }; - -const asTriggerName = (table: string, trigger: TriggerOptions) => - asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]); diff --git a/server/src/sql-tools/processors/unique-constraint.processor.ts b/server/src/sql-tools/processors/unique-constraint.processor.ts index 4d962a0a04..0cbfc26a70 100644 --- a/server/src/sql-tools/processors/unique-constraint.processor.ts +++ b/server/src/sql-tools/processors/unique-constraint.processor.ts @@ -1,4 +1,3 @@ -import { asKey } from 'src/sql-tools/helpers'; import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processUniqueConstraints: Processor = (ctx, items) => { @@ -16,7 +15,7 @@ export const processUniqueConstraints: Processor = (ctx, items) => { table.constraints.push({ type: ConstraintType.UNIQUE, - name: options.name || asUniqueConstraintName(tableName, columnNames), + name: options.name || ctx.getNameFor({ type: 'unique', tableName, columnNames }), tableName, columnNames, synchronize: options.synchronize ?? true, @@ -41,9 +40,17 @@ export const processUniqueConstraints: Processor = (ctx, items) => { } if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { + const uniqueConstraintName = + options.uniqueConstraintName || + ctx.getNameFor({ + type: 'unique', + tableName: table.name, + columnNames: [column.name], + }); + table.constraints.push({ type: ConstraintType.UNIQUE, - name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), + name: uniqueConstraintName, tableName: table.name, columnNames: [column.name], synchronize: options.synchronize ?? true, @@ -51,5 +58,3 @@ export const processUniqueConstraints: Processor = (ctx, items) => { } } }; - -const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts index aaef55dd8d..9e7983383e 100644 --- a/server/src/sql-tools/public_api.ts +++ b/server/src/sql-tools/public_api.ts @@ -1,4 +1,28 @@ -export * from 'src/sql-tools/decorators'; +export * from 'src/sql-tools/decorators/after-delete.decorator'; +export * from 'src/sql-tools/decorators/after-insert.decorator'; +export * from 'src/sql-tools/decorators/before-update.decorator'; +export * from 'src/sql-tools/decorators/check.decorator'; +export * from 'src/sql-tools/decorators/column.decorator'; +export * from 'src/sql-tools/decorators/configuration-parameter.decorator'; +export * from 'src/sql-tools/decorators/create-date-column.decorator'; +export * from 'src/sql-tools/decorators/database.decorator'; +export * from 'src/sql-tools/decorators/delete-date-column.decorator'; +export * from 'src/sql-tools/decorators/extension.decorator'; +export * from 'src/sql-tools/decorators/extensions.decorator'; +export * from 'src/sql-tools/decorators/foreign-key-column.decorator'; +export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; +export * from 'src/sql-tools/decorators/generated-column.decorator'; +export * from 'src/sql-tools/decorators/index.decorator'; +export * from 'src/sql-tools/decorators/primary-column.decorator'; +export * from 'src/sql-tools/decorators/primary-generated-column.decorator'; +export * from 'src/sql-tools/decorators/table.decorator'; +export * from 'src/sql-tools/decorators/trigger-function.decorator'; +export * from 'src/sql-tools/decorators/trigger.decorator'; +export * from 'src/sql-tools/decorators/unique.decorator'; +export * from 'src/sql-tools/decorators/update-date-column.decorator'; +export * from 'src/sql-tools/naming/default.naming'; +export * from 'src/sql-tools/naming/hash.naming'; +export * from 'src/sql-tools/naming/naming.interface'; export * from 'src/sql-tools/register-enum'; export * from 'src/sql-tools/register-function'; export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff'; diff --git a/server/src/sql-tools/readers/column.reader.ts b/server/src/sql-tools/readers/column.reader.ts index dfd2878185..249bd77f2c 100644 --- a/server/src/sql-tools/readers/column.reader.ts +++ b/server/src/sql-tools/readers/column.reader.ts @@ -76,6 +76,8 @@ export const readColumns: Reader = async (ctx, db) => { const item: DatabaseColumn = { type: column.data_type as ColumnType, + // TODO infer this from PK constraints + primary: false, name: columnName, tableName: column.table_name, nullable: column.is_nullable === 'YES', diff --git a/server/src/sql-tools/schema-diff.spec.ts b/server/src/sql-tools/schema-diff.spec.ts index 0667d22d6b..fe249b4e29 100644 --- a/server/src/sql-tools/schema-diff.spec.ts +++ b/server/src/sql-tools/schema-diff.spec.ts @@ -28,6 +28,7 @@ const fromColumn = (column: Partial>): Databas columns: [ { name: 'column1', + primary: false, synchronize: true, isArray: false, type: 'character varying', @@ -63,6 +64,7 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => { columns: [ { name: 'column1', + primary: false, synchronize: true, isArray: false, type: 'character varying', @@ -97,6 +99,7 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => { columns: [ { name: 'column1', + primary: false, synchronize: true, isArray: false, type: 'character varying', @@ -140,6 +143,7 @@ const newSchema = (schema: { columns.push({ tableName, name: columnName, + primary: false, type: column.type || 'character varying', isArray: column.isArray ?? false, nullable: column.nullable ?? false, @@ -182,6 +186,7 @@ describe(schemaDiff.name, () => { const column: DatabaseColumn = { type: 'character varying', tableName: 'table1', + primary: false, name: 'column1', isArray: false, nullable: false, @@ -264,6 +269,7 @@ describe(schemaDiff.name, () => { column: { tableName: 'table1', isArray: false, + primary: false, name: 'column2', nullable: false, type: 'character varying', diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts index 24091a0256..5dd3c26fdf 100644 --- a/server/src/sql-tools/schema-diff.ts +++ b/server/src/sql-tools/schema-diff.ts @@ -40,10 +40,13 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio TableDrop: [], ColumnAdd: [], ColumnAlter: [], + ColumnRename: [], ColumnDrop: [], ConstraintAdd: [], ConstraintDrop: [], + ConstraintRename: [], IndexCreate: [], + IndexRename: [], IndexDrop: [], TriggerCreate: [], TriggerDrop: [], @@ -72,11 +75,14 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio ...itemMap.TableCreate, ...itemMap.ColumnAlter, ...itemMap.ColumnAdd, + ...itemMap.ColumnRename, ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK), + ...itemMap.ConstraintRename, ...itemMap.IndexCreate, + ...itemMap.IndexRename, ...itemMap.TriggerCreate, ...itemMap.ColumnDrop, ...itemMap.TableDrop, diff --git a/server/src/sql-tools/schema-from-code.ts b/server/src/sql-tools/schema-from-code.ts index 6b4ab93b8d..2e19f414e4 100644 --- a/server/src/sql-tools/schema-from-code.ts +++ b/server/src/sql-tools/schema-from-code.ts @@ -20,9 +20,9 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { name: ctx.overrideTableName, columns: [ { - primary: true, name: 'name', tableName: ctx.overrideTableName, + primary: true, type: 'character varying', nullable: false, isArray: false, @@ -31,6 +31,7 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { { name: 'value', tableName: ctx.overrideTableName, + primary: false, type: 'jsonb', nullable: false, isArray: false, diff --git a/server/src/sql-tools/transformers/column.transformer.spec.ts b/server/src/sql-tools/transformers/column.transformer.spec.ts index 1386b08c93..1e29d4bff6 100644 --- a/server/src/sql-tools/transformers/column.transformer.spec.ts +++ b/server/src/sql-tools/transformers/column.transformer.spec.ts @@ -13,6 +13,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'character varying', nullable: false, isArray: false, @@ -30,6 +31,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'character varying', nullable: true, isArray: false, @@ -47,6 +49,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'character varying', enumName: 'table1_column1_enum', nullable: true, @@ -65,6 +68,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'boolean', nullable: true, isArray: true, diff --git a/server/src/sql-tools/transformers/column.transformer.ts b/server/src/sql-tools/transformers/column.transformer.ts index 43188c24c2..ffa565e533 100644 --- a/server/src/sql-tools/transformers/column.transformer.ts +++ b/server/src/sql-tools/transformers/column.transformer.ts @@ -12,8 +12,12 @@ export const transformColumns: SqlTransformer = (ctx, item) => { return asColumnAlter(item.tableName, item.columnName, item.changes); } + case 'ColumnRename': { + return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`; + } + case 'ColumnDrop': { - return asColumnDrop(item.tableName, item.columnName); + return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`; } default: { @@ -28,10 +32,6 @@ const asColumnAdd = (column: DatabaseColumn): string => { ); }; -const asColumnDrop = (tableName: string, columnName: string): string => { - return `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}";`; -}; - export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => { const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`; const items: string[] = []; diff --git a/server/src/sql-tools/transformers/constraint.transformer.ts b/server/src/sql-tools/transformers/constraint.transformer.ts index bb8b30e7ce..94421e56fa 100644 --- a/server/src/sql-tools/transformers/constraint.transformer.ts +++ b/server/src/sql-tools/transformers/constraint.transformer.ts @@ -5,11 +5,15 @@ import { ActionType, ConstraintType, DatabaseConstraint } from 'src/sql-tools/ty export const transformConstraints: SqlTransformer = (ctx, item) => { switch (item.type) { case 'ConstraintAdd': { - return asConstraintAdd(item.constraint); + return `ALTER TABLE "${item.constraint.tableName}" ADD ${asConstraintBody(item.constraint)};`; + } + + case 'ConstraintRename': { + return `ALTER TABLE "${item.tableName}" RENAME CONSTRAINT "${item.oldName}" TO "${item.newName}";`; } case 'ConstraintDrop': { - return asConstraintDrop(item.tableName, item.constraintName); + return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`; } default: { return false; @@ -52,11 +56,3 @@ export const asConstraintBody = (constraint: DatabaseConstraint): string => { } } }; - -export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => { - return `ALTER TABLE "${constraint.tableName}" ADD ${asConstraintBody(constraint)};`; -}; - -export const asConstraintDrop = (tableName: string, constraintName: string): string => { - return `ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}";`; -}; diff --git a/server/src/sql-tools/transformers/index.transformer.ts b/server/src/sql-tools/transformers/index.transformer.ts index bfdbf8e157..acd65140ee 100644 --- a/server/src/sql-tools/transformers/index.transformer.ts +++ b/server/src/sql-tools/transformers/index.transformer.ts @@ -8,8 +8,12 @@ export const transformIndexes: SqlTransformer = (ctx, item) => { return asIndexCreate(item.index); } + case 'IndexRename': { + return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`; + } + case 'IndexDrop': { - return asIndexDrop(item.indexName); + return `DROP INDEX "${item.indexName}";`; } default: { @@ -50,7 +54,3 @@ export const asIndexCreate = (index: DatabaseIndex): string => { return sql + ';'; }; - -export const asIndexDrop = (indexName: string): string => { - return `DROP INDEX "${indexName}";`; -}; diff --git a/server/src/sql-tools/transformers/table.transformer.spec.ts b/server/src/sql-tools/transformers/table.transformer.spec.ts index 662e1ba7fd..0d89fcd278 100644 --- a/server/src/sql-tools/transformers/table.transformer.spec.ts +++ b/server/src/sql-tools/transformers/table.transformer.spec.ts @@ -19,6 +19,7 @@ const table1: DatabaseTable = { }, { name: 'column2', + primary: false, tableName: 'table1', type: 'character varying', nullable: true, @@ -106,6 +107,7 @@ describe(transformTables.name, () => { columns: [ { tableName: 'table1', + primary: false, name: 'column1', type: 'character varying', isArray: false, @@ -137,6 +139,7 @@ describe(transformTables.name, () => { { tableName: 'table1', name: 'column1', + primary: false, type: 'character varying', isArray: false, nullable: true, @@ -167,6 +170,7 @@ describe(transformTables.name, () => { columns: [ { tableName: 'table1', + primary: false, name: 'column1', type: 'character varying', length: 2, @@ -198,6 +202,7 @@ describe(transformTables.name, () => { columns: [ { tableName: 'table1', + primary: false, name: 'column1', type: 'character varying', isArray: true, diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts index 617adc0a58..9529067040 100644 --- a/server/src/sql-tools/types.ts +++ b/server/src/sql-tools/types.ts @@ -1,12 +1,14 @@ import { Kysely, ColumnType as KyselyColumnType } from 'kysely'; import { ProcessorContext } from 'src/sql-tools/contexts/processor-context'; import { ReaderContext } from 'src/sql-tools/contexts/reader-context'; +import { NamingInterface } from 'src/sql-tools/naming/naming.interface'; import { RegisterItem } from 'src/sql-tools/register-item'; export type BaseContextOptions = { databaseName?: string; schemaName?: string; overrideTableName?: string; + namingStrategy?: 'default' | 'hash' | NamingInterface; }; export type SchemaFromCodeOptions = BaseContextOptions & { @@ -386,7 +388,7 @@ export type DatabaseConstraint = | DatabaseCheckConstraint; export type DatabaseColumn = { - primary?: boolean; + primary: boolean; name: string; tableName: string; comment?: string; @@ -487,11 +489,14 @@ export type SchemaDiff = { reason: string } & ( | { type: 'TableCreate'; table: DatabaseTable } | { type: 'TableDrop'; tableName: string } | { type: 'ColumnAdd'; column: DatabaseColumn } + | { type: 'ColumnRename'; tableName: string; oldName: string; newName: string } | { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges } | { type: 'ColumnDrop'; tableName: string; columnName: string } | { type: 'ConstraintAdd'; constraint: DatabaseConstraint } + | { type: 'ConstraintRename'; tableName: string; oldName: string; newName: string } | { type: 'ConstraintDrop'; tableName: string; constraintName: string } | { type: 'IndexCreate'; index: DatabaseIndex } + | { type: 'IndexRename'; tableName: string; oldName: string; newName: string } | { type: 'IndexDrop'; indexName: string } | { type: 'TriggerCreate'; trigger: DatabaseTrigger } | { type: 'TriggerDrop'; tableName: string; triggerName: string } @@ -509,11 +514,15 @@ export type Comparer = { onMissing: (source: T) => SchemaDiff[]; onExtra: (target: T) => SchemaDiff[]; onCompare: CompareFunction; + /** if two items have the same key, they are considered identical and can be renamed via `onRename` */ + getRenameKey?: (item: T) => string; + onRename?: (source: T, target: T) => SchemaDiff[]; }; export enum Reason { MissingInSource = 'missing in source', MissingInTarget = 'missing in target', + Rename = 'name has changed', } export type Timestamp = KyselyColumnType; diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts index c38c29cde0..288f7c6698 100644 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts @@ -83,7 +83,7 @@ export const schema: DatabaseSchema = { }, { type: ConstraintType.UNIQUE, - name: 'REL_3fcca5cc563abf256fc346e3ff', + name: 'UQ_3fcca5cc563abf256fc346e3ff4', tableName: 'table2', columnNames: ['parentId'], synchronize: true,