refactor: sql-tools (#19717)

This commit is contained in:
Jason Rasmussen
2025-07-03 10:59:17 -04:00
committed by GitHub
parent 484529e61e
commit 6044663e26
160 changed files with 1120 additions and 1186 deletions

View File

@@ -0,0 +1,26 @@
import { asKey } from 'src/sql-tools/helpers';
import { ConstraintType, Processor } from 'src/sql-tools/types';
export const processCheckConstraints: Processor = (builder, items) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'checkConstraint')) {
const table = builder.getTableByObject(object);
if (!table) {
builder.warnMissingTable('@Check', object);
continue;
}
const tableName = table.name;
table.constraints.push({
type: ConstraintType.CHECK,
name: options.name || asCheckConstraintName(tableName, options.expression),
tableName,
expression: options.expression,
synchronize: options.synchronize ?? true,
});
}
};
const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]);

View File

@@ -0,0 +1,55 @@
import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
import { fromColumnValue } from 'src/sql-tools/helpers';
import { Processor } from 'src/sql-tools/types';
export const processColumns: Processor = (builder, items) => {
for (const {
type,
item: { object, propertyName, options },
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
const table = builder.getTableByObject(object.constructor);
if (!table) {
builder.warnMissingTable(type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName);
continue;
}
const columnName = options.name ?? String(propertyName);
const existingColumn = table.columns.find((column) => column.name === columnName);
if (existingColumn) {
// TODO log warnings if column name is not unique
continue;
}
let defaultValue = fromColumnValue(options.default);
let nullable = options.nullable ?? false;
// map `{ default: null }` to `{ nullable: true }`
if (defaultValue === null) {
nullable = true;
defaultValue = undefined;
}
const isEnum = !!(options as ColumnOptions).enum;
builder.addColumn(
table,
{
name: columnName,
tableName: table.name,
primary: options.primary ?? false,
default: defaultValue,
nullable,
isArray: (options as ColumnOptions).array ?? false,
length: options.length,
type: isEnum ? 'enum' : options.type || 'character varying',
enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined,
comment: options.comment,
storage: options.storage,
identity: options.identity,
synchronize: options.synchronize ?? true,
},
options,
propertyName,
);
}
};

View File

@@ -0,0 +1,16 @@
import { fromColumnValue } from 'src/sql-tools/helpers';
import { Processor } from 'src/sql-tools/types';
export const processConfigurationParameters: Processor = (builder, items) => {
for (const {
item: { options },
} of items.filter((item) => item.type === 'configurationParameter')) {
builder.parameters.push({
databaseName: builder.databaseName,
name: options.name,
value: fromColumnValue(options.value),
scope: options.scope,
synchronize: options.synchronize ?? true,
});
}
};

View File

@@ -0,0 +1,10 @@
import { asSnakeCase } from 'src/sql-tools/helpers';
import { Processor } from 'src/sql-tools/types';
export const processDatabases: Processor = (builder, items) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'database')) {
builder.databaseName = options.name || asSnakeCase(object.name);
}
};

View File

@@ -0,0 +1,8 @@
import { Processor } from 'src/sql-tools/types';
export const processEnums: Processor = (builder, items) => {
for (const { item } of items.filter((item) => item.type === 'enum')) {
// TODO log warnings if enum name is not unique
builder.enums.push(item);
}
};

View File

@@ -0,0 +1,12 @@
import { Processor } from 'src/sql-tools/types';
export const processExtensions: Processor = (builder, items) => {
for (const {
item: { options },
} of items.filter((item) => item.type === 'extension')) {
builder.extensions.push({
name: options.name,
synchronize: options.synchronize ?? true,
});
}
};

View File

@@ -0,0 +1,61 @@
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
export const processForeignKeyColumns: Processor = (builder, items) => {
for (const {
item: { object, propertyName, options, target },
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName);
if (!table) {
builder.warnMissingTable('@ForeignKeyColumn', object);
continue;
}
if (!column) {
// should be impossible since they are pre-created in `column.processor.ts`
builder.warnMissingColumn('@ForeignKeyColumn', object, propertyName);
continue;
}
const referenceTable = builder.getTableByObject(target());
if (!referenceTable) {
builder.warnMissingTable('@ForeignKeyColumn', object, propertyName);
continue;
}
const columnNames = [column.name];
const referenceColumns = referenceTable.columns.filter((column) => column.primary);
// infer FK column type from reference table
if (referenceColumns.length === 1) {
column.type = referenceColumns[0].type;
}
const referenceColumnNames = referenceColumns.map((column) => column.name);
const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames);
table.constraints.push({
name,
tableName: table.name,
columnNames,
type: ConstraintType.FOREIGN_KEY,
referenceTableName: referenceTable.name,
referenceColumnNames,
onUpdate: options.onUpdate as ActionType,
onDelete: options.onDelete as ActionType,
synchronize: options.synchronize ?? true,
});
if (options.unique || options.uniqueConstraintName) {
table.constraints.push({
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames),
tableName: table.name,
columnNames,
type: ConstraintType.UNIQUE,
synchronize: options.synchronize ?? true,
});
}
}
};
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);

View File

@@ -0,0 +1,80 @@
import { asForeignKeyConstraintName } from 'src/sql-tools/helpers';
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
export const processForeignKeyConstraints: Processor = (builder, items, config) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'foreignKeyConstraint')) {
const table = builder.getTableByObject(object);
if (!table) {
builder.warnMissingTable('@ForeignKeyConstraint', { name: 'referenceTable' });
continue;
}
const referenceTable = builder.getTableByObject(options.referenceTable());
if (!referenceTable) {
const referenceTableName = options.referenceTable()?.name;
builder.warn(
'@ForeignKeyConstraint.referenceTable',
`Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''),
);
continue;
}
let missingColumn = false;
for (const columnName of options.columns) {
if (!table.columns.some(({ name }) => name === columnName)) {
const metadata = builder.getTableMetadata(table);
builder.warn('@ForeignKeyConstraint.columns', `Unable to find column (${metadata.object.name}.${columnName})`);
missingColumn = true;
}
}
for (const columnName of options.referenceColumns || []) {
if (!referenceTable.columns.some(({ name }) => name === columnName)) {
const metadata = builder.getTableMetadata(referenceTable);
builder.warn(
'@ForeignKeyConstraint.referenceColumns',
`Unable to find column (${metadata.object.name}.${columnName})`,
);
missingColumn = true;
}
}
if (missingColumn) {
continue;
}
const referenceColumns =
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
const name = options.name || asForeignKeyConstraintName(table.name, options.columns);
table.constraints.push({
type: ConstraintType.FOREIGN_KEY,
name,
tableName: table.name,
columnNames: options.columns,
referenceTableName: referenceTable.name,
referenceColumnNames: referenceColumns,
onUpdate: options.onUpdate as ActionType,
onDelete: options.onDelete as ActionType,
synchronize: options.synchronize ?? true,
});
if (options.index === false) {
continue;
}
if (options.index || options.indexName || config.createForeignKeyIndexes) {
table.indexes.push({
name: options.indexName || builder.asIndexName(table.name, options.columns),
tableName: table.name,
columnNames: options.columns,
unique: false,
synchronize: options.synchronize ?? true,
});
}
}
};

View File

@@ -0,0 +1,8 @@
import { Processor } from 'src/sql-tools/types';
export const processFunctions: Processor = (builder, items) => {
for (const { item } of items.filter((item) => item.type === 'function')) {
// TODO log warnings if function name is not unique
builder.functions.push(item);
}
};

View File

@@ -0,0 +1,74 @@
import { Processor } from 'src/sql-tools/types';
export const processIndexes: Processor = (builder, items, config) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'index')) {
const table = builder.getTableByObject(object);
if (!table) {
builder.warnMissingTable('@Check', object);
continue;
}
table.indexes.push({
name: options.name || builder.asIndexName(table.name, options.columns, options.where),
tableName: table.name,
unique: options.unique ?? false,
expression: options.expression,
using: options.using,
with: options.with,
where: options.where,
columnNames: options.columns,
synchronize: options.synchronize ?? true,
});
}
// column indexes
for (const {
type,
item: { object, propertyName, options },
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName);
if (!table) {
builder.warnMissingTable('@Column', object);
continue;
}
if (!column) {
// should be impossible since they are created in `column.processor.ts`
builder.warnMissingColumn('@Column', object, propertyName);
continue;
}
if (options.index === false) {
continue;
}
const isIndexRequested =
options.indexName || options.index || (type === 'foreignKeyColumn' && config.createForeignKeyIndexes);
if (!isIndexRequested) {
continue;
}
const indexName = options.indexName || builder.asIndexName(table.name, [column.name]);
const isIndexPresent = table.indexes.some((index) => index.name === indexName);
if (isIndexPresent) {
continue;
}
const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary }) => primary === true).length === 1;
if (isOnlyPrimaryColumn) {
// will have an index created by the primary key constraint
continue;
}
table.indexes.push({
name: indexName,
tableName: table.name,
unique: false,
columnNames: [column.name],
synchronize: options.synchronize ?? true,
});
}
};

View File

@@ -0,0 +1,32 @@
import { processCheckConstraints } from 'src/sql-tools/processors/check-constraint.processor';
import { processColumns } from 'src/sql-tools/processors/column.processor';
import { processConfigurationParameters } from 'src/sql-tools/processors/configuration-parameter.processor';
import { processDatabases } from 'src/sql-tools/processors/database.processor';
import { processEnums } from 'src/sql-tools/processors/enum.processor';
import { processExtensions } from 'src/sql-tools/processors/extension.processor';
import { processForeignKeyColumns } from 'src/sql-tools/processors/foreign-key-column.processor';
import { processForeignKeyConstraints } from 'src/sql-tools/processors/foreign-key-constraint.processor';
import { processFunctions } from 'src/sql-tools/processors/function.processor';
import { processIndexes } from 'src/sql-tools/processors/index.processor';
import { processPrimaryKeyConstraints } from 'src/sql-tools/processors/primary-key-contraint.processor';
import { processTables } from 'src/sql-tools/processors/table.processor';
import { processTriggers } from 'src/sql-tools/processors/trigger.processor';
import { processUniqueConstraints } from 'src/sql-tools/processors/unique-constraint.processor';
import { Processor } from 'src/sql-tools/types';
export const processors: Processor[] = [
processDatabases,
processConfigurationParameters,
processEnums,
processExtensions,
processFunctions,
processTables,
processColumns,
processForeignKeyColumns,
processForeignKeyConstraints,
processUniqueConstraints,
processCheckConstraints,
processPrimaryKeyConstraints,
processIndexes,
processTriggers,
];

View File

@@ -0,0 +1,27 @@
import { asKey } from 'src/sql-tools/helpers';
import { ConstraintType, Processor } from 'src/sql-tools/types';
export const processPrimaryKeyConstraints: Processor = (builder) => {
for (const table of builder.tables) {
const columnNames: string[] = [];
for (const column of table.columns) {
if (column.primary) {
columnNames.push(column.name);
}
}
if (columnNames.length > 0) {
const tableMetadata = builder.getTableMetadata(table);
table.constraints.push({
type: ConstraintType.PRIMARY_KEY,
name: tableMetadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames),
tableName: table.name,
columnNames,
synchronize: tableMetadata.options.synchronize ?? true,
});
}
}
};
const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns);

View File

@@ -0,0 +1,28 @@
import { asSnakeCase } from 'src/sql-tools/helpers';
import { Processor } from 'src/sql-tools/types';
export const processTables: Processor = (builder, items) => {
for (const {
item: { options, object },
} of items.filter((item) => item.type === 'table')) {
const test = builder.getTableByObject(object);
if (test) {
throw new Error(
`Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`,
);
}
builder.addTable(
{
name: options.name || asSnakeCase(object.name),
columns: [],
constraints: [],
indexes: [],
triggers: [],
synchronize: options.synchronize ?? true,
},
options,
object,
);
}
};

View File

@@ -0,0 +1,31 @@
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 = (builder, items) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'trigger')) {
const table = builder.getTableByObject(object);
if (!table) {
builder.warnMissingTable('@Trigger', object);
continue;
}
table.triggers.push({
name: options.name || asTriggerName(table.name, options),
tableName: table.name,
timing: options.timing,
actions: options.actions,
when: options.when,
scope: options.scope,
referencingNewTableAs: options.referencingNewTableAs,
referencingOldTableAs: options.referencingOldTableAs,
functionName: options.functionName,
synchronize: options.synchronize ?? true,
});
}
};
const asTriggerName = (table: string, trigger: TriggerOptions) =>
asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]);

View File

@@ -0,0 +1,55 @@
import { asKey } from 'src/sql-tools/helpers';
import { ConstraintType, Processor } from 'src/sql-tools/types';
export const processUniqueConstraints: Processor = (builder, items) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'uniqueConstraint')) {
const table = builder.getTableByObject(object);
if (!table) {
builder.warnMissingTable('@Unique', object);
continue;
}
const tableName = table.name;
const columnNames = options.columns;
table.constraints.push({
type: ConstraintType.UNIQUE,
name: options.name || asUniqueConstraintName(tableName, columnNames),
tableName,
columnNames,
synchronize: options.synchronize ?? true,
});
}
// column level constraints
for (const {
type,
item: { object, propertyName, options },
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName);
if (!table) {
builder.warnMissingTable('@Column', object);
continue;
}
if (!column) {
// should be impossible since they are created in `column.processor.ts`
builder.warnMissingColumn('@Column', object, propertyName);
continue;
}
if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) {
table.constraints.push({
type: ConstraintType.UNIQUE,
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]),
tableName: table.name,
columnNames: [column.name],
synchronize: options.synchronize ?? true,
});
}
}
};
const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns);