Initial commit - Event Planner application
This commit is contained in:
83
node_modules/@mikro-orm/sql/schema/DatabaseSchema.d.ts
generated
vendored
Normal file
83
node_modules/@mikro-orm/sql/schema/DatabaseSchema.d.ts
generated
vendored
Normal 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
360
node_modules/@mikro-orm/sql/schema/DatabaseSchema.js
generated
vendored
Normal 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
105
node_modules/@mikro-orm/sql/schema/DatabaseTable.d.ts
generated
vendored
Normal 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
1022
node_modules/@mikro-orm/sql/schema/DatabaseTable.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
68
node_modules/@mikro-orm/sql/schema/SchemaComparator.d.ts
generated
vendored
Normal file
68
node_modules/@mikro-orm/sql/schema/SchemaComparator.d.ts
generated
vendored
Normal 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
753
node_modules/@mikro-orm/sql/schema/SchemaComparator.js
generated
vendored
Normal 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
118
node_modules/@mikro-orm/sql/schema/SchemaHelper.d.ts
generated
vendored
Normal 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
684
node_modules/@mikro-orm/sql/schema/SchemaHelper.js
generated
vendored
Normal 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');
|
||||
}
|
||||
}
|
||||
87
node_modules/@mikro-orm/sql/schema/SqlSchemaGenerator.d.ts
generated
vendored
Normal file
87
node_modules/@mikro-orm/sql/schema/SqlSchemaGenerator.d.ts
generated
vendored
Normal 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 };
|
||||
553
node_modules/@mikro-orm/sql/schema/SqlSchemaGenerator.js
generated
vendored
Normal file
553
node_modules/@mikro-orm/sql/schema/SqlSchemaGenerator.js
generated
vendored
Normal 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
5
node_modules/@mikro-orm/sql/schema/index.d.ts
generated
vendored
Normal 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
5
node_modules/@mikro-orm/sql/schema/index.js
generated
vendored
Normal 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';
|
||||
Reference in New Issue
Block a user