Initial commit - Event Planner application

This commit is contained in:
mberlin
2026-03-18 14:55:56 -03:00
commit 86d779eb4d
7548 changed files with 1006324 additions and 0 deletions

83
node_modules/@mikro-orm/sql/schema/DatabaseSchema.d.ts generated vendored Normal file
View File

@@ -0,0 +1,83 @@
import { type Configuration, type Dictionary, type EntityMetadata } from '@mikro-orm/core';
import { DatabaseTable } from './DatabaseTable.js';
import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
import type { DatabaseView } from '../typings.js';
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
/**
* @internal
*/
export declare class DatabaseSchema {
#private;
readonly name: string;
constructor(platform: AbstractSqlPlatform, name: string);
addTable(name: string, schema: string | undefined | null, comment?: string): DatabaseTable;
getTables(): DatabaseTable[];
/** @internal */
setTables(tables: DatabaseTable[]): void;
/** @internal */
setNamespaces(namespaces: Set<string>): void;
getTable(name: string): DatabaseTable | undefined;
hasTable(name: string): boolean;
addView(
name: string,
schema: string | undefined | null,
definition: string,
materialized?: boolean,
withData?: boolean,
): DatabaseView;
getViews(): DatabaseView[];
/** @internal */
setViews(views: DatabaseView[]): void;
getView(name: string): DatabaseView | undefined;
hasView(name: string): boolean;
setNativeEnums(
nativeEnums: Dictionary<{
name: string;
schema?: string;
items: string[];
}>,
): void;
getNativeEnums(): Dictionary<{
name: string;
schema?: string;
items: string[];
}>;
getNativeEnum(name: string): {
name: string;
schema?: string;
items: string[];
};
hasNamespace(namespace: string): boolean;
hasNativeEnum(name: string): boolean;
getNamespaces(): string[];
static create(
connection: AbstractSqlConnection,
platform: AbstractSqlPlatform,
config: Configuration,
schemaName?: string,
schemas?: string[],
takeTables?: (string | RegExp)[],
skipTables?: (string | RegExp)[],
skipViews?: (string | RegExp)[],
): Promise<DatabaseSchema>;
static fromMetadata(
metadata: EntityMetadata[],
platform: AbstractSqlPlatform,
config: Configuration,
schemaName?: string,
em?: any,
): DatabaseSchema;
private static getViewDefinition;
private static getSchemaName;
/**
* Add a foreign key from a TPT child entity's PK to its parent entity's PK.
* This FK uses ON DELETE CASCADE to ensure child rows are deleted when parent is deleted.
*/
private static addTPTForeignKey;
private static matchName;
private static isNameAllowed;
private static isTableNameAllowed;
private static shouldHaveColumn;
toJSON(): Dictionary;
prune(schema: string | undefined, wildcardSchemaTables: string[]): void;
}

360
node_modules/@mikro-orm/sql/schema/DatabaseSchema.js generated vendored Normal file
View File

@@ -0,0 +1,360 @@
import { ReferenceKind, isRaw } from '@mikro-orm/core';
import { DatabaseTable } from './DatabaseTable.js';
/**
* @internal
*/
export class DatabaseSchema {
name;
#tables = [];
#views = [];
#namespaces = new Set();
#nativeEnums = {}; // for postgres
#platform;
constructor(platform, name) {
this.name = name;
this.#platform = platform;
}
addTable(name, schema, comment) {
const namespaceName = schema ?? this.name;
const table = new DatabaseTable(this.#platform, name, namespaceName);
table.nativeEnums = this.#nativeEnums;
table.comment = comment;
this.#tables.push(table);
if (namespaceName != null) {
this.#namespaces.add(namespaceName);
}
return table;
}
getTables() {
return this.#tables;
}
/** @internal */
setTables(tables) {
this.#tables = tables;
}
/** @internal */
setNamespaces(namespaces) {
this.#namespaces = namespaces;
}
getTable(name) {
return this.#tables.find(t => t.name === name || `${t.schema}.${t.name}` === name);
}
hasTable(name) {
return !!this.getTable(name);
}
addView(name, schema, definition, materialized, withData) {
const namespaceName = schema ?? this.name;
const view = { name, schema: namespaceName, definition, materialized, withData };
this.#views.push(view);
if (namespaceName != null) {
this.#namespaces.add(namespaceName);
}
return view;
}
getViews() {
return this.#views;
}
/** @internal */
setViews(views) {
this.#views = views;
}
getView(name) {
return this.#views.find(v => v.name === name || `${v.schema}.${v.name}` === name);
}
hasView(name) {
return !!this.getView(name);
}
setNativeEnums(nativeEnums) {
this.#nativeEnums = nativeEnums;
for (const nativeEnum of Object.values(nativeEnums)) {
if (nativeEnum.schema && nativeEnum.schema !== '*') {
this.#namespaces.add(nativeEnum.schema);
}
}
}
getNativeEnums() {
return this.#nativeEnums;
}
getNativeEnum(name) {
return this.#nativeEnums[name];
}
hasNamespace(namespace) {
return this.#namespaces.has(namespace);
}
hasNativeEnum(name) {
return name in this.#nativeEnums;
}
getNamespaces() {
return [...this.#namespaces];
}
static async create(connection, platform, config, schemaName, schemas, takeTables, skipTables, skipViews) {
const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema') ?? platform.getDefaultSchemaName());
const allTables = await platform.getSchemaHelper().getAllTables(connection, schemas);
const parts = config.get('migrations').tableName.split('.');
const migrationsTableName = parts[1] ?? parts[0];
const migrationsSchemaName = parts.length > 1 ? parts[0] : config.get('schema', platform.getDefaultSchemaName());
const tables = allTables.filter(
t =>
this.isTableNameAllowed(t.table_name, takeTables, skipTables) &&
(t.table_name !== migrationsTableName || (t.schema_name && t.schema_name !== migrationsSchemaName)),
);
await platform
.getSchemaHelper()
.loadInformationSchema(schema, connection, tables, schemas && schemas.length > 0 ? schemas : undefined);
// Load views from database
await platform.getSchemaHelper().loadViews(schema, connection);
// Load materialized views (PostgreSQL only)
if (platform.supportsMaterializedViews()) {
await platform.getSchemaHelper().loadMaterializedViews(schema, connection, schemaName);
}
// Filter out skipped views
if (skipViews && skipViews.length > 0) {
schema.#views = schema.#views.filter(v => this.isNameAllowed(v.name, skipViews));
}
return schema;
}
static fromMetadata(metadata, platform, config, schemaName, em) {
const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema'));
const nativeEnums = {};
const skipColumns = config.get('schemaGenerator').skipColumns || {};
for (const meta of metadata) {
// Skip view entities when collecting native enums
if (meta.view) {
continue;
}
for (const prop of meta.props) {
if (prop.nativeEnumName) {
let key = prop.nativeEnumName;
let enumName = prop.nativeEnumName;
let enumSchema = meta.schema ?? schema.name;
if (key.includes('.')) {
const [explicitSchema, ...parts] = prop.nativeEnumName.split('.');
enumName = parts.join('.');
key = enumName;
enumSchema = explicitSchema;
}
if (enumSchema && enumSchema !== '*' && enumSchema !== platform.getDefaultSchemaName()) {
key = enumSchema + '.' + key;
}
nativeEnums[key] = {
name: enumName,
schema: enumSchema,
items: prop.items?.map(val => '' + val) ?? [],
};
}
}
}
schema.setNativeEnums(nativeEnums);
for (const meta of metadata) {
// Handle view entities separately
if (meta.view) {
const viewDefinition = this.getViewDefinition(meta, em, platform);
if (viewDefinition) {
schema.addView(
meta.collection,
this.getSchemaName(meta, config, schemaName),
viewDefinition,
meta.materialized,
meta.withData,
);
}
continue;
}
const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName));
table.comment = meta.comment;
// For TPT child entities, only use ownProps (properties defined in this entity only)
// For all other entities (including TPT root), use all props
const propsToProcess =
meta.inheritanceType === 'tpt' && meta.tptParent && meta.ownProps ? meta.ownProps : meta.props;
for (const prop of propsToProcess) {
if (!this.shouldHaveColumn(meta, prop, skipColumns)) {
continue;
}
table.addColumnFromProperty(prop, meta, config);
}
// For TPT child entities, always include the PK columns (they form the FK to parent)
if (meta.inheritanceType === 'tpt' && meta.tptParent) {
const pkProps = meta.primaryKeys.map(pk => meta.properties[pk]);
for (const pkProp of pkProps) {
// Only add if not already added (it might be in ownProps if defined in this entity)
if (!propsToProcess.includes(pkProp)) {
table.addColumnFromProperty(pkProp, meta, config);
}
// Child PK must not be autoincrement — it references the parent PK via FK
for (const field of pkProp.fieldNames) {
const col = table.getColumn(field);
if (col) {
col.autoincrement = false;
}
}
}
// Add FK from child PK to parent PK with ON DELETE CASCADE
this.addTPTForeignKey(table, meta, config, platform);
}
meta.indexes.forEach(index => table.addIndex(meta, index, 'index'));
meta.uniques.forEach(index => table.addIndex(meta, index, 'unique'));
// For TPT child entities, the PK is also defined here
const pkPropsForIndex =
meta.inheritanceType === 'tpt' && meta.tptParent
? meta.primaryKeys.map(pk => meta.properties[pk])
: meta.props.filter(prop => prop.primary);
table.addIndex(meta, { properties: pkPropsForIndex.map(prop => prop.name) }, 'primary');
for (const check of meta.checks) {
const columnName = check.property ? meta.properties[check.property].fieldNames[0] : undefined;
const expression = isRaw(check.expression)
? platform.formatQuery(check.expression.sql, check.expression.params)
: check.expression;
table.addCheck({
name: check.name,
expression,
definition: `check (${expression})`,
columnName,
});
}
}
return schema;
}
static getViewDefinition(meta, em, platform) {
if (typeof meta.expression === 'string') {
return meta.expression;
}
// Expression is a function, need to evaluate it
/* v8 ignore next */
if (!em) {
return undefined;
}
const result = meta.expression(em, {}, {});
// Async expressions are not supported for view entities
if (result && typeof result.then === 'function') {
throw new Error(
`View entity ${meta.className} expression returned a Promise. Async expressions are not supported for view entities.`,
);
}
/* v8 ignore next */
if (typeof result === 'string') {
return result;
}
/* v8 ignore next */
if (isRaw(result)) {
return platform.formatQuery(result.sql, result.params);
}
// Check if it's a QueryBuilder (has getFormattedQuery method)
if (result && typeof result.getFormattedQuery === 'function') {
return result.getFormattedQuery();
}
/* v8 ignore next - fallback for unknown result types */
return undefined;
}
static getSchemaName(meta, config, schema) {
return (meta.schema === '*' ? schema : meta.schema) ?? config.get('schema');
}
/**
* Add a foreign key from a TPT child entity's PK to its parent entity's PK.
* This FK uses ON DELETE CASCADE to ensure child rows are deleted when parent is deleted.
*/
static addTPTForeignKey(table, meta, config, platform) {
const parent = meta.tptParent;
const pkColumnNames = meta.primaryKeys.flatMap(pk => meta.properties[pk].fieldNames);
const parentPkColumnNames = parent.primaryKeys.flatMap(pk => parent.properties[pk].fieldNames);
// Determine the parent table name with schema
const parentSchema =
parent.schema === '*' ? undefined : (parent.schema ?? config.get('schema', platform.getDefaultSchemaName()));
const parentTableName = parentSchema ? `${parentSchema}.${parent.tableName}` : parent.tableName;
// Create FK constraint name
const constraintName = platform.getIndexName(table.name, pkColumnNames, 'foreign');
// Add the foreign key to the table
const fks = table.getForeignKeys();
fks[constraintName] = {
constraintName,
columnNames: pkColumnNames,
localTableName: table.getShortestName(false),
referencedColumnNames: parentPkColumnNames,
referencedTableName: parentTableName,
deleteRule: 'cascade', // TPT always uses cascade delete
updateRule: 'cascade', // TPT always uses cascade update
};
}
static matchName(name, nameToMatch) {
return typeof nameToMatch === 'string'
? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
: nameToMatch.test(name);
}
static isNameAllowed(name, skipNames) {
return !(skipNames?.some(pattern => this.matchName(name, pattern)) ?? false);
}
static isTableNameAllowed(tableName, takeTables, skipTables) {
return (
(takeTables?.some(tableNameToMatch => this.matchName(tableName, tableNameToMatch)) ?? true) &&
this.isNameAllowed(tableName, skipTables)
);
}
static shouldHaveColumn(meta, prop, skipColumns) {
if (prop.persist === false || (prop.columnTypes?.length ?? 0) === 0) {
return false;
}
// Check if column should be skipped
if (skipColumns) {
const tableName = meta.tableName;
const tableSchema = meta.schema;
const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName;
// Check for skipColumns by table name or fully qualified table name
const columnsToSkip = skipColumns[tableName] || skipColumns[fullTableName];
if (columnsToSkip) {
for (const fieldName of prop.fieldNames) {
if (columnsToSkip.some(pattern => this.matchName(fieldName, pattern))) {
return false;
}
}
}
}
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
return true;
}
const getRootProperty = prop => (prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop);
const rootProp = getRootProperty(prop);
if (rootProp.kind === ReferenceKind.EMBEDDED) {
return prop === rootProp || !rootProp.object;
}
return (
[ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind) ||
(prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner)
);
}
toJSON() {
return {
name: this.name,
namespaces: [...this.#namespaces],
tables: this.#tables,
views: this.#views,
nativeEnums: this.#nativeEnums,
};
}
prune(schema, wildcardSchemaTables) {
const hasWildcardSchema = wildcardSchemaTables.length > 0;
this.#tables = this.#tables.filter(table => {
return (
(!schema && !hasWildcardSchema) || // no schema specified and we don't have any multi-schema entity
table.schema === schema || // specified schema matches the table's one
(!schema && !wildcardSchemaTables.includes(table.name))
); // no schema specified and the table has fixed one provided
});
this.#views = this.#views.filter(view => {
/* v8 ignore next */
return (
(!schema && !hasWildcardSchema) ||
view.schema === schema ||
(!schema && !wildcardSchemaTables.includes(view.name))
);
});
// remove namespaces of ignored tables and views
for (const ns of this.#namespaces) {
if (
!this.#tables.some(t => t.schema === ns) &&
!this.#views.some(v => v.schema === ns) &&
!Object.values(this.#nativeEnums).some(e => e.schema === ns)
) {
this.#namespaces.delete(ns);
}
}
}
}

105
node_modules/@mikro-orm/sql/schema/DatabaseTable.d.ts generated vendored Normal file
View File

@@ -0,0 +1,105 @@
import {
type Configuration,
type DeferMode,
type Dictionary,
type EntityMetadata,
type EntityProperty,
type IndexCallback,
type NamingStrategy,
} from '@mikro-orm/core';
import type { SchemaHelper } from './SchemaHelper.js';
import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings.js';
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
/**
* @internal
*/
export declare class DatabaseTable {
#private;
readonly name: string;
readonly schema?: string | undefined;
nativeEnums: Dictionary<{
name: string;
schema?: string;
items: string[];
}>;
comment?: string;
constructor(platform: AbstractSqlPlatform, name: string, schema?: string | undefined);
getQuotedName(): string;
getColumns(): Column[];
getColumn(name: string): Column | undefined;
removeColumn(name: string): void;
getIndexes(): IndexDef[];
getChecks(): CheckDef[];
/** @internal */
setIndexes(indexes: IndexDef[]): void;
/** @internal */
setChecks(checks: CheckDef[]): void;
/** @internal */
setForeignKeys(fks: Dictionary<ForeignKey>): void;
init(
cols: Column[],
indexes: IndexDef[] | undefined,
checks: CheckDef[] | undefined,
pks: string[],
fks?: Dictionary<ForeignKey>,
enums?: Dictionary<string[]>,
): void;
addColumn(column: Column): void;
addColumnFromProperty(prop: EntityProperty, meta: EntityMetadata, config: Configuration): void;
private getIndexName;
getEntityDeclaration(
namingStrategy: NamingStrategy,
schemaHelper: SchemaHelper,
scalarPropertiesForRelations: 'always' | 'never' | 'smart',
): EntityMetadata;
private foreignKeysToProps;
private findFkIndex;
private getIndexProperties;
private getSafeBaseNameForFkProp;
/**
* The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names.
*/
getShortestName(skipDefaultSchema?: boolean): string;
getForeignKeys(): Dictionary<ForeignKey>;
hasColumn(columnName: string): boolean;
getIndex(indexName: string): IndexDef | undefined;
hasIndex(indexName: string): boolean;
getCheck(checkName: string): CheckDef | undefined;
hasCheck(checkName: string): boolean;
getPrimaryKey(): IndexDef | undefined;
hasPrimaryKey(): boolean;
private getForeignKeyDeclaration;
private getPropertyDeclaration;
private getReferenceKind;
private getPropertyName;
private getPropertyTypeForForeignKey;
private getPropertyTypeForColumn;
private getPropertyDefaultValue;
private processIndexExpression;
addIndex(
meta: EntityMetadata,
index: {
properties?: string | string[];
name?: string;
type?: string;
expression?: string | IndexCallback<any>;
deferMode?: DeferMode | `${DeferMode}`;
options?: Dictionary;
columns?: {
name: string;
sort?: 'ASC' | 'DESC' | 'asc' | 'desc';
nulls?: 'FIRST' | 'LAST' | 'first' | 'last';
length?: number;
collation?: string;
}[];
include?: string | string[];
fillFactor?: number;
invisible?: boolean;
disabled?: boolean;
clustered?: boolean;
},
type: 'index' | 'unique' | 'primary',
): void;
addCheck(check: CheckDef): void;
toJSON(): Dictionary;
}

1022
node_modules/@mikro-orm/sql/schema/DatabaseTable.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
import { type Dictionary } from '@mikro-orm/core';
import type { Column, ForeignKey, IndexDef, SchemaDifference, TableDifference } from '../typings.js';
import type { DatabaseSchema } from './DatabaseSchema.js';
import type { DatabaseTable } from './DatabaseTable.js';
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
/**
* Compares two Schemas and return an instance of SchemaDifference.
*/
export declare class SchemaComparator {
#private;
constructor(platform: AbstractSqlPlatform);
/**
* Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema.
*
* The returned differences are returned in such a way that they contain the
* operations to change the schema stored in fromSchema to the schema that is
* stored in toSchema.
*/
compare(fromSchema: DatabaseSchema, toSchema: DatabaseSchema, inverseDiff?: SchemaDifference): SchemaDifference;
/**
* Returns the difference between the tables fromTable and toTable.
* If there are no differences this method returns the boolean false.
*/
diffTable(
fromTable: DatabaseTable,
toTable: DatabaseTable,
inverseTableDiff?: TableDifference,
): TableDifference | false;
/**
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*/
private detectColumnRenamings;
/**
* Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*/
private detectIndexRenamings;
diffForeignKey(key1: ForeignKey, key2: ForeignKey, tableDifferences: TableDifference): boolean;
/**
* Returns the difference between the columns
*/
diffColumn(fromColumn: Column, toColumn: Column, fromTable: DatabaseTable, logging?: boolean): Set<string>;
diffEnumItems(items1?: string[], items2?: string[]): boolean;
diffComment(comment1?: string, comment2?: string): boolean;
/**
* Finds the difference between the indexes index1 and index2.
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
*/
diffIndex(index1: IndexDef, index2: IndexDef): boolean;
/**
* Checks if the other index already fulfills all the indexing and constraint needs of the current one.
*/
isIndexFulfilledBy(index1: IndexDef, index2: IndexDef): boolean;
/**
* Compare advanced column options between two indexes.
*/
private compareIndexColumns;
/**
* Compare two arrays for equality (order matters).
*/
private compareArrays;
diffExpression(expr1: string, expr2: string): boolean;
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
hasSameDefaultValue(from: Column, to: Column): boolean;
private mapColumnToProperty;
private log;
}

753
node_modules/@mikro-orm/sql/schema/SchemaComparator.js generated vendored Normal file
View File

@@ -0,0 +1,753 @@
import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils } from '@mikro-orm/core';
/**
* Compares two Schemas and return an instance of SchemaDifference.
*/
export class SchemaComparator {
#helper;
#logger;
#platform;
constructor(platform) {
this.#platform = platform;
this.#helper = this.#platform.getSchemaHelper();
this.#logger = this.#platform.getConfig().getLogger();
}
/**
* Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema.
*
* The returned differences are returned in such a way that they contain the
* operations to change the schema stored in fromSchema to the schema that is
* stored in toSchema.
*/
compare(fromSchema, toSchema, inverseDiff) {
const diff = {
newTables: {},
removedTables: {},
changedTables: {},
newViews: {},
changedViews: {},
removedViews: {},
orphanedForeignKeys: [],
newNativeEnums: [],
removedNativeEnums: [],
newNamespaces: new Set(),
removedNamespaces: new Set(),
fromSchema,
};
const foreignKeysToTable = {};
for (const namespace of toSchema.getNamespaces()) {
if (fromSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
continue;
}
diff.newNamespaces.add(namespace);
}
for (const namespace of fromSchema.getNamespaces()) {
if (toSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
continue;
}
diff.removedNamespaces.add(namespace);
}
for (const [key, nativeEnum] of Object.entries(toSchema.getNativeEnums())) {
if (fromSchema.hasNativeEnum(key)) {
continue;
}
if (nativeEnum.schema === '*' && fromSchema.hasNativeEnum(`${toSchema.name}.${key}`)) {
continue;
}
diff.newNativeEnums.push(nativeEnum);
}
for (const [key, nativeEnum] of Object.entries(fromSchema.getNativeEnums())) {
if (toSchema.hasNativeEnum(key)) {
continue;
}
if (
key.startsWith(`${fromSchema.name}.`) &&
(fromSchema.name !== toSchema.name ||
toSchema.getNativeEnum(key.substring(fromSchema.name.length + 1))?.schema === '*')
) {
continue;
}
diff.removedNativeEnums.push(nativeEnum);
}
for (const table of toSchema.getTables()) {
const tableName = table.getShortestName(false);
if (!fromSchema.hasTable(tableName)) {
diff.newTables[tableName] = toSchema.getTable(tableName);
} else {
const tableDifferences = this.diffTable(
fromSchema.getTable(tableName),
toSchema.getTable(tableName),
inverseDiff?.changedTables[tableName],
);
if (tableDifferences !== false) {
diff.changedTables[tableName] = tableDifferences;
}
}
}
// Check if there are tables removed
for (let table of fromSchema.getTables()) {
const tableName = table.getShortestName();
table = fromSchema.getTable(tableName);
if (!toSchema.hasTable(tableName)) {
diff.removedTables[tableName] = table;
}
// also remember all foreign keys that point to a specific table
for (const foreignKey of Object.values(table.getForeignKeys())) {
if (!foreignKeysToTable[foreignKey.referencedTableName]) {
foreignKeysToTable[foreignKey.referencedTableName] = [];
}
foreignKeysToTable[foreignKey.referencedTableName].push(foreignKey);
}
}
for (const table of Object.values(diff.removedTables)) {
const tableName = (table.schema ? table.schema + '.' : '') + table.name;
if (!foreignKeysToTable[tableName]) {
continue;
}
diff.orphanedForeignKeys.push(...foreignKeysToTable[tableName]);
// Deleting duplicated foreign keys present both on the orphanedForeignKey and the removedForeignKeys from changedTables.
for (const foreignKey of foreignKeysToTable[tableName]) {
const localTableName = foreignKey.localTableName;
if (!diff.changedTables[localTableName]) {
continue;
}
for (const [key, fk] of Object.entries(diff.changedTables[localTableName].removedForeignKeys)) {
// We check if the key is from the removed table, if not we skip.
if (tableName !== fk.referencedTableName) {
continue;
}
delete diff.changedTables[localTableName].removedForeignKeys[key];
}
}
}
// Compare views
for (const toView of toSchema.getViews()) {
const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
if (!fromSchema.hasView(toView.name) && !fromSchema.hasView(viewName)) {
diff.newViews[viewName] = toView;
this.log(`view ${viewName} added`);
} else {
const fromView = fromSchema.getView(toView.name) ?? fromSchema.getView(viewName);
if (fromView && this.diffExpression(fromView.definition, toView.definition)) {
diff.changedViews[viewName] = { from: fromView, to: toView };
this.log(`view ${viewName} changed`);
}
}
}
// Check for removed views
for (const fromView of fromSchema.getViews()) {
const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name;
if (!toSchema.hasView(fromView.name) && !toSchema.hasView(viewName)) {
diff.removedViews[viewName] = fromView;
this.log(`view ${viewName} removed`);
}
}
return diff;
}
/**
* Returns the difference between the tables fromTable and toTable.
* If there are no differences this method returns the boolean false.
*/
diffTable(fromTable, toTable, inverseTableDiff) {
let changes = 0;
const tableDifferences = {
name: fromTable.getShortestName(),
addedColumns: {},
addedForeignKeys: {},
addedIndexes: {},
addedChecks: {},
changedColumns: {},
changedForeignKeys: {},
changedIndexes: {},
changedChecks: {},
removedColumns: {},
removedForeignKeys: {},
removedIndexes: {},
removedChecks: {},
renamedColumns: {},
renamedIndexes: {},
fromTable,
toTable,
};
if (this.diffComment(fromTable.comment, toTable.comment)) {
tableDifferences.changedComment = toTable.comment;
this.log(`table comment changed for ${tableDifferences.name}`, {
fromTableComment: fromTable.comment,
toTableComment: toTable.comment,
});
changes++;
}
const fromTableColumns = fromTable.getColumns();
const toTableColumns = toTable.getColumns();
// See if all the columns in "from" table exist in "to" table
for (const column of toTableColumns) {
if (fromTable.hasColumn(column.name)) {
continue;
}
tableDifferences.addedColumns[column.name] = column;
this.log(`column ${tableDifferences.name}.${column.name} of type ${column.type} added`);
changes++;
}
/* See if there are any removed columns in "to" table */
for (const column of fromTableColumns) {
// See if column is removed in "to" table.
if (!toTable.hasColumn(column.name)) {
tableDifferences.removedColumns[column.name] = column;
this.log(`column ${tableDifferences.name}.${column.name} removed`);
changes++;
continue;
}
// See if column has changed properties in "to" table.
const changedProperties = this.diffColumn(column, toTable.getColumn(column.name), fromTable, true);
if (changedProperties.size === 0) {
continue;
}
if (changedProperties.size === 1 && changedProperties.has('generated')) {
tableDifferences.addedColumns[column.name] = toTable.getColumn(column.name);
tableDifferences.removedColumns[column.name] = column;
changes++;
continue;
}
tableDifferences.changedColumns[column.name] = {
oldColumnName: column.name,
fromColumn: column,
column: toTable.getColumn(column.name),
changedProperties,
};
this.log(`column ${tableDifferences.name}.${column.name} changed`, { changedProperties });
changes++;
}
this.detectColumnRenamings(tableDifferences, inverseTableDiff);
const fromTableIndexes = fromTable.getIndexes();
const toTableIndexes = toTable.getIndexes();
// See if all the indexes in "from" table exist in "to" table
for (const index of Object.values(toTableIndexes)) {
if ((index.primary && fromTableIndexes.find(i => i.primary)) || fromTable.hasIndex(index.keyName)) {
continue;
}
tableDifferences.addedIndexes[index.keyName] = index;
this.log(`index ${index.keyName} added to table ${tableDifferences.name}`, { index });
changes++;
}
// See if there are any removed indexes in "to" table
for (const index of fromTableIndexes) {
// See if index is removed in "to" table.
if ((index.primary && !toTable.hasPrimaryKey()) || (!index.primary && !toTable.hasIndex(index.keyName))) {
tableDifferences.removedIndexes[index.keyName] = index;
this.log(`index ${index.keyName} removed from table ${tableDifferences.name}`);
changes++;
continue;
}
// See if index has changed in "to" table.
const toTableIndex = index.primary ? toTable.getPrimaryKey() : toTable.getIndex(index.keyName);
if (!this.diffIndex(index, toTableIndex)) {
continue;
}
tableDifferences.changedIndexes[index.keyName] = toTableIndex;
this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
fromTableIndex: index,
toTableIndex,
});
changes++;
}
this.detectIndexRenamings(tableDifferences);
const fromTableChecks = fromTable.getChecks();
const toTableChecks = toTable.getChecks();
// See if all the checks in "from" table exist in "to" table
for (const check of toTableChecks) {
if (fromTable.hasCheck(check.name)) {
continue;
}
tableDifferences.addedChecks[check.name] = check;
this.log(`check constraint ${check.name} added to table ${tableDifferences.name}`, { check });
changes++;
}
// See if there are any removed checks in "to" table
for (const check of fromTableChecks) {
if (!toTable.hasCheck(check.name)) {
tableDifferences.removedChecks[check.name] = check;
this.log(`check constraint ${check.name} removed from table ${tableDifferences.name}`);
changes++;
continue;
}
// See if check has changed in "to" table
const toTableCheck = toTable.getCheck(check.name);
const toColumn = toTable.getColumn(check.columnName);
const fromColumn = fromTable.getColumn(check.columnName);
if (!this.diffExpression(check.expression, toTableCheck.expression)) {
continue;
}
if (
fromColumn?.enumItems &&
toColumn?.enumItems &&
!this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)
) {
continue;
}
this.log(`check constraint ${check.name} changed in table ${tableDifferences.name}`, {
fromTableCheck: check,
toTableCheck,
});
tableDifferences.changedChecks[check.name] = toTableCheck;
changes++;
}
const fromForeignKeys = { ...fromTable.getForeignKeys() };
const toForeignKeys = { ...toTable.getForeignKeys() };
for (const fromConstraint of Object.values(fromForeignKeys)) {
for (const toConstraint of Object.values(toForeignKeys)) {
if (!this.diffForeignKey(fromConstraint, toConstraint, tableDifferences)) {
delete fromForeignKeys[fromConstraint.constraintName];
delete toForeignKeys[toConstraint.constraintName];
} else if (fromConstraint.constraintName.toLowerCase() === toConstraint.constraintName.toLowerCase()) {
this.log(`FK constraint ${fromConstraint.constraintName} changed in table ${tableDifferences.name}`, {
fromConstraint,
toConstraint,
});
tableDifferences.changedForeignKeys[toConstraint.constraintName] = toConstraint;
changes++;
delete fromForeignKeys[fromConstraint.constraintName];
delete toForeignKeys[toConstraint.constraintName];
}
}
}
for (const fromConstraint of Object.values(fromForeignKeys)) {
tableDifferences.removedForeignKeys[fromConstraint.constraintName] = fromConstraint;
this.log(`FK constraint ${fromConstraint.constraintName} removed from table ${tableDifferences.name}`);
changes++;
}
for (const toConstraint of Object.values(toForeignKeys)) {
tableDifferences.addedForeignKeys[toConstraint.constraintName] = toConstraint;
this.log(`FK constraint ${toConstraint.constraintName} added to table ${tableDifferences.name}`, {
constraint: toConstraint,
});
changes++;
}
return changes ? tableDifferences : false;
}
/**
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*/
detectColumnRenamings(tableDifferences, inverseTableDiff) {
const renameCandidates = {};
const oldFKs = Object.values(tableDifferences.fromTable.getForeignKeys());
const newFKs = Object.values(tableDifferences.toTable.getForeignKeys());
for (const addedColumn of Object.values(tableDifferences.addedColumns)) {
for (const removedColumn of Object.values(tableDifferences.removedColumns)) {
const diff = this.diffColumn(addedColumn, removedColumn, tableDifferences.fromTable);
if (diff.size !== 0) {
continue;
}
const wasFK = oldFKs.some(fk => fk.columnNames.includes(removedColumn.name));
const isFK = newFKs.some(fk => fk.columnNames.includes(addedColumn.name));
if (wasFK !== isFK) {
continue;
}
const renamedColumn = inverseTableDiff?.renamedColumns[addedColumn.name];
if (renamedColumn && renamedColumn?.name !== removedColumn.name) {
continue;
}
renameCandidates[addedColumn.name] = renameCandidates[addedColumn.name] ?? [];
renameCandidates[addedColumn.name].push([removedColumn, addedColumn]);
}
}
for (const candidateColumns of Object.values(renameCandidates)) {
if (candidateColumns.length !== 1) {
continue;
}
const [removedColumn, addedColumn] = candidateColumns[0];
const removedColumnName = removedColumn.name;
const addedColumnName = addedColumn.name;
/* v8 ignore next */
if (tableDifferences.renamedColumns[removedColumnName]) {
continue;
}
tableDifferences.renamedColumns[removedColumnName] = addedColumn;
delete tableDifferences.addedColumns[addedColumnName];
delete tableDifferences.removedColumns[removedColumnName];
this.log(`renamed column detected in table ${tableDifferences.name}`, {
old: removedColumnName,
new: addedColumnName,
});
}
}
/**
* Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*/
detectIndexRenamings(tableDifferences) {
const renameCandidates = {};
// Gather possible rename candidates by comparing each added and removed index based on semantics.
for (const addedIndex of Object.values(tableDifferences.addedIndexes)) {
for (const removedIndex of Object.values(tableDifferences.removedIndexes)) {
if (this.diffIndex(addedIndex, removedIndex)) {
continue;
}
renameCandidates[addedIndex.keyName] = renameCandidates[addedIndex.keyName] ?? [];
renameCandidates[addedIndex.keyName].push([removedIndex, addedIndex]);
}
}
for (const candidateIndexes of Object.values(renameCandidates)) {
// If the current rename candidate contains exactly one semantically equal index, we can safely rename it.
// Otherwise it is unclear if a rename action is really intended, therefore we let those ambiguous indexes be added/dropped.
if (candidateIndexes.length !== 1) {
continue;
}
const [removedIndex, addedIndex] = candidateIndexes[0];
const removedIndexName = removedIndex.keyName;
const addedIndexName = addedIndex.keyName;
if (tableDifferences.renamedIndexes[removedIndexName]) {
continue;
}
tableDifferences.renamedIndexes[removedIndexName] = addedIndex;
delete tableDifferences.addedIndexes[addedIndexName];
delete tableDifferences.removedIndexes[removedIndexName];
this.log(`renamed index detected in table ${tableDifferences.name}`, {
old: removedIndexName,
new: addedIndexName,
});
}
}
diffForeignKey(key1, key2, tableDifferences) {
if (key1.columnNames.join('~').toLowerCase() !== key2.columnNames.join('~').toLowerCase()) {
return true;
}
if (key1.referencedColumnNames.join('~').toLowerCase() !== key2.referencedColumnNames.join('~').toLowerCase()) {
return true;
}
if (key1.constraintName !== key2.constraintName) {
return true;
}
if (key1.referencedTableName !== key2.referencedTableName) {
return true;
}
if (key1.deferMode !== key2.deferMode) {
return true;
}
if (key1.localTableName === key1.referencedTableName && !this.#platform.supportsMultipleCascadePaths()) {
return false;
}
if (key1.columnNames.some(col => tableDifferences.changedColumns[col]?.changedProperties.has('type'))) {
return true;
}
const defaultRule = ['restrict', 'no action'];
const rule = (key, method) => {
return (key[method] ?? defaultRule[0]).toLowerCase().replace(defaultRule[1], defaultRule[0]).replace(/"/g, '');
};
const compare = method => rule(key1, method) === rule(key2, method);
// Skip updateRule comparison for platforms that don't support ON UPDATE (e.g., Oracle)
const updateRuleDiffers = this.#platform.supportsOnUpdate() && !compare('updateRule');
return updateRuleDiffers || !compare('deleteRule');
}
/**
* Returns the difference between the columns
*/
diffColumn(fromColumn, toColumn, fromTable, logging) {
const changedProperties = new Set();
const fromProp = this.mapColumnToProperty({ ...fromColumn, autoincrement: false });
const toProp = this.mapColumnToProperty({ ...toColumn, autoincrement: false });
const fromColumnType = this.#platform.normalizeColumnType(
fromColumn.mappedType.getColumnType(fromProp, this.#platform).toLowerCase(),
fromProp,
);
const fromNativeEnum =
fromTable.nativeEnums[fromColumnType] ??
Object.values(fromTable.nativeEnums).find(e => e.name === fromColumnType && e.schema !== '*');
let toColumnType = this.#platform.normalizeColumnType(
toColumn.mappedType.getColumnType(toProp, this.#platform).toLowerCase(),
toProp,
);
const log = (msg, params) => {
if (logging) {
const copy = Utils.copy(params);
Utils.dropUndefinedProperties(copy);
this.log(msg, copy);
}
};
if (
fromColumnType !== toColumnType &&
(!fromNativeEnum || `${fromNativeEnum.schema}.${fromNativeEnum.name}` !== toColumnType) &&
!(fromColumn.ignoreSchemaChanges?.includes('type') || toColumn.ignoreSchemaChanges?.includes('type')) &&
!fromColumn.generated &&
!toColumn.generated
) {
if (
!toColumnType.includes('.') &&
fromTable.schema &&
fromTable.schema !== this.#platform.getDefaultSchemaName()
) {
toColumnType = `${fromTable.schema}.${toColumnType}`;
}
if (fromColumnType !== toColumnType) {
log(`'type' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumnType, toColumnType });
changedProperties.add('type');
}
}
if (!!fromColumn.nullable !== !!toColumn.nullable && !fromColumn.generated && !toColumn.generated) {
log(`'nullable' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('nullable');
}
if (this.diffExpression(fromColumn.generated, toColumn.generated)) {
log(`'generated' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('generated');
}
if (!!fromColumn.autoincrement !== !!toColumn.autoincrement) {
log(`'autoincrement' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('autoincrement');
}
if (!!fromColumn.unsigned !== !!toColumn.unsigned && this.#platform.supportsUnsigned()) {
log(`'unsigned' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('unsigned');
}
if (
!(fromColumn.ignoreSchemaChanges?.includes('default') || toColumn.ignoreSchemaChanges?.includes('default')) &&
!this.hasSameDefaultValue(fromColumn, toColumn)
) {
log(`'default' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('default');
}
if (this.diffComment(fromColumn.comment, toColumn.comment)) {
log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('comment');
}
if (
!(fromColumn.mappedType instanceof ArrayType) &&
!(toColumn.mappedType instanceof ArrayType) &&
this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)
) {
log(`'enumItems' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('enumItems');
}
if (
(fromColumn.extra || '').toLowerCase() !== (toColumn.extra || '').toLowerCase() &&
!(fromColumn.ignoreSchemaChanges?.includes('extra') || toColumn.ignoreSchemaChanges?.includes('extra'))
) {
log(`'extra' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
changedProperties.add('extra');
}
return changedProperties;
}
diffEnumItems(items1 = [], items2 = []) {
return items1.length !== items2.length || items1.some((v, i) => v !== items2[i]);
}
diffComment(comment1, comment2) {
// A null value and an empty string are actually equal for a comment so they should not trigger a change.
// eslint-disable-next-line eqeqeq
return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
}
/**
* Finds the difference between the indexes index1 and index2.
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
*/
diffIndex(index1, index2) {
// if one of them is a custom expression or full text index, compare only by name
if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
return index1.keyName !== index2.keyName;
}
return !this.isIndexFulfilledBy(index1, index2) || !this.isIndexFulfilledBy(index2, index1);
}
/**
* Checks if the other index already fulfills all the indexing and constraint needs of the current one.
*/
isIndexFulfilledBy(index1, index2) {
// allow the other index to be equally large only. It being larger is an option but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
if (index1.columnNames.length !== index2.columnNames.length) {
return false;
}
function spansColumns() {
for (let i = 0; i < index1.columnNames.length; i++) {
if (index1.columnNames[i] === index2.columnNames[i]) {
continue;
}
return false;
}
return true;
}
// Check if columns are the same, and even in the same order
if (!spansColumns()) {
return false;
}
// Compare advanced column options (sort order, nulls, length, collation)
if (!this.compareIndexColumns(index1, index2)) {
return false;
}
// Compare INCLUDE columns for covering indexes
if (!this.compareArrays(index1.include, index2.include)) {
return false;
}
// Compare fill factor
if (index1.fillFactor !== index2.fillFactor) {
return false;
}
// Compare invisible flag
if (!!index1.invisible !== !!index2.invisible) {
return false;
}
// Compare disabled flag
if (!!index1.disabled !== !!index2.disabled) {
return false;
}
// Compare clustered flag
if (!!index1.clustered !== !!index2.clustered) {
return false;
}
if (!index1.unique && !index1.primary) {
// this is a special case: If the current key is neither primary or unique, any unique or
// primary key will always have the same effect for the index and there cannot be any constraint
// overlaps. This means a primary or unique index can always fulfill the requirements of just an
// index that has no constraints.
return true;
}
if (this.#platform.supportsDeferredUniqueConstraints() && index1.deferMode !== index2.deferMode) {
return false;
}
return index1.primary === index2.primary && index1.unique === index2.unique;
}
/**
* Compare advanced column options between two indexes.
*/
compareIndexColumns(index1, index2) {
const cols1 = index1.columns ?? [];
const cols2 = index2.columns ?? [];
// If neither has column options, they match
if (cols1.length === 0 && cols2.length === 0) {
return true;
}
// If only one has column options, they don't match
if (cols1.length !== cols2.length) {
return false;
}
// Compare each column's options
// Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
// and the columns array is derived from those same column names
for (let i = 0; i < cols1.length; i++) {
const c1 = cols1[i];
const c2 = cols2[i];
const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
if (sort1 !== sort2) {
return false;
}
const defaultNulls = s => (s === 'DESC' ? 'FIRST' : 'LAST');
const nulls1 = c1.nulls?.toUpperCase() ?? defaultNulls(sort1);
const nulls2 = c2.nulls?.toUpperCase() ?? defaultNulls(sort2);
if (nulls1 !== nulls2) {
return false;
}
if (c1.length !== c2.length) {
return false;
}
if (c1.collation !== c2.collation) {
return false;
}
}
return true;
}
/**
* Compare two arrays for equality (order matters).
*/
compareArrays(arr1, arr2) {
if (!arr1 && !arr2) {
return true;
}
if (!arr1 || !arr2 || arr1.length !== arr2.length) {
return false;
}
return arr1.every((val, i) => val === arr2[i]);
}
diffExpression(expr1, expr2) {
// expressions like check constraints might be normalized by the driver,
// e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
const simplify = str => {
return (
str
?.replace(/_\w+'(.*?)'/g, '$1')
.replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
// MySQL normalizes count(*) to count(0)
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
// Remove quotes first so we can process identifiers
.replace(/['"`]/g, '')
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
// Strip these prefixes - match word.word patterns and keep only the last part
.replace(/\b\w+\.(\w+)/g, '$1')
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
.replace(/\binner\s+join\b/gi, 'join')
// Remove redundant column aliases like `title AS title` -> `title`
.replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
// Remove AS keyword (optional in SQL, MySQL may add/remove it)
.replace(/\bas\b/gi, '')
// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
.replace(/[()\n[\]*]|::\w+| +/g, '')
.replace(/anyarray\[(.*)]/gi, '$1')
.toLowerCase()
// PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
// After removing AS and whitespace, this results in duplicate adjacent words
// Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
// Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
.replace(/(\w{3,})\1/g, '$1')
// Remove trailing semicolon (PostgreSQL adds it to view definitions)
.replace(/;$/, '')
);
};
return simplify(expr1) !== simplify(expr2);
}
parseJsonDefault(defaultValue) {
/* v8 ignore next */
if (!defaultValue) {
return null;
}
const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
return parseJsonSafe(val);
}
hasSameDefaultValue(from, to) {
if (
from.default == null ||
from.default.toString().toLowerCase() === 'null' ||
from.default.toString().startsWith('nextval(')
) {
return to.default == null || to.default.toLowerCase() === 'null';
}
if (to.mappedType instanceof BooleanType) {
const defaultValueFrom = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + from.default);
const defaultValueTo = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + to.default);
return defaultValueFrom === defaultValueTo;
}
if (to.mappedType instanceof JsonType) {
const defaultValueFrom = this.parseJsonDefault(from.default);
const defaultValueTo = this.parseJsonDefault(to.default);
return Utils.equals(defaultValueFrom, defaultValueTo);
}
if (to.mappedType instanceof DateTimeType && from.default && to.default) {
// normalize now/current_timestamp defaults, also remove `()` from the end of default expression
const defaultValueFrom = from.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
return defaultValueFrom === defaultValueTo;
}
if (from.default && to.default) {
return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
}
if (['', this.#helper.getDefaultEmptyString()].includes(to.default) && from.default != null) {
return ['', this.#helper.getDefaultEmptyString()].includes(from.default.toString());
}
// eslint-disable-next-line eqeqeq
return from.default == to.default; // == intentionally
}
mapColumnToProperty(column) {
const length = /\w+\((\d+)\)/.exec(column.type);
const match = /\w+\((\d+), ?(\d+)\)/.exec(column.type);
return {
fieldNames: [column.name],
columnTypes: [column.type],
items: column.enumItems,
...column,
length: length ? +length[1] : column.length,
precision: match ? +match[1] : column.precision,
scale: match ? +match[2] : column.scale,
};
}
log(message, params) {
if (params) {
message += ' ' + inspect(params);
}
this.#logger.log('schema', message);
}
}

118
node_modules/@mikro-orm/sql/schema/SchemaHelper.d.ts generated vendored Normal file
View File

@@ -0,0 +1,118 @@
import { type Connection, type Dictionary, type Options, RawQueryFragment } from '@mikro-orm/core';
import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../typings.js';
import type { DatabaseSchema } from './DatabaseSchema.js';
import type { DatabaseTable } from './DatabaseTable.js';
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
export declare abstract class SchemaHelper {
protected readonly platform: AbstractSqlPlatform;
constructor(platform: AbstractSqlPlatform);
/** Returns SQL to prepend to schema migration scripts (e.g., disabling FK checks). */
getSchemaBeginning(_charset: string, disableForeignKeys?: boolean): string;
/** Returns SQL to disable foreign key checks. */
disableForeignKeysSQL(): string;
/** Returns SQL to re-enable foreign key checks. */
enableForeignKeysSQL(): string;
/** Returns SQL to append to schema migration scripts (e.g., re-enabling FK checks). */
getSchemaEnd(disableForeignKeys?: boolean): string;
finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
appendComments(table: DatabaseTable): string[];
supportsSchemaConstraints(): boolean;
getPrimaryKeys(
connection: AbstractSqlConnection,
indexes: IndexDef[] | undefined,
tableName: string,
schemaName?: string,
): Promise<string[]>;
inferLengthFromColumnType(type: string): number | undefined;
protected getTableKey(t: Table): string;
getCreateNativeEnumSQL(name: string, values: unknown[], schema?: string): string;
getDropNativeEnumSQL(name: string, schema?: string): string;
getAlterNativeEnumSQL(name: string, schema?: string, value?: string, items?: string[], oldItems?: string[]): string;
/** Loads table metadata (columns, indexes, foreign keys) from the database information schema. */
abstract loadInformationSchema(
schema: DatabaseSchema,
connection: AbstractSqlConnection,
tables: Table[],
schemas?: string[],
): Promise<void>;
/** Returns the SQL query to list all tables in the database. */
getListTablesSQL(): string;
/** Retrieves all tables from the database. */
getAllTables(connection: AbstractSqlConnection, schemas?: string[]): Promise<Table[]>;
getListViewsSQL(): string;
loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
/** Returns SQL to rename a column in a table. */
getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column, schemaName?: string): string;
/** Returns SQL to create an index on a table. */
getCreateIndexSQL(tableName: string, index: IndexDef): string;
/**
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
*/
protected getCreateIndexSuffix(_index: IndexDef): string;
/**
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
*/
protected getIndexColumns(index: IndexDef): string;
/** Returns SQL to drop an index. */
getDropIndexSQL(tableName: string, index: IndexDef): string;
getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
/** Returns SQL statements to apply a table difference (add/drop/alter columns, indexes, foreign keys). */
alterTable(diff: TableDifference, safe?: boolean): string[];
/** Returns SQL to add columns to an existing table. */
getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[];
getDropColumnsSQL(tableName: string, columns: Column[], schemaName?: string): string;
hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
castColumn(name: string, type: string): string;
alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
getNamespaces(connection: AbstractSqlConnection): Promise<string[]>;
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
normalizeDefaultValue(
defaultValue: string | RawQueryFragment,
length?: number,
defaultValues?: Dictionary<string[]>,
): string | number;
getCreateDatabaseSQL(name: string): string;
getDropDatabaseSQL(name: string): string;
getCreateNamespaceSQL(name: string): string;
getDropNamespaceSQL(name: string): string;
getDatabaseExistsSQL(name: string): string;
getDatabaseNotExistsError(dbName: string): string;
getManagementDbName(): string;
getDefaultEmptyString(): string;
databaseExists(connection: Connection, name: string): Promise<boolean>;
append(array: string[], sql: string | string[], pad?: boolean): void;
/** Returns SQL statements to create a table with all its columns, primary key, indexes, and checks. */
createTable(table: DatabaseTable, alter?: boolean): string[];
alterTableComment(table: DatabaseTable, comment?: string): string;
/** Returns SQL to create a foreign key constraint on a table. */
createForeignKey(table: DatabaseTable, foreignKey: ForeignKey, alterTable?: boolean, inline?: boolean): string;
splitTableName(name: string, skipDefaultSchema?: boolean): [string | undefined, string];
getReferencedTableName(referencedTableName: string, schema?: string): string;
createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
createCheck(table: DatabaseTable, check: CheckDef): string;
protected getTableName(table: string, schema?: string): string;
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
get options(): NonNullable<Options['schemaGenerator']>;
protected processComment(comment: string): string;
protected quote(...keys: (string | undefined)[]): string;
dropForeignKey(tableName: string, constraintName: string): string;
dropIndex(table: string, index: IndexDef, oldIndexName?: string): string;
dropConstraint(table: string, name: string): string;
/** Returns SQL to drop a table if it exists. */
dropTableIfExists(name: string, schema?: string): string;
createView(name: string, schema: string | undefined, definition: string): string;
dropViewIfExists(name: string, schema?: string): string;
createMaterializedView(name: string, schema: string | undefined, definition: string, withData?: boolean): string;
dropMaterializedViewIfExists(name: string, schema?: string): string;
refreshMaterializedView(name: string, schema?: string, concurrently?: boolean): string;
getListMaterializedViewsSQL(): string;
loadMaterializedViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string): Promise<void>;
}

684
node_modules/@mikro-orm/sql/schema/SchemaHelper.js generated vendored Normal file
View File

@@ -0,0 +1,684 @@
import { RawQueryFragment, Utils } from '@mikro-orm/core';
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
export class SchemaHelper {
platform;
constructor(platform) {
this.platform = platform;
}
/** Returns SQL to prepend to schema migration scripts (e.g., disabling FK checks). */
getSchemaBeginning(_charset, disableForeignKeys) {
if (disableForeignKeys) {
return `${this.disableForeignKeysSQL()}\n`;
}
return '';
}
/** Returns SQL to disable foreign key checks. */
disableForeignKeysSQL() {
return '';
}
/** Returns SQL to re-enable foreign key checks. */
enableForeignKeysSQL() {
return '';
}
/** Returns SQL to append to schema migration scripts (e.g., re-enabling FK checks). */
getSchemaEnd(disableForeignKeys) {
if (disableForeignKeys) {
return `${this.enableForeignKeysSQL()}\n`;
}
return '';
}
finalizeTable(table, charset, collate) {
return '';
}
appendComments(table) {
return [];
}
supportsSchemaConstraints() {
return true;
}
async getPrimaryKeys(connection, indexes = [], tableName, schemaName) {
const pks = indexes.filter(i => i.primary).map(pk => pk.columnNames);
return Utils.flatten(pks);
}
inferLengthFromColumnType(type) {
const match = /^\w+\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
if (!match) {
return;
}
return +match[1];
}
getTableKey(t) {
const unquote = str => str.replace(/['"`]/g, '');
const parts = t.table_name.split('.');
if (parts.length > 1) {
return `${unquote(parts[0])}.${unquote(parts[1])}`;
}
if (t.schema_name) {
return `${unquote(t.schema_name)}.${unquote(t.table_name)}`;
}
return unquote(t.table_name);
}
getCreateNativeEnumSQL(name, values, schema) {
throw new Error('Not supported by given driver');
}
getDropNativeEnumSQL(name, schema) {
throw new Error('Not supported by given driver');
}
getAlterNativeEnumSQL(name, schema, value, items, oldItems) {
throw new Error('Not supported by given driver');
}
/** Returns the SQL query to list all tables in the database. */
getListTablesSQL() {
throw new Error('Not supported by given driver');
}
/** Retrieves all tables from the database. */
async getAllTables(connection, schemas) {
return connection.execute(this.getListTablesSQL());
}
getListViewsSQL() {
throw new Error('Not supported by given driver');
}
async loadViews(schema, connection, schemaName) {
throw new Error('Not supported by given driver');
}
/** Returns SQL to rename a column in a table. */
getRenameColumnSQL(tableName, oldColumnName, to, schemaName) {
tableName = this.quote(tableName);
oldColumnName = this.quote(oldColumnName);
const columnName = this.quote(to.name);
const schemaReference = schemaName !== undefined && schemaName !== 'public' ? '"' + schemaName + '".' : '';
const tableReference = schemaReference + tableName;
return `alter table ${tableReference} rename column ${oldColumnName} to ${columnName}`;
}
/** Returns SQL to create an index on a table. */
getCreateIndexSQL(tableName, index) {
/* v8 ignore next */
if (index.expression) {
return index.expression;
}
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${index.keyName}'`);
}
tableName = this.quote(tableName);
const keyName = this.quote(index.keyName);
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
if (index.unique && index.constraint) {
sql = `alter table ${tableName} add constraint ${keyName} unique`;
}
if (index.columnNames.some(column => column.includes('.'))) {
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
const columns = this.platform.getJsonIndexDefinition(index);
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${defer}`;
}
// Build column list with advanced options
const columns = this.getIndexColumns(index);
sql += ` (${columns})`;
// Add INCLUDE clause for covering indexes (PostgreSQL, MSSQL)
if (index.include?.length) {
sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
}
return sql + this.getCreateIndexSuffix(index) + defer;
}
/**
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
*/
getCreateIndexSuffix(_index) {
return '';
}
/**
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
*/
getIndexColumns(index) {
if (index.columns?.length) {
return index.columns
.map(col => {
let colDef = this.quote(col.name);
// Collation comes after column name (SQLite syntax: column COLLATE name)
if (col.collation) {
colDef += ` collate ${col.collation}`;
}
// Sort order
if (col.sort) {
colDef += ` ${col.sort}`;
}
// NULLS ordering (PostgreSQL)
if (col.nulls) {
colDef += ` nulls ${col.nulls}`;
}
return colDef;
})
.join(', ');
}
return index.columnNames.map(c => this.quote(c)).join(', ');
}
/** Returns SQL to drop an index. */
getDropIndexSQL(tableName, index) {
return `drop index ${this.quote(index.keyName)}`;
}
getRenameIndexSQL(tableName, index, oldIndexName) {
return [
this.getDropIndexSQL(tableName, { ...index, keyName: oldIndexName }),
this.getCreateIndexSQL(tableName, index),
];
}
/** Returns SQL statements to apply a table difference (add/drop/alter columns, indexes, foreign keys). */
alterTable(diff, safe) {
const ret = [];
const [schemaName, tableName] = this.splitTableName(diff.name);
if (this.platform.supportsNativeEnums()) {
const changedNativeEnums = [];
for (const { column, changedProperties } of Object.values(diff.changedColumns)) {
if (!column.nativeEnumName) {
continue;
}
const key =
schemaName && schemaName !== this.platform.getDefaultSchemaName() && !column.nativeEnumName.includes('.')
? schemaName + '.' + column.nativeEnumName
: column.nativeEnumName;
if (changedProperties.has('enumItems') && key in diff.fromTable.nativeEnums) {
changedNativeEnums.push([column.nativeEnumName, column.enumItems, diff.fromTable.nativeEnums[key].items]);
}
}
Utils.removeDuplicates(changedNativeEnums).forEach(([enumName, itemsNew, itemsOld]) => {
// postgres allows only adding new items
const newItems = itemsNew.filter(val => !itemsOld.includes(val));
if (enumName.includes('.')) {
const [enumSchemaName, rawEnumName] = enumName.split('.');
ret.push(
...newItems.map(val => this.getAlterNativeEnumSQL(rawEnumName, enumSchemaName, val, itemsNew, itemsOld)),
);
return;
}
ret.push(...newItems.map(val => this.getAlterNativeEnumSQL(enumName, schemaName, val, itemsNew, itemsOld)));
});
}
for (const index of Object.values(diff.removedIndexes)) {
ret.push(this.dropIndex(diff.name, index));
}
for (const index of Object.values(diff.changedIndexes)) {
ret.push(this.dropIndex(diff.name, index));
}
for (const check of Object.values(diff.removedChecks)) {
ret.push(this.dropConstraint(diff.name, check.name));
}
for (const check of Object.values(diff.changedChecks)) {
ret.push(this.dropConstraint(diff.name, check.name));
}
/* v8 ignore next */
if (!safe && Object.values(diff.removedColumns).length > 0) {
ret.push(this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName));
}
if (Object.values(diff.addedColumns).length > 0) {
this.append(ret, this.getAddColumnsSQL(diff.toTable, Object.values(diff.addedColumns)));
}
for (const column of Object.values(diff.addedColumns)) {
const foreignKey = Object.values(diff.addedForeignKeys).find(
fk => fk.columnNames.length === 1 && fk.columnNames[0] === column.name,
);
if (foreignKey && this.options.createForeignKeyConstraints) {
delete diff.addedForeignKeys[foreignKey.constraintName];
ret.push(this.createForeignKey(diff.toTable, foreignKey));
}
}
for (const { column, changedProperties } of Object.values(diff.changedColumns)) {
if (changedProperties.size === 1 && changedProperties.has('comment')) {
continue;
}
if (changedProperties.size === 1 && changedProperties.has('enumItems') && column.nativeEnumName) {
continue;
}
this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
}
for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff =>
diff.changedProperties.has('comment'),
)) {
if (
['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems'].some(t => changedProperties.has(t))
) {
continue; // will be handled via column update
}
ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
}
for (const [oldColumnName, column] of Object.entries(diff.renamedColumns)) {
ret.push(this.getRenameColumnSQL(tableName, oldColumnName, column, schemaName));
}
for (const foreignKey of Object.values(diff.addedForeignKeys)) {
ret.push(this.createForeignKey(diff.toTable, foreignKey));
}
for (const foreignKey of Object.values(diff.changedForeignKeys)) {
ret.push(this.createForeignKey(diff.toTable, foreignKey));
}
for (const index of Object.values(diff.addedIndexes)) {
ret.push(this.createIndex(index, diff.toTable));
}
for (const index of Object.values(diff.changedIndexes)) {
ret.push(this.createIndex(index, diff.toTable, true));
}
for (const [oldIndexName, index] of Object.entries(diff.renamedIndexes)) {
if (index.unique) {
ret.push(this.dropIndex(diff.name, index, oldIndexName));
ret.push(this.createIndex(index, diff.toTable));
} else {
ret.push(...this.getRenameIndexSQL(diff.name, index, oldIndexName));
}
}
for (const check of Object.values(diff.addedChecks)) {
ret.push(this.createCheck(diff.toTable, check));
}
for (const check of Object.values(diff.changedChecks)) {
ret.push(this.createCheck(diff.toTable, check));
}
if ('changedComment' in diff) {
ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
}
return ret;
}
/** Returns SQL to add columns to an existing table. */
getAddColumnsSQL(table, columns) {
const adds = columns
.map(column => {
return `add ${this.createTableColumn(column, table)}`;
})
.join(', ');
return [`alter table ${table.getQuotedName()} ${adds}`];
}
getDropColumnsSQL(tableName, columns, schemaName) {
const name = this.quote(this.getTableName(tableName, schemaName));
const drops = columns.map(column => `drop column ${this.quote(column.name)}`).join(', ');
return `alter table ${name} ${drops}`;
}
hasNonDefaultPrimaryKeyName(table) {
const pkIndex = table.getPrimaryKey();
if (!pkIndex || !this.platform.supportsCustomPrimaryKeyNames()) {
return false;
}
const defaultName = this.platform.getDefaultPrimaryName(table.name, pkIndex.columnNames);
return pkIndex?.keyName !== defaultName;
}
/* v8 ignore next */
castColumn(name, type) {
return '';
}
alterTableColumn(column, table, changedProperties) {
const sql = [];
if (changedProperties.has('default') && column.default == null) {
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} drop default`);
}
if (changedProperties.has('type')) {
let type = column.type + (column.generated ? ` generated always as ${column.generated}` : '');
if (column.nativeEnumName) {
type = this.quote(this.getTableName(type, table.schema));
}
sql.push(
`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + this.castColumn(column.name, type)}`,
);
}
if (changedProperties.has('default') && column.default != null) {
sql.push(
`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} set default ${column.default}`,
);
}
if (changedProperties.has('nullable')) {
const action = column.nullable ? 'drop' : 'set';
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} ${action} not null`);
}
return sql;
}
createTableColumn(column, table, changedProperties) {
const compositePK = table.getPrimaryKey()?.composite;
const primaryKey = !changedProperties && !this.hasNonDefaultPrimaryKeyName(table);
const columnType = column.type + (column.generated ? ` generated always as ${column.generated}` : '');
const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement;
const col = [this.quote(column.name), columnType];
Utils.runIfNotEmpty(() => col.push('unsigned'), column.unsigned && this.platform.supportsUnsigned());
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
Utils.runIfNotEmpty(() => col.push('auto_increment'), column.autoincrement);
Utils.runIfNotEmpty(() => col.push('unique'), column.autoincrement && !column.primary);
if (
column.autoincrement &&
!column.generated &&
!compositePK &&
(!changedProperties || changedProperties.has('autoincrement') || changedProperties.has('type'))
) {
Utils.runIfNotEmpty(() => col.push('primary key'), primaryKey && column.primary);
}
if (useDefault) {
// https://dev.mysql.com/doc/refman/9.0/en/data-type-defaults.html
const needsExpression = [
'blob',
'text',
'json',
'point',
'linestring',
'polygon',
'multipoint',
'multilinestring',
'multipolygon',
'geometrycollection',
].some(type => column.type.toLowerCase().startsWith(type));
const defaultSql = needsExpression && !column.default.startsWith('(') ? `(${column.default})` : column.default;
col.push(`default ${defaultSql}`);
}
Utils.runIfNotEmpty(() => col.push(column.extra), column.extra);
Utils.runIfNotEmpty(() => col.push(`comment ${this.platform.quoteValue(column.comment)}`), column.comment);
return col.join(' ');
}
getPreAlterTable(tableDiff, safe) {
return [];
}
getPostAlterTable(tableDiff, safe) {
return [];
}
getChangeColumnCommentSQL(tableName, to, schemaName) {
return '';
}
async getNamespaces(connection) {
return [];
}
async mapIndexes(indexes) {
const map = {};
indexes.forEach(index => {
if (map[index.keyName]) {
if (index.columnNames.length > 0) {
map[index.keyName].composite = true;
map[index.keyName].columnNames.push(index.columnNames[0]);
}
// Merge columns array for advanced column options (sort, length, collation, etc.)
if (index.columns?.length) {
map[index.keyName].columns ??= [];
map[index.keyName].columns.push(index.columns[0]);
}
// Merge INCLUDE columns
if (index.include?.length) {
map[index.keyName].include ??= [];
map[index.keyName].include.push(index.include[0]);
}
} else {
map[index.keyName] = index;
}
});
return Object.values(map);
}
mapForeignKeys(fks, tableName, schemaName) {
return fks.reduce((ret, fk) => {
if (ret[fk.constraint_name]) {
ret[fk.constraint_name].columnNames.push(fk.column_name);
ret[fk.constraint_name].referencedColumnNames.push(fk.referenced_column_name);
} else {
ret[fk.constraint_name] = {
columnNames: [fk.column_name],
constraintName: fk.constraint_name,
localTableName: schemaName ? `${schemaName}.${tableName}` : tableName,
referencedTableName: fk.referenced_schema_name
? `${fk.referenced_schema_name}.${fk.referenced_table_name}`
: fk.referenced_table_name,
referencedColumnNames: [fk.referenced_column_name],
updateRule: fk.update_rule.toLowerCase(),
deleteRule: fk.delete_rule.toLowerCase(),
deferMode: fk.defer_mode,
};
}
return ret;
}, {});
}
normalizeDefaultValue(defaultValue, length, defaultValues = {}) {
if (defaultValue == null) {
return defaultValue;
}
if (defaultValue instanceof RawQueryFragment) {
return this.platform.formatQuery(defaultValue.sql, defaultValue.params);
}
const genericValue = defaultValue.replace(/\(\d+\)/, '(?)').toLowerCase();
const norm = defaultValues[genericValue];
if (!norm) {
return defaultValue;
}
return norm[0].replace('(?)', length != null ? `(${length})` : '');
}
getCreateDatabaseSQL(name) {
name = this.quote(name);
// two line breaks to force separate execution
return `create database ${name};\n\nuse ${name}`;
}
getDropDatabaseSQL(name) {
return `drop database if exists ${this.quote(name)}`;
}
/* v8 ignore next */
getCreateNamespaceSQL(name) {
return `create schema if not exists ${this.quote(name)}`;
}
/* v8 ignore next */
getDropNamespaceSQL(name) {
return `drop schema if exists ${this.quote(name)}`;
}
getDatabaseExistsSQL(name) {
return `select 1 from information_schema.schemata where schema_name = '${name}'`;
}
getDatabaseNotExistsError(dbName) {
return `Unknown database '${dbName}'`;
}
getManagementDbName() {
return 'information_schema';
}
getDefaultEmptyString() {
return "''";
}
async databaseExists(connection, name) {
try {
const res = await connection.execute(this.getDatabaseExistsSQL(name));
return res.length > 0;
} catch (e) {
if (e instanceof Error && e.message.includes(this.getDatabaseNotExistsError(name))) {
return false;
}
/* v8 ignore next */
throw e;
}
}
append(array, sql, pad = false) {
const length = array.length;
for (const row of Utils.asArray(sql)) {
if (!row) {
continue;
}
let tmp = row.trim();
if (!tmp.endsWith(';')) {
tmp += ';';
}
array.push(tmp);
}
if (pad && array.length > length) {
array.push('');
}
}
/** Returns SQL statements to create a table with all its columns, primary key, indexes, and checks. */
createTable(table, alter) {
let sql = `create table ${table.getQuotedName()} (`;
const columns = table.getColumns();
const lastColumn = columns[columns.length - 1].name;
for (const column of columns) {
const col = this.createTableColumn(column, table);
if (col) {
const comma = column.name === lastColumn ? '' : ', ';
sql += col + comma;
}
}
const primaryKey = table.getPrimaryKey();
const createPrimary =
!table.getColumns().some(c => c.autoincrement && c.primary) || this.hasNonDefaultPrimaryKeyName(table);
if (createPrimary && primaryKey) {
const name = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(primaryKey.keyName)} ` : '';
sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`;
}
sql += ')';
sql += this.finalizeTable(
table,
this.platform.getConfig().get('charset'),
this.platform.getConfig().get('collate'),
);
const ret = [];
this.append(ret, sql);
this.append(ret, this.appendComments(table));
for (const index of table.getIndexes()) {
this.append(ret, this.createIndex(index, table));
}
if (!alter) {
for (const check of table.getChecks()) {
this.append(ret, this.createCheck(table, check));
}
}
return ret;
}
alterTableComment(table, comment) {
return `alter table ${table.getQuotedName()} comment = ${this.platform.quoteValue(comment ?? '')}`;
}
/** Returns SQL to create a foreign key constraint on a table. */
createForeignKey(table, foreignKey, alterTable = true, inline = false) {
if (!this.options.createForeignKeyConstraints) {
return '';
}
const constraintName = this.quote(foreignKey.constraintName);
const columnNames = foreignKey.columnNames.map(c => this.quote(c)).join(', ');
const referencedColumnNames = foreignKey.referencedColumnNames.map(c => this.quote(c)).join(', ');
const referencedTableName = this.quote(this.getReferencedTableName(foreignKey.referencedTableName, table.schema));
const sql = [];
if (alterTable) {
sql.push(`alter table ${table.getQuotedName()} add`);
}
sql.push(`constraint ${constraintName}`);
if (!inline) {
sql.push(`foreign key (${columnNames})`);
}
sql.push(`references ${referencedTableName} (${referencedColumnNames})`);
if (foreignKey.localTableName !== foreignKey.referencedTableName || this.platform.supportsMultipleCascadePaths()) {
if (foreignKey.updateRule) {
sql.push(`on update ${foreignKey.updateRule}`);
}
if (foreignKey.deleteRule) {
sql.push(`on delete ${foreignKey.deleteRule}`);
}
}
if (foreignKey.deferMode) {
sql.push(`deferrable initially ${foreignKey.deferMode}`);
}
return sql.join(' ');
}
splitTableName(name, skipDefaultSchema = false) {
const parts = name.split('.');
const tableName = parts.pop();
let schemaName = parts.pop();
if (skipDefaultSchema && schemaName === this.platform.getDefaultSchemaName()) {
schemaName = undefined;
}
return [schemaName, tableName];
}
getReferencedTableName(referencedTableName, schema) {
const [schemaName, tableName] = this.splitTableName(referencedTableName);
schema = schemaName ?? schema ?? this.platform.getConfig().get('schema');
/* v8 ignore next */
if (schema && schemaName === '*') {
return `${schema}.${referencedTableName.replace(/^\*\./, '')}`;
}
return this.getTableName(tableName, schema);
}
createIndex(index, table, createPrimary = false) {
if (index.primary && !createPrimary) {
return '';
}
if (index.expression) {
return index.expression;
}
const columns = index.columnNames.map(c => this.quote(c)).join(', ');
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
if (index.primary) {
const keyName = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${index.keyName} ` : '';
return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`;
}
if (index.type === 'fulltext') {
const columns = index.columnNames.map(name => ({ name, type: table.getColumn(name).type }));
if (this.platform.supportsCreatingFullTextIndex()) {
return this.platform.getFullTextIndexExpression(index.keyName, table.schema, table.name, columns);
}
}
return this.getCreateIndexSQL(table.getShortestName(), index);
}
createCheck(table, check) {
return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
}
getTableName(table, schema) {
if (schema && schema !== this.platform.getDefaultSchemaName()) {
return `${schema}.${table}`;
}
return table;
}
getTablesGroupedBySchemas(tables) {
return tables.reduce((acc, table) => {
const schemaTables = acc.get(table.schema_name);
if (!schemaTables) {
acc.set(table.schema_name, [table]);
return acc;
}
schemaTables.push(table);
return acc;
}, new Map());
}
get options() {
return this.platform.getConfig().get('schemaGenerator');
}
processComment(comment) {
return comment;
}
quote(...keys) {
return this.platform.quoteIdentifier(keys.filter(Boolean).join('.'));
}
dropForeignKey(tableName, constraintName) {
return `alter table ${this.quote(tableName)} drop foreign key ${this.quote(constraintName)}`;
}
dropIndex(table, index, oldIndexName = index.keyName) {
if (index.primary) {
return `alter table ${this.quote(table)} drop primary key`;
}
return `alter table ${this.quote(table)} drop index ${this.quote(oldIndexName)}`;
}
dropConstraint(table, name) {
return `alter table ${this.quote(table)} drop constraint ${this.quote(name)}`;
}
/** Returns SQL to drop a table if it exists. */
dropTableIfExists(name, schema) {
let sql = `drop table if exists ${this.quote(this.getTableName(name, schema))}`;
if (this.platform.usesCascadeStatement()) {
sql += ' cascade';
}
return sql;
}
createView(name, schema, definition) {
const viewName = this.quote(this.getTableName(name, schema));
return `create view ${viewName} as ${definition}`;
}
dropViewIfExists(name, schema) {
let sql = `drop view if exists ${this.quote(this.getTableName(name, schema))}`;
if (this.platform.usesCascadeStatement()) {
sql += ' cascade';
}
return sql;
}
createMaterializedView(name, schema, definition, withData = true) {
throw new Error('Not supported by given driver');
}
dropMaterializedViewIfExists(name, schema) {
throw new Error('Not supported by given driver');
}
refreshMaterializedView(name, schema, concurrently = false) {
throw new Error('Not supported by given driver');
}
getListMaterializedViewsSQL() {
throw new Error('Not supported by given driver');
}
async loadMaterializedViews(schema, connection, schemaName) {
throw new Error('Not supported by given driver');
}
}

View File

@@ -0,0 +1,87 @@
import {
type ClearDatabaseOptions,
type CreateSchemaOptions,
type DropSchemaOptions,
type EnsureDatabaseOptions,
type EntityMetadata,
type ISchemaGenerator,
type MikroORM,
type Options,
type Transaction,
type UpdateSchemaOptions,
} from '@mikro-orm/core';
import { AbstractSchemaGenerator } from '@mikro-orm/core/schema';
import type { SchemaDifference } from '../typings.js';
import { DatabaseSchema } from './DatabaseSchema.js';
import type { AbstractSqlDriver } from '../AbstractSqlDriver.js';
import type { SchemaHelper } from './SchemaHelper.js';
/** Generates and manages SQL database schemas based on entity metadata. Supports create, update, and drop operations. */
export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<AbstractSqlDriver> implements ISchemaGenerator {
protected readonly helper: SchemaHelper;
protected readonly options: NonNullable<Options['schemaGenerator']>;
protected lastEnsuredDatabase?: string;
static register(orm: MikroORM): void;
create(options?: CreateSchemaOptions): Promise<void>;
/**
* Returns true if the database was created.
*/
ensureDatabase(options?: EnsureDatabaseOptions): Promise<boolean>;
getTargetSchema(schema?: string): DatabaseSchema;
protected getOrderedMetadata(schema?: string): EntityMetadata[];
getCreateSchemaSQL(options?: CreateSchemaOptions): Promise<string>;
drop(options?: DropSchemaOptions): Promise<void>;
createNamespace(name: string): Promise<void>;
dropNamespace(name: string): Promise<void>;
clear(options?: ClearDatabaseOptions): Promise<void>;
getDropSchemaSQL(options?: Omit<DropSchemaOptions, 'dropDb'>): Promise<string>;
private getSchemaName;
update(options?: UpdateSchemaOptions<DatabaseSchema>): Promise<void>;
getUpdateSchemaSQL(options?: UpdateSchemaOptions<DatabaseSchema>): Promise<string>;
getUpdateSchemaMigrationSQL(options?: UpdateSchemaOptions<DatabaseSchema>): Promise<{
up: string;
down: string;
}>;
private prepareSchemaForComparison;
diffToSQL(
schemaDiff: SchemaDifference,
options: {
wrap?: boolean;
safe?: boolean;
dropTables?: boolean;
schema?: string;
},
): string;
/**
* We need to drop foreign keys first for all tables to allow dropping PK constraints.
*/
private preAlterTable;
/**
* creates new database and connects to it
*/
createDatabase(
name?: string,
options?: {
skipOnConnect?: boolean;
},
): Promise<void>;
dropDatabase(name?: string): Promise<void>;
execute(
sql: string,
options?: {
wrap?: boolean;
ctx?: Transaction;
},
): Promise<void>;
dropTableIfExists(name: string, schema?: string): Promise<void>;
private wrapSchema;
private append;
private matchName;
private isTableSkipped;
/**
* Sorts views by their dependencies so that views depending on other views are created after their dependencies.
* Uses topological sort based on view definition string matching.
*/
private sortViewsByDependencies;
private escapeRegExp;
}
export { SqlSchemaGenerator as SchemaGenerator };

View File

@@ -0,0 +1,553 @@
import { CommitOrderCalculator, TableNotFoundException, Utils } from '@mikro-orm/core';
import { AbstractSchemaGenerator } from '@mikro-orm/core/schema';
import { DatabaseSchema } from './DatabaseSchema.js';
import { SchemaComparator } from './SchemaComparator.js';
/** Generates and manages SQL database schemas based on entity metadata. Supports create, update, and drop operations. */
export class SqlSchemaGenerator extends AbstractSchemaGenerator {
helper = this.platform.getSchemaHelper();
options = this.config.get('schemaGenerator');
lastEnsuredDatabase;
static register(orm) {
orm.config.registerExtension('@mikro-orm/schema-generator', () => new SqlSchemaGenerator(orm.em));
}
async create(options) {
await this.ensureDatabase();
const sql = await this.getCreateSchemaSQL(options);
await this.execute(sql);
}
/**
* Returns true if the database was created.
*/
async ensureDatabase(options) {
await this.connection.ensureConnection();
const dbName = this.config.get('dbName');
if (this.lastEnsuredDatabase === dbName && !options?.forceCheck) {
return true;
}
const exists = await this.helper.databaseExists(this.connection, dbName);
this.lastEnsuredDatabase = dbName;
if (!exists) {
const managementDbName = this.helper.getManagementDbName();
if (managementDbName) {
this.config.set('dbName', managementDbName);
await this.driver.reconnect({ skipOnConnect: true });
await this.createDatabase(dbName, { skipOnConnect: true });
}
if (options?.create) {
await this.create(options);
}
return true;
}
/* v8 ignore next */
if (options?.clear) {
await this.clear({ ...options, clearIdentityMap: false });
}
return false;
}
getTargetSchema(schema) {
const metadata = this.getOrderedMetadata(schema);
const schemaName = schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
return DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName, this.em);
}
getOrderedMetadata(schema) {
const metadata = super.getOrderedMetadata(schema);
// Filter out skipped tables
return metadata.filter(meta => {
const tableName = meta.tableName;
const tableSchema = meta.schema ?? schema ?? this.config.get('schema');
return !this.isTableSkipped(tableName, tableSchema);
});
}
async getCreateSchemaSQL(options = {}) {
const toSchema = this.getTargetSchema(options.schema);
const ret = [];
for (const namespace of toSchema.getNamespaces()) {
if (namespace === this.platform.getDefaultSchemaName()) {
continue;
}
const sql = this.helper.getCreateNamespaceSQL(namespace);
this.append(ret, sql);
}
if (this.platform.supportsNativeEnums()) {
const created = [];
for (const [enumName, enumOptions] of Object.entries(toSchema.getNativeEnums())) {
/* v8 ignore next */
if (created.includes(enumName)) {
continue;
}
created.push(enumName);
const sql = this.helper.getCreateNativeEnumSQL(
enumOptions.name,
enumOptions.items,
this.getSchemaName(enumOptions, options),
);
this.append(ret, sql);
}
}
for (const table of toSchema.getTables()) {
this.append(ret, this.helper.createTable(table), true);
}
if (this.helper.supportsSchemaConstraints()) {
for (const table of toSchema.getTables()) {
const fks = Object.values(table.getForeignKeys()).map(fk => this.helper.createForeignKey(table, fk));
this.append(ret, fks, true);
}
}
// Create views after tables (views may depend on tables)
// Sort views by dependencies (views depending on other views come later)
const sortedViews = this.sortViewsByDependencies(toSchema.getViews());
for (const view of sortedViews) {
if (view.materialized) {
this.append(
ret,
this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true),
);
} else {
this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
}
}
return this.wrapSchema(ret, options);
}
async drop(options = {}) {
if (options.dropDb) {
const name = this.config.get('dbName');
return this.dropDatabase(name);
}
const sql = await this.getDropSchemaSQL(options);
await this.execute(sql);
}
async createNamespace(name) {
const sql = this.helper.getCreateNamespaceSQL(name);
await this.execute(sql);
}
async dropNamespace(name) {
const sql = this.helper.getDropNamespaceSQL(name);
await this.execute(sql);
}
async clear(options) {
// truncate by default, so no value is considered as true
/* v8 ignore next */
if (options?.truncate === false) {
return super.clear(options);
}
if (this.options.disableForeignKeysForClear) {
await this.execute(this.helper.disableForeignKeysSQL());
}
const schema = options?.schema ?? this.config.get('schema', this.platform.getDefaultSchemaName());
for (const meta of this.getOrderedMetadata(schema).reverse()) {
try {
await this.driver
.createQueryBuilder(meta.class, this.em?.getTransactionContext(), 'write', false)
.withSchema(schema)
.truncate()
.execute();
} catch (e) {
if (this.platform.getExceptionConverter().convertException(e) instanceof TableNotFoundException) {
continue;
}
throw e;
}
}
if (this.options.disableForeignKeysForClear) {
await this.execute(this.helper.enableForeignKeysSQL());
}
if (options?.clearIdentityMap ?? true) {
this.clearIdentityMap();
}
}
async getDropSchemaSQL(options = {}) {
await this.ensureDatabase();
const metadata = this.getOrderedMetadata(options.schema).reverse();
const schemas = this.getTargetSchema(options.schema).getNamespaces();
const schema = await DatabaseSchema.create(
this.connection,
this.platform,
this.config,
options.schema,
schemas,
undefined,
this.options.skipTables,
this.options.skipViews,
);
const ret = [];
// Drop views first (views may depend on tables)
// Drop in reverse dependency order (dependent views first)
const targetSchema = this.getTargetSchema(options.schema);
const sortedViews = this.sortViewsByDependencies(targetSchema.getViews()).reverse();
for (const view of sortedViews) {
if (view.materialized) {
this.append(ret, this.helper.dropMaterializedViewIfExists(view.name, view.schema));
} else {
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
}
}
// remove FKs explicitly if we can't use a cascading statement and we don't disable FK checks (we need this for circular relations)
for (const meta of metadata) {
if (!this.platform.usesCascadeStatement() && (!this.options.disableForeignKeys || options.dropForeignKeys)) {
const table = schema.getTable(meta.tableName);
if (!table) {
continue;
}
const foreignKeys = Object.values(table.getForeignKeys());
for (const fk of foreignKeys) {
this.append(ret, this.helper.dropForeignKey(table.getShortestName(), fk.constraintName));
}
}
}
for (const meta of metadata) {
this.append(ret, this.helper.dropTableIfExists(meta.tableName, this.getSchemaName(meta, options)));
}
if (this.platform.supportsNativeEnums()) {
for (const columnName of Object.keys(schema.getNativeEnums())) {
const sql = this.helper.getDropNativeEnumSQL(columnName, options.schema ?? this.config.get('schema'));
this.append(ret, sql);
}
}
if (options.dropMigrationsTable) {
this.append(
ret,
this.helper.dropTableIfExists(this.config.get('migrations').tableName, this.config.get('schema')),
);
}
return this.wrapSchema(ret, options);
}
getSchemaName(meta, options) {
const schemaName = options.schema ?? this.config.get('schema');
/* v8 ignore next */
const resolvedName = meta.schema && meta.schema === '*' ? schemaName : (meta.schema ?? schemaName);
// skip default schema name
if (resolvedName === this.platform.getDefaultSchemaName()) {
return undefined;
}
return resolvedName;
}
async update(options = {}) {
const sql = await this.getUpdateSchemaSQL(options);
await this.execute(sql);
}
async getUpdateSchemaSQL(options = {}) {
await this.ensureDatabase();
const { fromSchema, toSchema } = await this.prepareSchemaForComparison(options);
const comparator = new SchemaComparator(this.platform);
const diffUp = comparator.compare(fromSchema, toSchema);
return this.diffToSQL(diffUp, options);
}
async getUpdateSchemaMigrationSQL(options = {}) {
if (!options.fromSchema) {
await this.ensureDatabase();
}
const { fromSchema, toSchema } = await this.prepareSchemaForComparison(options);
const comparator = new SchemaComparator(this.platform);
const diffUp = comparator.compare(fromSchema, toSchema);
const diffDown = comparator.compare(toSchema, fromSchema, diffUp);
return {
up: this.diffToSQL(diffUp, options),
down: this.platform.supportsDownMigrations() ? this.diffToSQL(diffDown, options) : '',
};
}
async prepareSchemaForComparison(options) {
options.safe ??= false;
options.dropTables ??= true;
const toSchema = this.getTargetSchema(options.schema);
const schemas = toSchema.getNamespaces();
const fromSchema =
options.fromSchema ??
(await DatabaseSchema.create(
this.connection,
this.platform,
this.config,
options.schema,
schemas,
undefined,
this.options.skipTables,
this.options.skipViews,
));
const wildcardSchemaTables = [...this.metadata.getAll().values()]
.filter(meta => meta.schema === '*')
.map(meta => meta.tableName);
fromSchema.prune(options.schema, wildcardSchemaTables);
toSchema.prune(options.schema, wildcardSchemaTables);
return { fromSchema, toSchema };
}
diffToSQL(schemaDiff, options) {
const ret = [];
globalThis.idx = 0;
if (this.platform.supportsSchemas()) {
for (const newNamespace of schemaDiff.newNamespaces) {
const sql = this.helper.getCreateNamespaceSQL(newNamespace);
this.append(ret, sql);
}
}
if (this.platform.supportsNativeEnums()) {
for (const newNativeEnum of schemaDiff.newNativeEnums) {
const sql = this.helper.getCreateNativeEnumSQL(
newNativeEnum.name,
newNativeEnum.items,
this.getSchemaName(newNativeEnum, options),
);
this.append(ret, sql);
}
}
// Drop removed and changed views first (before modifying tables they may depend on)
// Drop in reverse dependency order (dependent views first)
if (options.dropTables && !options.safe) {
const sortedRemovedViews = this.sortViewsByDependencies(Object.values(schemaDiff.removedViews)).reverse();
for (const view of sortedRemovedViews) {
if (view.materialized) {
this.append(ret, this.helper.dropMaterializedViewIfExists(view.name, view.schema));
} else {
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
}
}
}
// Drop changed views (they will be recreated after table changes)
// Also in reverse dependency order
const changedViewsFrom = Object.values(schemaDiff.changedViews).map(v => v.from);
const sortedChangedViewsFrom = this.sortViewsByDependencies(changedViewsFrom).reverse();
for (const view of sortedChangedViewsFrom) {
if (view.materialized) {
this.append(ret, this.helper.dropMaterializedViewIfExists(view.name, view.schema));
} else {
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
}
}
if (!options.safe && this.options.createForeignKeyConstraints) {
for (const orphanedForeignKey of schemaDiff.orphanedForeignKeys) {
const [schemaName, tableName] = this.helper.splitTableName(orphanedForeignKey.localTableName, true);
/* v8 ignore next */
const name = (schemaName ? schemaName + '.' : '') + tableName;
this.append(ret, this.helper.dropForeignKey(name, orphanedForeignKey.constraintName));
}
if (schemaDiff.orphanedForeignKeys.length > 0) {
ret.push('');
}
}
for (const newTable of Object.values(schemaDiff.newTables)) {
this.append(ret, this.helper.createTable(newTable, true), true);
}
if (this.helper.supportsSchemaConstraints()) {
for (const newTable of Object.values(schemaDiff.newTables)) {
const sql = [];
if (this.options.createForeignKeyConstraints) {
const fks = Object.values(newTable.getForeignKeys()).map(fk => this.helper.createForeignKey(newTable, fk));
this.append(sql, fks);
}
for (const check of newTable.getChecks()) {
this.append(sql, this.helper.createCheck(newTable, check));
}
this.append(ret, sql, true);
}
}
if (options.dropTables && !options.safe) {
for (const table of Object.values(schemaDiff.removedTables)) {
this.append(ret, this.helper.dropTableIfExists(table.name, table.schema));
}
if (Utils.hasObjectKeys(schemaDiff.removedTables)) {
ret.push('');
}
}
for (const changedTable of Object.values(schemaDiff.changedTables)) {
this.append(ret, this.preAlterTable(changedTable, options.safe), true);
}
for (const changedTable of Object.values(schemaDiff.changedTables)) {
this.append(ret, this.helper.alterTable(changedTable, options.safe), true);
}
for (const changedTable of Object.values(schemaDiff.changedTables)) {
this.append(ret, this.helper.getPostAlterTable(changedTable, options.safe), true);
}
if (!options.safe && this.platform.supportsNativeEnums()) {
for (const removedNativeEnum of schemaDiff.removedNativeEnums) {
this.append(ret, this.helper.getDropNativeEnumSQL(removedNativeEnum.name, removedNativeEnum.schema));
}
}
if (options.dropTables && !options.safe) {
for (const removedNamespace of schemaDiff.removedNamespaces) {
const sql = this.helper.getDropNamespaceSQL(removedNamespace);
this.append(ret, sql);
}
}
// Create new views after all table changes are done
// Sort views by dependencies (views depending on other views come later)
const sortedNewViews = this.sortViewsByDependencies(Object.values(schemaDiff.newViews));
for (const view of sortedNewViews) {
if (view.materialized) {
this.append(
ret,
this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true),
);
} else {
this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
}
}
// Recreate changed views (also sorted by dependencies)
const changedViews = Object.values(schemaDiff.changedViews).map(v => v.to);
const sortedChangedViews = this.sortViewsByDependencies(changedViews);
for (const view of sortedChangedViews) {
if (view.materialized) {
this.append(
ret,
this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true),
);
} else {
this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
}
}
return this.wrapSchema(ret, options);
}
/**
* We need to drop foreign keys first for all tables to allow dropping PK constraints.
*/
preAlterTable(diff, safe) {
const ret = [];
this.append(ret, this.helper.getPreAlterTable(diff, safe));
for (const foreignKey of Object.values(diff.removedForeignKeys)) {
ret.push(this.helper.dropForeignKey(diff.toTable.getShortestName(), foreignKey.constraintName));
}
for (const foreignKey of Object.values(diff.changedForeignKeys)) {
ret.push(this.helper.dropForeignKey(diff.toTable.getShortestName(), foreignKey.constraintName));
}
return ret;
}
/**
* creates new database and connects to it
*/
async createDatabase(name, options) {
name ??= this.config.get('dbName');
const sql = this.helper.getCreateDatabaseSQL(name);
if (sql) {
await this.execute(sql);
}
this.config.set('dbName', name);
await this.driver.reconnect(options);
}
async dropDatabase(name) {
name ??= this.config.get('dbName');
this.config.set('dbName', this.helper.getManagementDbName());
await this.driver.reconnect();
await this.execute(this.helper.getDropDatabaseSQL(name));
this.config.set('dbName', name);
}
async execute(sql, options = {}) {
options.wrap ??= false;
const lines = this.wrapSchema(sql, options).split('\n');
const groups = [];
let i = 0;
for (const line of lines) {
if (line.trim() === '') {
if (groups[i]?.length > 0) {
i++;
}
continue;
}
groups[i] ??= [];
groups[i].push(line.trim());
}
if (groups.length === 0) {
return;
}
if (this.platform.supportsMultipleStatements()) {
for (const group of groups) {
const query = group.join('\n');
await this.driver.execute(query);
}
return;
}
const statements = groups.flatMap(group => {
return group
.join('\n')
.split(';\n')
.map(s => s.trim())
.filter(s => s);
});
await Utils.runSerial(statements, stmt => this.driver.execute(stmt));
}
async dropTableIfExists(name, schema) {
const sql = this.helper.dropTableIfExists(name, schema);
return this.execute(sql);
}
wrapSchema(sql, options) {
const array = Utils.asArray(sql);
if (array.length === 0) {
return '';
}
if (array[array.length - 1] === '') {
array.pop();
}
if (options.wrap === false) {
return array.join('\n') + '\n';
}
let ret = this.helper.getSchemaBeginning(this.config.get('charset'), this.options.disableForeignKeys);
ret += array.join('\n') + '\n';
ret += this.helper.getSchemaEnd(this.options.disableForeignKeys);
return ret;
}
append(array, sql, pad) {
return this.helper.append(array, sql, pad);
}
matchName(name, nameToMatch) {
return typeof nameToMatch === 'string'
? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
: nameToMatch.test(name);
}
isTableSkipped(tableName, schemaName) {
const skipTables = this.options.skipTables;
if (!skipTables || skipTables.length === 0) {
return false;
}
const fullTableName = schemaName ? `${schemaName}.${tableName}` : tableName;
return skipTables.some(pattern => this.matchName(tableName, pattern) || this.matchName(fullTableName, pattern));
}
/**
* Sorts views by their dependencies so that views depending on other views are created after their dependencies.
* Uses topological sort based on view definition string matching.
*/
sortViewsByDependencies(views) {
if (views.length <= 1) {
return views;
}
// Use CommitOrderCalculator for topological sort
const calc = new CommitOrderCalculator();
// Map views to numeric indices for the calculator
const viewToIndex = new Map();
const indexToView = new Map();
for (let i = 0; i < views.length; i++) {
viewToIndex.set(views[i], i);
indexToView.set(i, views[i]);
calc.addNode(i);
}
// Check each view's definition for references to other view names
for (const view of views) {
const definition = view.definition.toLowerCase();
const viewIndex = viewToIndex.get(view);
for (const otherView of views) {
if (otherView === view) {
continue;
}
// Check if the definition references the other view's name
// Use word boundary matching to avoid false positives
const patterns = [new RegExp(`\\b${this.escapeRegExp(otherView.name.toLowerCase())}\\b`)];
if (otherView.schema) {
patterns.push(
new RegExp(`\\b${this.escapeRegExp(`${otherView.schema}.${otherView.name}`.toLowerCase())}\\b`),
);
}
for (const pattern of patterns) {
if (pattern.test(definition)) {
// view depends on otherView, so otherView must come first
// addDependency(from, to) puts `from` before `to` in result
const otherIndex = viewToIndex.get(otherView);
calc.addDependency(otherIndex, viewIndex, 1);
break;
}
}
}
}
// Sort and map back to views
return calc.sort().map(index => indexToView.get(index));
}
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
// for back compatibility
export { SqlSchemaGenerator as SchemaGenerator };

5
node_modules/@mikro-orm/sql/schema/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export * from './DatabaseSchema.js';
export * from './DatabaseTable.js';
export * from './SqlSchemaGenerator.js';
export * from './SchemaHelper.js';
export * from './SchemaComparator.js';

5
node_modules/@mikro-orm/sql/schema/index.js generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export * from './DatabaseSchema.js';
export * from './DatabaseTable.js';
export * from './SqlSchemaGenerator.js';
export * from './SchemaHelper.js';
export * from './SchemaComparator.js';