feat: extension, triggers, functions, comments, parameters management in sql-tools (#17269)

feat: sql-tools extension, triggers, functions, comments, parameters
This commit is contained in:
Jason Rasmussen 2025-04-07 15:12:12 -04:00 committed by GitHub
parent 51c2c60231
commit e7a5b96ed0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 5205 additions and 2295 deletions

View file

@ -0,0 +1,26 @@
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asCheckConstraintName } from 'src/sql-tools/helpers';
import { DatabaseConstraintType } 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 = resolveTable(builder, object);
if (!table) {
onMissingTable(builder, '@Check', object);
continue;
}
const tableName = table.name;
table.constraints.push({
type: DatabaseConstraintType.CHECK,
name: options.name || asCheckConstraintName(tableName, options.expression),
tableName,
expression: options.expression,
synchronize: options.synchronize ?? true,
});
}
};

View file

@ -0,0 +1,32 @@
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
import { onMissingTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asIndexName } from 'src/sql-tools/helpers';
export const processColumnIndexes: Processor = (builder, items) => {
for (const {
item: { object, propertyName, options },
} of items.filter((item) => item.type === 'columnIndex')) {
const { table, column } = resolveColumn(builder, object, propertyName);
if (!table) {
onMissingTable(builder, '@ColumnIndex', object);
continue;
}
if (!column) {
onMissingColumn(builder, `@ColumnIndex`, object, propertyName);
continue;
}
table.indexes.push({
name: options.name || asIndexName(table.name, [column.name], options.where),
tableName: table.name,
unique: options.unique ?? false,
expression: options.expression,
using: options.using,
where: options.where,
columnNames: [column.name],
synchronize: options.synchronize ?? true,
});
}
};

View file

@ -0,0 +1,103 @@
import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
import { asMetadataKey, asUniqueConstraintName, fromColumnValue } from 'src/sql-tools/helpers';
import { DatabaseColumn, DatabaseConstraintType } 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 = resolveTable(builder, object.constructor);
if (!table) {
onMissingTable(builder, 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;
}
const tableName = table.name;
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;
const column: DatabaseColumn = {
name: columnName,
tableName,
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,
};
writeMetadata(object, propertyName, { name: column.name, options });
table.columns.push(column);
if (type === 'column' && !options.primary && options.unique) {
table.constraints.push({
type: DatabaseConstraintType.UNIQUE,
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]),
tableName: table.name,
columnNames: [column.name],
synchronize: options.synchronize ?? true,
});
}
}
};
type ColumnMetadata = { name: string; options: ColumnOptions };
export const resolveColumn = (builder: SchemaBuilder, object: object, propertyName: string | symbol) => {
const table = resolveTable(builder, object.constructor);
if (!table) {
return {};
}
const metadata = readMetadata(object, propertyName);
if (!metadata) {
return { table };
}
const column = table.columns.find((column) => column.name === metadata.name);
return { table, column };
};
export const onMissingColumn = (
builder: SchemaBuilder,
context: string,
object: object,
propertyName?: symbol | string,
) => {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
builder.warnings.push(`[${context}] Unable to find column (${label})`);
};
const METADATA_KEY = asMetadataKey('table-metadata');
const writeMetadata = (object: object, propertyName: symbol | string, metadata: ColumnMetadata) =>
Reflect.defineMetadata(METADATA_KEY, metadata, object, propertyName);
const readMetadata = (object: object, propertyName: symbol | string): ColumnMetadata | undefined =>
Reflect.getMetadata(METADATA_KEY, object, propertyName);

View file

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

View file

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

View file

@ -0,0 +1,8 @@
import { Processor } from 'src/sql-tools/from-code/processors/type';
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/from-code/processors/type';
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,59 @@
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asForeignKeyConstraintName, asRelationKeyConstraintName } from 'src/sql-tools/helpers';
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
export const processForeignKeyConstraints: Processor = (builder, items) => {
for (const {
item: { object, propertyName, options, target },
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
const { table, column } = resolveColumn(builder, object, propertyName);
if (!table) {
onMissingTable(builder, '@ForeignKeyColumn', object);
continue;
}
if (!column) {
// should be impossible since they are pre-created in `column.processor.ts`
onMissingColumn(builder, '@ForeignKeyColumn', object, propertyName);
continue;
}
const referenceTable = resolveTable(builder, target());
if (!referenceTable) {
onMissingTable(builder, '@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;
}
table.constraints.push({
name: options.constraintName || asForeignKeyConstraintName(table.name, columnNames),
tableName: table.name,
columnNames,
type: DatabaseConstraintType.FOREIGN_KEY,
referenceTableName: referenceTable.name,
referenceColumnNames: referenceColumns.map((column) => column.name),
onUpdate: options.onUpdate as DatabaseActionType,
onDelete: options.onDelete as DatabaseActionType,
synchronize: options.synchronize ?? true,
});
if (options.unique) {
table.constraints.push({
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames),
tableName: table.name,
columnNames,
type: DatabaseConstraintType.UNIQUE,
synchronize: options.synchronize ?? true,
});
}
}
};

View file

@ -0,0 +1,8 @@
import { Processor } from 'src/sql-tools/from-code/processors/type';
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,27 @@
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asIndexName } from 'src/sql-tools/helpers';
export const processIndexes: Processor = (builder, items) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'index')) {
const table = resolveTable(builder, object);
if (!table) {
onMissingTable(builder, '@Check', object);
continue;
}
table.indexes.push({
name: options.name || 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,
});
}
};

View file

@ -0,0 +1,24 @@
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asPrimaryKeyConstraintName } from 'src/sql-tools/helpers';
import { DatabaseConstraintType } 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) {
table.constraints.push({
type: DatabaseConstraintType.PRIMARY_KEY,
name: table.metadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames),
tableName: table.name,
columnNames,
synchronize: table.metadata.options.synchronize ?? true,
});
}
}
};

View file

@ -0,0 +1,51 @@
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
import { asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
export const processTables: Processor = (builder, items) => {
for (const {
item: { options, object },
} of items.filter((item) => item.type === 'table')) {
const tableName = options.name || asSnakeCase(object.name);
writeMetadata(object, { name: tableName, options });
builder.tables.push({
name: tableName,
columns: [],
constraints: [],
indexes: [],
triggers: [],
synchronize: options.synchronize ?? true,
metadata: { options, object },
});
}
};
export const resolveTable = (builder: SchemaBuilder, object: object) => {
const metadata = readMetadata(object);
if (!metadata) {
return;
}
return builder.tables.find((table) => table.name === metadata.name);
};
export const onMissingTable = (
builder: SchemaBuilder,
context: string,
object: object,
propertyName?: symbol | string,
) => {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
builder.warnings.push(`[${context}] Unable to find table (${label})`);
};
const METADATA_KEY = asMetadataKey('table-metadata');
type TableMetadata = { name: string; options: TableOptions };
const readMetadata = (object: object): TableMetadata | undefined => Reflect.getMetadata(METADATA_KEY, object);
const writeMetadata = (object: object, metadata: TableMetadata): void =>
Reflect.defineMetadata(METADATA_KEY, metadata, object);

View file

@ -0,0 +1,28 @@
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asTriggerName } from 'src/sql-tools/helpers';
export const processTriggers: Processor = (builder, items) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'trigger')) {
const table = resolveTable(builder, object);
if (!table) {
onMissingTable(builder, '@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,
});
}
};

View file

@ -0,0 +1,9 @@
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
import { RegisterItem } from 'src/sql-tools/from-code/register-item';
import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types';
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export type TableWithMetadata = DatabaseTable & { metadata: { options: TableOptions; object: Function } };
export type SchemaBuilder = Omit<DatabaseSchema, 'tables'> & { tables: TableWithMetadata[] };
export type Processor = (builder: SchemaBuilder, items: RegisterItem[]) => void;

View file

@ -0,0 +1,27 @@
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { asUniqueConstraintName } from 'src/sql-tools/helpers';
import { DatabaseConstraintType } 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 = resolveTable(builder, object);
if (!table) {
onMissingTable(builder, '@Unique', object);
continue;
}
const tableName = table.name;
const columnNames = options.columns;
table.constraints.push({
type: DatabaseConstraintType.UNIQUE,
name: options.name || asUniqueConstraintName(tableName, columnNames),
tableName,
columnNames,
synchronize: options.synchronize ?? true,
});
}
};