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