feat: naming strategy (#19848)

* feat: naming strategy

* feat: detect renames
This commit is contained in:
Jason Rasmussen 2025-07-11 11:35:10 -04:00 committed by GitHub
parent 1d19d308e2
commit 9e48ae3052
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 517 additions and 127 deletions

View File

@ -9,7 +9,13 @@ import { ConfigRepository } from 'src/repositories/config.repository';
import { DatabaseRepository } from 'src/repositories/database.repository'; import { DatabaseRepository } from 'src/repositories/database.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import 'src/schema'; 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'; import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database';
const main = async () => { const main = async () => {
@ -107,7 +113,22 @@ const compare = async () => {
const { database } = configRepository.getEnv(); const { database } = configRepository.getEnv();
const db = postgres(asPostgresConnectionConfig(database.config)); const db = postgres(asPostgresConnectionConfig(database.config));
const source = schemaFromCode({ overrides: true }); const tables = new Set<string>();
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, {}); const target = await schemaFromDatabase(db, {});
console.log(source.warnings.join('\n')); console.log(source.warnings.join('\n'));

View File

@ -35,7 +35,11 @@ export class StackTable {
updateId!: Generated<string>; updateId!: Generated<string>;
//TODO: Add constraint to ensure primary asset exists in the assets array //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; primaryAssetId!: string;
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })

View File

@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest';
const testColumn: DatabaseColumn = { const testColumn: DatabaseColumn = {
name: 'test', name: 'test',
tableName: 'table1', tableName: 'table1',
primary: false,
nullable: false, nullable: false,
isArray: false, isArray: false,
type: 'character varying', type: 'character varying',

View File

@ -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'; import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types';
export const compareColumns: Comparer<DatabaseColumn> = { 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) => [ onMissing: (source) => [
{ {
type: 'ColumnAdd', type: 'ColumnAdd',
@ -67,7 +92,7 @@ export const compareColumns: Comparer<DatabaseColumn> = {
return items; return items;
}, },
}; } satisfies Comparer<DatabaseColumn>;
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
return [ return [

View File

@ -1,4 +1,4 @@
import { haveEqualColumns } from 'src/sql-tools/helpers'; import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers';
import { import {
CompareFunction, CompareFunction,
Comparer, Comparer,
@ -13,6 +13,37 @@ import {
} from 'src/sql-tools/types'; } from 'src/sql-tools/types';
export const compareConstraints: Comparer<DatabaseConstraint> = { export const compareConstraints: Comparer<DatabaseConstraint> = {
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) => [ onMissing: (source) => [
{ {
type: 'ConstraintAdd', type: 'ConstraintAdd',

View File

@ -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'; import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types';
export const compareIndexes: Comparer<DatabaseIndex> = { export const compareIndexes: Comparer<DatabaseIndex> = {
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) => [ onMissing: (source) => [
{ {
type: 'IndexCreate', type: 'IndexCreate',

View File

@ -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 { import {
BaseContextOptions, BaseContextOptions,
DatabaseEnum, DatabaseEnum,
@ -11,6 +14,26 @@ import {
const asOverrideKey = (type: string, name: string) => `${type}:${name}`; 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 { export class BaseContext {
databaseName: string; databaseName: string;
schemaName: string; schemaName: string;
@ -24,10 +47,17 @@ export class BaseContext {
overrides: DatabaseOverride[] = []; overrides: DatabaseOverride[] = [];
warnings: string[] = []; warnings: string[] = [];
private namingStrategy: NamingInterface;
constructor(options: BaseContextOptions) { constructor(options: BaseContextOptions) {
this.databaseName = options.databaseName ?? 'postgres'; this.databaseName = options.databaseName ?? 'postgres';
this.schemaName = options.schemaName ?? 'public'; this.schemaName = options.schemaName ?? 'public';
this.overrideTableName = options.overrideTableName ?? 'migration_overrides'; this.overrideTableName = options.overrideTableName ?? 'migration_overrides';
this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash');
}
getNameFor(item: NamingItem) {
return this.namingStrategy.getName(item);
} }
getTableByName(name: string) { getTableByName(name: string) {

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */ /* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { BaseContext } from 'src/sql-tools/contexts/base-context'; import { BaseContext } from 'src/sql-tools/contexts/base-context';
import { ColumnOptions, TableOptions } from 'src/sql-tools/decorators'; import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
import { asKey } from 'src/sql-tools/helpers'; import { TableOptions } from 'src/sql-tools/decorators/table.decorator';
import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types'; import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types';
type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map<string | symbol, DatabaseColumn> }; type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map<string | symbol, DatabaseColumn> };
@ -59,19 +59,6 @@ export class ProcessorContext extends BaseContext {
tableMetadata.methodToColumn.set(propertyName, column); 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) { warnMissingTable(context: string, object: object, propertyName?: symbol | string) {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
this.warn(context, `Unable to find table (${label})`); this.warn(context, `Unable to find table (${label})`);

View File

@ -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';

View File

@ -2,13 +2,6 @@ import { createHash } from 'node:crypto';
import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; import { ColumnValue } from 'src/sql-tools/decorators/column.decorator';
import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types'; 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 = <T extends { name?: string }>(options: string | T): T => { export const asOptions = <T extends { name?: string }>(options: string | T): T => {
if (typeof options === 'string') { if (typeof options === 'string') {
return { name: options } as T; return { name: options } as T;
@ -79,6 +72,10 @@ export const compare = <T extends { name: string; synchronize: boolean }>(
const items: SchemaDiff[] = []; const items: SchemaDiff[] = [];
const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]);
const missingKeys = new Set<string>();
const extraKeys = new Set<string>();
// common keys
for (const key of keys) { for (const key of keys) {
const source = sourceMap[key]; const source = sourceMap[key];
const target = targetMap[key]; const target = targetMap[key];
@ -92,22 +89,63 @@ export const compare = <T extends { name: string; synchronize: boolean }>(
} }
if (source && !target) { if (source && !target) {
items.push(...comparer.onMissing(source)); missingKeys.add(key);
} else if (!source && target) { continue;
items.push(...comparer.onExtra(target)); }
} else {
if ( if (!source && target) {
haveEqualOverrides( extraKeys.add(key);
source as unknown as { override?: DatabaseOverride }, continue;
target as unknown as { override?: DatabaseOverride }, }
)
) { 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<string, string> = {};
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; 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; 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 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 => { export const asJsonString = (value: unknown): string => {
return `'${escape(JSON.stringify(value))}'::jsonb`; return `'${escape(JSON.stringify(value))}'::jsonb`;
}; };
@ -202,3 +238,6 @@ const escape = (value: string) => {
.replaceAll(/[\r]/g, String.raw`\r`) .replaceAll(/[\r]/g, String.raw`\r`)
.replaceAll(/[\t]/g, String.raw`\t`); .replaceAll(/[\t]/g, String.raw`\t`);
}; };
export const asRenameKey = (values: Array<string | boolean | number | undefined>) =>
values.map((value) => value ?? '').join('|');

View File

@ -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}`;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -1,4 +1,3 @@
import { asKey } from 'src/sql-tools/helpers';
import { ConstraintType, Processor } from 'src/sql-tools/types'; import { ConstraintType, Processor } from 'src/sql-tools/types';
export const processCheckConstraints: Processor = (ctx, items) => { export const processCheckConstraints: Processor = (ctx, items) => {
@ -15,12 +14,10 @@ export const processCheckConstraints: Processor = (ctx, items) => {
table.constraints.push({ table.constraints.push({
type: ConstraintType.CHECK, type: ConstraintType.CHECK,
name: options.name || asCheckConstraintName(tableName, options.expression), name: options.name || ctx.getNameFor({ type: 'check', tableName, expression: options.expression }),
tableName, tableName,
expression: options.expression, expression: options.expression,
synchronize: options.synchronize ?? true, synchronize: options.synchronize ?? true,
}); });
} }
}; };
const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]);

View File

@ -13,7 +13,7 @@ export const processColumns: Processor = (ctx, items) => {
continue; 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); const existingColumn = table.columns.find((column) => column.name === columnName);
if (existingColumn) { if (existingColumn) {
// TODO log warnings if column name is not unique // TODO log warnings if column name is not unique

View File

@ -1,10 +1,9 @@
import { asSnakeCase } from 'src/sql-tools/helpers';
import { Processor } from 'src/sql-tools/types'; import { Processor } from 'src/sql-tools/types';
export const processDatabases: Processor = (ctx, items) => { export const processDatabases: Processor = (ctx, items) => {
for (const { for (const {
item: { object, options }, item: { object, options },
} of items.filter((item) => item.type === 'database')) { } 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 });
} }
}; };

View File

@ -1,4 +1,3 @@
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
export const processForeignKeyColumns: Processor = (ctx, items) => { export const processForeignKeyColumns: Processor = (ctx, items) => {
@ -31,15 +30,24 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
column.type = referenceColumns[0].type; column.type = referenceColumns[0].type;
} }
const referenceTableName = referenceTable.name;
const referenceColumnNames = referenceColumns.map((column) => column.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({ table.constraints.push({
name, name,
tableName: table.name, tableName: table.name,
columnNames, columnNames,
type: ConstraintType.FOREIGN_KEY, type: ConstraintType.FOREIGN_KEY,
referenceTableName: referenceTable.name, referenceTableName,
referenceColumnNames, referenceColumnNames,
onUpdate: options.onUpdate as ActionType, onUpdate: options.onUpdate as ActionType,
onDelete: options.onDelete as ActionType, onDelete: options.onDelete as ActionType,
@ -48,7 +56,7 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
if (options.unique || options.uniqueConstraintName) { if (options.unique || options.uniqueConstraintName) {
table.constraints.push({ table.constraints.push({
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames), name: options.uniqueConstraintName || ctx.getNameFor({ type: 'unique', tableName: table.name, columnNames }),
tableName: table.name, tableName: table.name,
columnNames, columnNames,
type: ConstraintType.UNIQUE, type: ConstraintType.UNIQUE,
@ -57,5 +65,3 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
} }
} }
}; };
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);

View File

@ -1,4 +1,3 @@
import { asForeignKeyConstraintName } from 'src/sql-tools/helpers';
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
export const processForeignKeyConstraints: Processor = (ctx, items) => { export const processForeignKeyConstraints: Processor = (ctx, items) => {
@ -46,18 +45,27 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => {
continue; continue;
} }
const referenceColumns = const referenceTableName = referenceTable.name;
const referenceColumnNames =
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name); 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({ table.constraints.push({
type: ConstraintType.FOREIGN_KEY, type: ConstraintType.FOREIGN_KEY,
name, name,
tableName: table.name, tableName: table.name,
columnNames: options.columns, columnNames: options.columns,
referenceTableName: referenceTable.name, referenceTableName,
referenceColumnNames: referenceColumns, referenceColumnNames,
onUpdate: options.onUpdate as ActionType, onUpdate: options.onUpdate as ActionType,
onDelete: options.onDelete as ActionType, onDelete: options.onDelete as ActionType,
synchronize: options.synchronize ?? true, synchronize: options.synchronize ?? true,
@ -68,8 +76,15 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => {
} }
if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) { 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({ table.indexes.push({
name: options.indexName || ctx.asIndexName(table.name, options.columns), name: indexName,
tableName: table.name, tableName: table.name,
columnNames: options.columns, columnNames: options.columns,
unique: false, unique: false,

View File

@ -10,8 +10,17 @@ export const processIndexes: Processor = (ctx, items) => {
continue; continue;
} }
const indexName =
options.name ||
ctx.getNameFor({
type: 'index',
tableName: table.name,
columnNames: options.columns,
where: options.where,
});
table.indexes.push({ table.indexes.push({
name: options.name || ctx.asIndexName(table.name, options.columns, options.where), name: indexName,
tableName: table.name, tableName: table.name,
unique: options.unique ?? false, unique: options.unique ?? false,
expression: options.expression, expression: options.expression,
@ -50,7 +59,13 @@ export const processIndexes: Processor = (ctx, items) => {
continue; 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); const isIndexPresent = table.indexes.some((index) => index.name === indexName);
if (isIndexPresent) { if (isIndexPresent) {

View File

@ -1,4 +1,3 @@
import { asKey } from 'src/sql-tools/helpers';
import { ConstraintType, Processor } from 'src/sql-tools/types'; import { ConstraintType, Processor } from 'src/sql-tools/types';
export const processPrimaryKeyConstraints: Processor = (ctx) => { export const processPrimaryKeyConstraints: Processor = (ctx) => {
@ -15,7 +14,13 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => {
const tableMetadata = ctx.getTableMetadata(table); const tableMetadata = ctx.getTableMetadata(table);
table.constraints.push({ table.constraints.push({
type: ConstraintType.PRIMARY_KEY, 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, tableName: table.name,
columnNames, columnNames,
synchronize: tableMetadata.options.synchronize ?? true, synchronize: tableMetadata.options.synchronize ?? true,
@ -23,5 +28,3 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => {
} }
} }
}; };
const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns);

View File

@ -1,4 +1,3 @@
import { asSnakeCase } from 'src/sql-tools/helpers';
import { Processor } from 'src/sql-tools/types'; import { Processor } from 'src/sql-tools/types';
export const processTables: Processor = (ctx, items) => { export const processTables: Processor = (ctx, items) => {
@ -14,7 +13,7 @@ export const processTables: Processor = (ctx, items) => {
ctx.addTable( ctx.addTable(
{ {
name: options.name || asSnakeCase(object.name), name: options.name || ctx.getNameFor({ type: 'table', name: object.name }),
columns: [], columns: [],
constraints: [], constraints: [],
indexes: [], indexes: [],

View File

@ -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'; import { Processor } from 'src/sql-tools/types';
export const processTriggers: Processor = (ctx, items) => { export const processTriggers: Processor = (ctx, items) => {
@ -12,8 +10,19 @@ export const processTriggers: Processor = (ctx, items) => {
continue; 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({ table.triggers.push({
name: options.name || asTriggerName(table.name, options), name: triggerName,
tableName: table.name, tableName: table.name,
timing: options.timing, timing: options.timing,
actions: options.actions, 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]);

View File

@ -1,4 +1,3 @@
import { asKey } from 'src/sql-tools/helpers';
import { ConstraintType, Processor } from 'src/sql-tools/types'; import { ConstraintType, Processor } from 'src/sql-tools/types';
export const processUniqueConstraints: Processor = (ctx, items) => { export const processUniqueConstraints: Processor = (ctx, items) => {
@ -16,7 +15,7 @@ export const processUniqueConstraints: Processor = (ctx, items) => {
table.constraints.push({ table.constraints.push({
type: ConstraintType.UNIQUE, type: ConstraintType.UNIQUE,
name: options.name || asUniqueConstraintName(tableName, columnNames), name: options.name || ctx.getNameFor({ type: 'unique', tableName, columnNames }),
tableName, tableName,
columnNames, columnNames,
synchronize: options.synchronize ?? true, synchronize: options.synchronize ?? true,
@ -41,9 +40,17 @@ export const processUniqueConstraints: Processor = (ctx, items) => {
} }
if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { 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({ table.constraints.push({
type: ConstraintType.UNIQUE, type: ConstraintType.UNIQUE,
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), name: uniqueConstraintName,
tableName: table.name, tableName: table.name,
columnNames: [column.name], columnNames: [column.name],
synchronize: options.synchronize ?? true, synchronize: options.synchronize ?? true,
@ -51,5 +58,3 @@ export const processUniqueConstraints: Processor = (ctx, items) => {
} }
} }
}; };
const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns);

View File

@ -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-enum';
export * from 'src/sql-tools/register-function'; export * from 'src/sql-tools/register-function';
export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff'; export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff';

View File

@ -76,6 +76,8 @@ export const readColumns: Reader = async (ctx, db) => {
const item: DatabaseColumn = { const item: DatabaseColumn = {
type: column.data_type as ColumnType, type: column.data_type as ColumnType,
// TODO infer this from PK constraints
primary: false,
name: columnName, name: columnName,
tableName: column.table_name, tableName: column.table_name,
nullable: column.is_nullable === 'YES', nullable: column.is_nullable === 'YES',

View File

@ -28,6 +28,7 @@ const fromColumn = (column: Partial<Omit<DatabaseColumn, 'tableName'>>): Databas
columns: [ columns: [
{ {
name: 'column1', name: 'column1',
primary: false,
synchronize: true, synchronize: true,
isArray: false, isArray: false,
type: 'character varying', type: 'character varying',
@ -63,6 +64,7 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => {
columns: [ columns: [
{ {
name: 'column1', name: 'column1',
primary: false,
synchronize: true, synchronize: true,
isArray: false, isArray: false,
type: 'character varying', type: 'character varying',
@ -97,6 +99,7 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => {
columns: [ columns: [
{ {
name: 'column1', name: 'column1',
primary: false,
synchronize: true, synchronize: true,
isArray: false, isArray: false,
type: 'character varying', type: 'character varying',
@ -140,6 +143,7 @@ const newSchema = (schema: {
columns.push({ columns.push({
tableName, tableName,
name: columnName, name: columnName,
primary: false,
type: column.type || 'character varying', type: column.type || 'character varying',
isArray: column.isArray ?? false, isArray: column.isArray ?? false,
nullable: column.nullable ?? false, nullable: column.nullable ?? false,
@ -182,6 +186,7 @@ describe(schemaDiff.name, () => {
const column: DatabaseColumn = { const column: DatabaseColumn = {
type: 'character varying', type: 'character varying',
tableName: 'table1', tableName: 'table1',
primary: false,
name: 'column1', name: 'column1',
isArray: false, isArray: false,
nullable: false, nullable: false,
@ -264,6 +269,7 @@ describe(schemaDiff.name, () => {
column: { column: {
tableName: 'table1', tableName: 'table1',
isArray: false, isArray: false,
primary: false,
name: 'column2', name: 'column2',
nullable: false, nullable: false,
type: 'character varying', type: 'character varying',

View File

@ -40,10 +40,13 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio
TableDrop: [], TableDrop: [],
ColumnAdd: [], ColumnAdd: [],
ColumnAlter: [], ColumnAlter: [],
ColumnRename: [],
ColumnDrop: [], ColumnDrop: [],
ConstraintAdd: [], ConstraintAdd: [],
ConstraintDrop: [], ConstraintDrop: [],
ConstraintRename: [],
IndexCreate: [], IndexCreate: [],
IndexRename: [],
IndexDrop: [], IndexDrop: [],
TriggerCreate: [], TriggerCreate: [],
TriggerDrop: [], TriggerDrop: [],
@ -72,11 +75,14 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio
...itemMap.TableCreate, ...itemMap.TableCreate,
...itemMap.ColumnAlter, ...itemMap.ColumnAlter,
...itemMap.ColumnAdd, ...itemMap.ColumnAdd,
...itemMap.ColumnRename,
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY),
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY),
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE),
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK), ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK),
...itemMap.ConstraintRename,
...itemMap.IndexCreate, ...itemMap.IndexCreate,
...itemMap.IndexRename,
...itemMap.TriggerCreate, ...itemMap.TriggerCreate,
...itemMap.ColumnDrop, ...itemMap.ColumnDrop,
...itemMap.TableDrop, ...itemMap.TableDrop,

View File

@ -20,9 +20,9 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
name: ctx.overrideTableName, name: ctx.overrideTableName,
columns: [ columns: [
{ {
primary: true,
name: 'name', name: 'name',
tableName: ctx.overrideTableName, tableName: ctx.overrideTableName,
primary: true,
type: 'character varying', type: 'character varying',
nullable: false, nullable: false,
isArray: false, isArray: false,
@ -31,6 +31,7 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
{ {
name: 'value', name: 'value',
tableName: ctx.overrideTableName, tableName: ctx.overrideTableName,
primary: false,
type: 'jsonb', type: 'jsonb',
nullable: false, nullable: false,
isArray: false, isArray: false,

View File

@ -13,6 +13,7 @@ describe(transformColumns.name, () => {
column: { column: {
name: 'column1', name: 'column1',
tableName: 'table1', tableName: 'table1',
primary: false,
type: 'character varying', type: 'character varying',
nullable: false, nullable: false,
isArray: false, isArray: false,
@ -30,6 +31,7 @@ describe(transformColumns.name, () => {
column: { column: {
name: 'column1', name: 'column1',
tableName: 'table1', tableName: 'table1',
primary: false,
type: 'character varying', type: 'character varying',
nullable: true, nullable: true,
isArray: false, isArray: false,
@ -47,6 +49,7 @@ describe(transformColumns.name, () => {
column: { column: {
name: 'column1', name: 'column1',
tableName: 'table1', tableName: 'table1',
primary: false,
type: 'character varying', type: 'character varying',
enumName: 'table1_column1_enum', enumName: 'table1_column1_enum',
nullable: true, nullable: true,
@ -65,6 +68,7 @@ describe(transformColumns.name, () => {
column: { column: {
name: 'column1', name: 'column1',
tableName: 'table1', tableName: 'table1',
primary: false,
type: 'boolean', type: 'boolean',
nullable: true, nullable: true,
isArray: true, isArray: true,

View File

@ -12,8 +12,12 @@ export const transformColumns: SqlTransformer = (ctx, item) => {
return asColumnAlter(item.tableName, item.columnName, item.changes); return asColumnAlter(item.tableName, item.columnName, item.changes);
} }
case 'ColumnRename': {
return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`;
}
case 'ColumnDrop': { case 'ColumnDrop': {
return asColumnDrop(item.tableName, item.columnName); return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`;
} }
default: { 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[] => { export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => {
const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`; const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`;
const items: string[] = []; const items: string[] = [];

View File

@ -5,11 +5,15 @@ import { ActionType, ConstraintType, DatabaseConstraint } from 'src/sql-tools/ty
export const transformConstraints: SqlTransformer = (ctx, item) => { export const transformConstraints: SqlTransformer = (ctx, item) => {
switch (item.type) { switch (item.type) {
case 'ConstraintAdd': { 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': { case 'ConstraintDrop': {
return asConstraintDrop(item.tableName, item.constraintName); return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`;
} }
default: { default: {
return false; 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}";`;
};

View File

@ -8,8 +8,12 @@ export const transformIndexes: SqlTransformer = (ctx, item) => {
return asIndexCreate(item.index); return asIndexCreate(item.index);
} }
case 'IndexRename': {
return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`;
}
case 'IndexDrop': { case 'IndexDrop': {
return asIndexDrop(item.indexName); return `DROP INDEX "${item.indexName}";`;
} }
default: { default: {
@ -50,7 +54,3 @@ export const asIndexCreate = (index: DatabaseIndex): string => {
return sql + ';'; return sql + ';';
}; };
export const asIndexDrop = (indexName: string): string => {
return `DROP INDEX "${indexName}";`;
};

View File

@ -19,6 +19,7 @@ const table1: DatabaseTable = {
}, },
{ {
name: 'column2', name: 'column2',
primary: false,
tableName: 'table1', tableName: 'table1',
type: 'character varying', type: 'character varying',
nullable: true, nullable: true,
@ -106,6 +107,7 @@ describe(transformTables.name, () => {
columns: [ columns: [
{ {
tableName: 'table1', tableName: 'table1',
primary: false,
name: 'column1', name: 'column1',
type: 'character varying', type: 'character varying',
isArray: false, isArray: false,
@ -137,6 +139,7 @@ describe(transformTables.name, () => {
{ {
tableName: 'table1', tableName: 'table1',
name: 'column1', name: 'column1',
primary: false,
type: 'character varying', type: 'character varying',
isArray: false, isArray: false,
nullable: true, nullable: true,
@ -167,6 +170,7 @@ describe(transformTables.name, () => {
columns: [ columns: [
{ {
tableName: 'table1', tableName: 'table1',
primary: false,
name: 'column1', name: 'column1',
type: 'character varying', type: 'character varying',
length: 2, length: 2,
@ -198,6 +202,7 @@ describe(transformTables.name, () => {
columns: [ columns: [
{ {
tableName: 'table1', tableName: 'table1',
primary: false,
name: 'column1', name: 'column1',
type: 'character varying', type: 'character varying',
isArray: true, isArray: true,

View File

@ -1,12 +1,14 @@
import { Kysely, ColumnType as KyselyColumnType } from 'kysely'; import { Kysely, ColumnType as KyselyColumnType } from 'kysely';
import { ProcessorContext } from 'src/sql-tools/contexts/processor-context'; import { ProcessorContext } from 'src/sql-tools/contexts/processor-context';
import { ReaderContext } from 'src/sql-tools/contexts/reader-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'; import { RegisterItem } from 'src/sql-tools/register-item';
export type BaseContextOptions = { export type BaseContextOptions = {
databaseName?: string; databaseName?: string;
schemaName?: string; schemaName?: string;
overrideTableName?: string; overrideTableName?: string;
namingStrategy?: 'default' | 'hash' | NamingInterface;
}; };
export type SchemaFromCodeOptions = BaseContextOptions & { export type SchemaFromCodeOptions = BaseContextOptions & {
@ -386,7 +388,7 @@ export type DatabaseConstraint =
| DatabaseCheckConstraint; | DatabaseCheckConstraint;
export type DatabaseColumn = { export type DatabaseColumn = {
primary?: boolean; primary: boolean;
name: string; name: string;
tableName: string; tableName: string;
comment?: string; comment?: string;
@ -487,11 +489,14 @@ export type SchemaDiff = { reason: string } & (
| { type: 'TableCreate'; table: DatabaseTable } | { type: 'TableCreate'; table: DatabaseTable }
| { type: 'TableDrop'; tableName: string } | { type: 'TableDrop'; tableName: string }
| { type: 'ColumnAdd'; column: DatabaseColumn } | { type: 'ColumnAdd'; column: DatabaseColumn }
| { type: 'ColumnRename'; tableName: string; oldName: string; newName: string }
| { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges } | { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges }
| { type: 'ColumnDrop'; tableName: string; columnName: string } | { type: 'ColumnDrop'; tableName: string; columnName: string }
| { type: 'ConstraintAdd'; constraint: DatabaseConstraint } | { type: 'ConstraintAdd'; constraint: DatabaseConstraint }
| { type: 'ConstraintRename'; tableName: string; oldName: string; newName: string }
| { type: 'ConstraintDrop'; tableName: string; constraintName: string } | { type: 'ConstraintDrop'; tableName: string; constraintName: string }
| { type: 'IndexCreate'; index: DatabaseIndex } | { type: 'IndexCreate'; index: DatabaseIndex }
| { type: 'IndexRename'; tableName: string; oldName: string; newName: string }
| { type: 'IndexDrop'; indexName: string } | { type: 'IndexDrop'; indexName: string }
| { type: 'TriggerCreate'; trigger: DatabaseTrigger } | { type: 'TriggerCreate'; trigger: DatabaseTrigger }
| { type: 'TriggerDrop'; tableName: string; triggerName: string } | { type: 'TriggerDrop'; tableName: string; triggerName: string }
@ -509,11 +514,15 @@ export type Comparer<T> = {
onMissing: (source: T) => SchemaDiff[]; onMissing: (source: T) => SchemaDiff[];
onExtra: (target: T) => SchemaDiff[]; onExtra: (target: T) => SchemaDiff[];
onCompare: CompareFunction<T>; onCompare: CompareFunction<T>;
/** 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 { export enum Reason {
MissingInSource = 'missing in source', MissingInSource = 'missing in source',
MissingInTarget = 'missing in target', MissingInTarget = 'missing in target',
Rename = 'name has changed',
} }
export type Timestamp = KyselyColumnType<Date, Date | string, Date | string>; export type Timestamp = KyselyColumnType<Date, Date | string, Date | string>;

View File

@ -83,7 +83,7 @@ export const schema: DatabaseSchema = {
}, },
{ {
type: ConstraintType.UNIQUE, type: ConstraintType.UNIQUE,
name: 'REL_3fcca5cc563abf256fc346e3ff', name: 'UQ_3fcca5cc563abf256fc346e3ff4',
tableName: 'table2', tableName: 'table2',
columnNames: ['parentId'], columnNames: ['parentId'],
synchronize: true, synchronize: true,