import { Table } from "./table/Table" import { TableColumn } from "./table/TableColumn" import { TableForeignKey } from "./table/TableForeignKey" import { TableIndex } from "./table/TableIndex" import { QueryRunner } from "../query-runner/QueryRunner" import { ColumnMetadata } from "../metadata/ColumnMetadata" import { EntityMetadata } from "../metadata/EntityMetadata" import { DataSource } from "../data-source/DataSource" import { SchemaBuilder } from "./SchemaBuilder" import { SqlInMemory } from "../driver/SqlInMemory" import { TableUtils } from "./util/TableUtils" import { TableColumnOptions } from "./options/TableColumnOptions" import { TableUnique } from "./table/TableUnique" import { TableCheck } from "./table/TableCheck" import { TableExclusion } from "./table/TableExclusion" import { View } from "./view/View" import { ViewUtils } from "./util/ViewUtils" import { DriverUtils } from "../driver/DriverUtils" import { PostgresQueryRunner } from "../driver/postgres/PostgresQueryRunner" /** * Creates complete tables schemas in the database based on the entity metadatas. * * Steps how schema is being built: * 1. load list of all tables with complete column and keys information from the db * 2. drop all (old) foreign keys that exist in the table, but does not exist in the metadata * 3. create new tables that does not exist in the db, but exist in the metadata * 4. drop all columns exist (left old) in the db table, but does not exist in the metadata * 5. add columns from metadata which does not exist in the table * 6. update all exist columns which metadata has changed * 7. update primary keys - update old and create new primary key from changed columns * 8. create foreign keys which does not exist in the table yet * 9. create indices which are missing in db yet, and drops indices which exist in the db, but does not exist in the metadata anymore */ export class RdbmsSchemaBuilder implements SchemaBuilder { readonly "@instanceof" = Symbol.for("RdbmsSchemaBuilder") /** * Used to execute schema creation queries in a single connection. */ protected queryRunner: QueryRunner private currentDatabase?: string private currentSchema?: string // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(protected connection: DataSource) {} // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Creates complete schemas for the given entity metadatas. */ async build(): Promise { this.queryRunner = this.connection.createQueryRunner() // this.connection.driver.database || this.currentDatabase; this.currentDatabase = this.connection.driver.database this.currentSchema = this.connection.driver.schema // CockroachDB implements asynchronous schema sync operations which can not been executed in transaction. // E.g. if you try to DROP column and ADD it again in the same transaction, crdb throws error. // In Spanner queries against the INFORMATION_SCHEMA can be used in a read-only transaction, // but not in a read-write transaction. const isUsingTransactions = !(this.connection.driver.options.type === "cockroachdb") && !(this.connection.driver.options.type === "spanner") && this.connection.options.migrationsTransactionMode !== "none" await this.queryRunner.beforeMigration() if (isUsingTransactions) { await this.queryRunner.startTransaction() } try { await this.createMetadataTableIfNecessary(this.queryRunner) // Flush the queryrunner table & view cache const tablePaths = this.entityToSyncMetadatas.map((metadata) => this.getTablePath(metadata), ) const viewPaths = this.viewEntityToSyncMetadatas.map((metadata) => this.getTablePath(metadata), ) await this.queryRunner.getTables(tablePaths) await this.queryRunner.getViews(viewPaths) await this.executeSchemaSyncOperationsInProperOrder() // if cache is enabled then perform cache-synchronization as well if (this.connection.queryResultCache) await this.connection.queryResultCache.synchronize( this.queryRunner, ) if (isUsingTransactions) { await this.queryRunner.commitTransaction() } } catch (error) { try { // we throw original error even if rollback thrown an error if (isUsingTransactions) { await this.queryRunner.rollbackTransaction() } } catch (rollbackError) {} throw error } finally { await this.queryRunner.afterMigration() await this.queryRunner.release() } } /** * Create the typeorm_metadata table if necessary. */ async createMetadataTableIfNecessary( queryRunner: QueryRunner, ): Promise { if ( this.viewEntityToSyncMetadatas.length > 0 || this.hasGeneratedColumns() ) { await this.createTypeormMetadataTable(queryRunner) } } /** * Returns sql queries to be executed by schema builder. */ async log(): Promise { this.queryRunner = this.connection.createQueryRunner() try { // Flush the queryrunner table & view cache const tablePaths = this.entityToSyncMetadatas.map((metadata) => this.getTablePath(metadata), ) const viewPaths = this.viewEntityToSyncMetadatas.map((metadata) => this.getTablePath(metadata), ) await this.queryRunner.getTables(tablePaths) await this.queryRunner.getViews(viewPaths) this.queryRunner.enableSqlMemory() await this.executeSchemaSyncOperationsInProperOrder() // if cache is enabled then perform cache-synchronization as well if (this.connection.queryResultCache) // todo: check this functionality await this.connection.queryResultCache.synchronize( this.queryRunner, ) return this.queryRunner.getMemorySql() } finally { // its important to disable this mode despite the fact we are release query builder // because there exist drivers which reuse same query runner. Also its important to disable // sql memory after call of getMemorySql() method because last one flushes sql memory. this.queryRunner.disableSqlMemory() await this.queryRunner.release() } } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- /** * Returns only entities that should be synced in the database. */ protected get entityToSyncMetadatas(): EntityMetadata[] { return this.connection.entityMetadatas.filter( (metadata) => metadata.synchronize && metadata.tableType !== "entity-child" && metadata.tableType !== "view", ) } /** * Returns only entities that should be synced in the database. */ protected get viewEntityToSyncMetadatas(): EntityMetadata[] { return ( this.connection.entityMetadatas .filter( (metadata) => metadata.tableType === "view" && metadata.synchronize, ) // sort views in creation order by dependencies .sort(ViewUtils.viewMetadataCmp) ) } /** * Checks if there are at least one generated column. */ protected hasGeneratedColumns(): boolean { return this.connection.entityMetadatas.some((entityMetadata) => { return entityMetadata.columns.some((column) => column.generatedType) }) } /** * Executes schema sync operations in a proper order. * Order of operations matter here. */ protected async executeSchemaSyncOperationsInProperOrder(): Promise { await this.dropOldViews() await this.dropOldForeignKeys() await this.dropOldIndices() await this.dropOldChecks() await this.dropOldExclusions() await this.dropCompositeUniqueConstraints() // await this.renameTables(); await this.renameColumns() await this.createNewTables() await this.dropRemovedColumns() await this.addNewColumns() await this.updatePrimaryKeys() await this.updateExistColumns() await this.createNewIndices() await this.createNewChecks() await this.createNewExclusions() await this.createCompositeUniqueConstraints() await this.createForeignKeys() await this.createViews() await this.createNewViewIndices() } private getTablePath( target: EntityMetadata | Table | View | TableForeignKey | string, ): string { const parsed = this.connection.driver.parseTableName(target) return this.connection.driver.buildTableName( parsed.tableName, parsed.schema || this.currentSchema, parsed.database || this.currentDatabase, ) } /** * Drops all (old) foreign keys that exist in the tables, but do not exist in the entity metadata. */ protected async dropOldForeignKeys(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue // find foreign keys that exist in the schemas but does not exist in the entity metadata const tableForeignKeysToDrop = table.foreignKeys.filter( (tableForeignKey) => { const metadataFK = metadata.foreignKeys.find( (metadataForeignKey) => tableForeignKey.name === metadataForeignKey.name && this.getTablePath(tableForeignKey) === this.getTablePath( metadataForeignKey.referencedEntityMetadata, ), ) return ( !metadataFK || (metadataFK.onDelete && metadataFK.onDelete !== tableForeignKey.onDelete) || (metadataFK.onUpdate && metadataFK.onUpdate !== tableForeignKey.onUpdate) ) }, ) if (tableForeignKeysToDrop.length === 0) continue this.connection.logger.logSchemaBuild( `dropping old foreign keys of ${ table.name }: ${tableForeignKeysToDrop .map((dbForeignKey) => dbForeignKey.name) .join(", ")}`, ) // drop foreign keys from the database await this.queryRunner.dropForeignKeys( table, tableForeignKeysToDrop, ) } } /** * Rename tables */ protected async renameTables(): Promise { // for (const metadata of this.entityToSyncMetadatas) { // const table = this.queryRunner.loadedTables.find(table => this.getTablePath(table) === this.getTablePath(metadata)); // } } /** * Renames columns. * Works if only one column per table was changed. * Changes only column name. If something besides name was changed, these changes will be ignored. */ protected async renameColumns(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue if (metadata.columns.length !== table.columns.length) continue const renamedMetadataColumns = metadata.columns .filter((c) => !c.isVirtualProperty) .filter((column) => { return !table.columns.find((tableColumn) => { return ( tableColumn.name === column.databaseName && tableColumn.type === this.connection.driver.normalizeType(column) && tableColumn.isNullable === column.isNullable && tableColumn.isUnique === this.connection.driver.normalizeIsUnique(column) ) }) }) if ( renamedMetadataColumns.length === 0 || renamedMetadataColumns.length > 1 ) continue const renamedTableColumns = table.columns.filter((tableColumn) => { return !metadata.columns.find((column) => { return ( !column.isVirtualProperty && column.databaseName === tableColumn.name && this.connection.driver.normalizeType(column) === tableColumn.type && column.isNullable === tableColumn.isNullable && this.connection.driver.normalizeIsUnique(column) === tableColumn.isUnique ) }) }) if ( renamedTableColumns.length === 0 || renamedTableColumns.length > 1 ) continue const renamedColumn = renamedTableColumns[0].clone() renamedColumn.name = renamedMetadataColumns[0].databaseName this.connection.logger.logSchemaBuild( `renaming column "${renamedTableColumns[0].name}" in "${table.name}" to "${renamedColumn.name}"`, ) await this.queryRunner.renameColumn( table, renamedTableColumns[0], renamedColumn, ) } } protected async dropOldIndices(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const dropQueries = table.indices .filter((tableIndex) => { const indexMetadata = metadata.indices.find( (index) => index.name === tableIndex.name, ) if (indexMetadata) { if (indexMetadata.synchronize === false) return false if (indexMetadata.isUnique !== tableIndex.isUnique) return true if (indexMetadata.isSpatial !== tableIndex.isSpatial) return true if ( this.connection.driver.isFullTextColumnTypeSupported() && indexMetadata.isFulltext !== tableIndex.isFulltext ) return true if ( indexMetadata.columns.length !== tableIndex.columnNames.length ) return true return !indexMetadata.columns.every( (column) => tableIndex.columnNames.indexOf( column.databaseName, ) !== -1, ) } return true }) .map(async (tableIndex) => { this.connection.logger.logSchemaBuild( `dropping an index: "${tableIndex.name}" from table ${table.name}`, ) await this.queryRunner.dropIndex(table, tableIndex) }) await Promise.all(dropQueries) } if (this.connection.options.type === "postgres") { const postgresQueryRunner: PostgresQueryRunner = < PostgresQueryRunner >this.queryRunner for (const metadata of this.viewEntityToSyncMetadatas) { const view = this.queryRunner.loadedViews.find( (view) => this.getTablePath(view) === this.getTablePath(metadata), ) if (!view) continue const dropQueries = view.indices .filter((tableIndex) => { const indexMetadata = metadata.indices.find( (index) => index.name === tableIndex.name, ) if (indexMetadata) { if (indexMetadata.synchronize === false) return false if (indexMetadata.isUnique !== tableIndex.isUnique) return true if ( indexMetadata.isSpatial !== tableIndex.isSpatial ) return true if ( this.connection.driver.isFullTextColumnTypeSupported() && indexMetadata.isFulltext !== tableIndex.isFulltext ) return true if ( indexMetadata.columns.length !== tableIndex.columnNames.length ) return true return !indexMetadata.columns.every( (column) => tableIndex.columnNames.indexOf( column.databaseName, ) !== -1, ) } return true }) .map(async (tableIndex) => { this.connection.logger.logSchemaBuild( `dropping an index: "${tableIndex.name}" from view ${view.name}`, ) await postgresQueryRunner.dropViewIndex( view, tableIndex, ) }) await Promise.all(dropQueries) } } } protected async dropOldChecks(): Promise { // Mysql does not support check constraints if ( DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql" ) return for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const oldChecks = table.checks.filter((tableCheck) => { return !metadata.checks.find( (checkMetadata) => checkMetadata.name === tableCheck.name, ) }) if (oldChecks.length === 0) continue this.connection.logger.logSchemaBuild( `dropping old check constraint: ${oldChecks .map((check) => `"${check.name}"`) .join(", ")} from table "${table.name}"`, ) await this.queryRunner.dropCheckConstraints(table, oldChecks) } } protected async dropCompositeUniqueConstraints(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const compositeUniques = table.uniques.filter((tableUnique) => { return ( tableUnique.columnNames.length > 1 && !metadata.uniques.find( (uniqueMetadata) => uniqueMetadata.name === tableUnique.name, ) ) }) if (compositeUniques.length === 0) continue this.connection.logger.logSchemaBuild( `dropping old unique constraint: ${compositeUniques .map((unique) => `"${unique.name}"`) .join(", ")} from table "${table.name}"`, ) await this.queryRunner.dropUniqueConstraints( table, compositeUniques, ) } } protected async dropOldExclusions(): Promise { // Only PostgreSQL supports exclusion constraints if (!(this.connection.driver.options.type === "postgres")) return for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const oldExclusions = table.exclusions.filter((tableExclusion) => { return !metadata.exclusions.find( (exclusionMetadata) => exclusionMetadata.name === tableExclusion.name, ) }) if (oldExclusions.length === 0) continue this.connection.logger.logSchemaBuild( `dropping old exclusion constraint: ${oldExclusions .map((exclusion) => `"${exclusion.name}"`) .join(", ")} from table "${table.name}"`, ) await this.queryRunner.dropExclusionConstraints( table, oldExclusions, ) } } /** * Creates tables that do not exist in the database yet. * New tables are created without foreign and primary keys. * Primary key only can be created in conclusion with auto generated column. */ protected async createNewTables(): Promise { for (const metadata of this.entityToSyncMetadatas) { // check if table does not exist yet const existTable = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (existTable) continue this.connection.logger.logSchemaBuild( `creating a new table: ${this.getTablePath(metadata)}`, ) // create a new table and sync it in the database const table = Table.create(metadata, this.connection.driver) await this.queryRunner.createTable(table, false, false) this.queryRunner.loadedTables.push(table) } } protected async createViews(): Promise { for (const metadata of this.viewEntityToSyncMetadatas) { // check if view does not exist yet const existView = this.queryRunner.loadedViews.find((view) => { const viewExpression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery() const metadataExpression = typeof metadata.expression === "string" ? metadata.expression.trim() : metadata.expression!(this.connection).getQuery() return ( this.getTablePath(view) === this.getTablePath(metadata) && viewExpression === metadataExpression ) }) if (existView) continue this.connection.logger.logSchemaBuild( `creating a new view: ${this.getTablePath(metadata)}`, ) // create a new view and sync it in the database const view = View.create(metadata, this.connection.driver) await this.queryRunner.createView(view, true) this.queryRunner.loadedViews.push(view) } } protected async dropOldViews(): Promise { const droppedViews: Array = [] const viewEntityToSyncMetadatas = this.viewEntityToSyncMetadatas // BuIld lookup cache for finding views metadata const viewToMetadata = new Map() for (const view of this.queryRunner.loadedViews) { const viewMetadata = viewEntityToSyncMetadatas.find((metadata) => { return this.getTablePath(view) === this.getTablePath(metadata) }) if (viewMetadata) { viewToMetadata.set(view, viewMetadata) } } // Gather all changed view, that need a drop for (const view of this.queryRunner.loadedViews) { const viewMetadata = viewToMetadata.get(view) if (!viewMetadata) { continue } const viewExpression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery() const metadataExpression = typeof viewMetadata.expression === "string" ? viewMetadata.expression.trim() : viewMetadata.expression!(this.connection).getQuery() if (viewExpression === metadataExpression) continue this.connection.logger.logSchemaBuild( `dropping an old view: ${view.name}`, ) // Collect view to be dropped droppedViews.push(view) } // Helper function that for a given view, will recursively return list of the view and all views that depend on it const viewDependencyChain = (view: View): View[] => { // Get the view metadata const viewMetadata = viewToMetadata.get(view) let viewWithDependencies = [view] // If no metadata is known for the view, simply return the view itself if (!viewMetadata) { return viewWithDependencies } // Iterate over all known views for (const [ currentView, currentMetadata, ] of viewToMetadata.entries()) { // Ignore self reference if (currentView === view) { continue } // If the currently iterated view depends on the passed in view if ( currentMetadata.dependsOn && (currentMetadata.dependsOn.has(viewMetadata.target) || currentMetadata.dependsOn.has(viewMetadata.name)) ) { // Recursively add currently iterate view and its dependents viewWithDependencies = viewWithDependencies.concat( viewDependencyChain(currentView), ) } } // Return all collected views return viewWithDependencies } // Collect final list of views to be dropped in a Set so there are no duplicates const droppedViewsWithDependencies: Set = new Set( // Collect all dropped views, and their dependencies droppedViews .map((view) => viewDependencyChain(view)) // Flattened to single Array ( can be replaced with flatMap, once supported) .reduce((all, segment) => { return all.concat(segment) }, []) // Sort the views to be dropped in creation order .sort((a, b) => { return ViewUtils.viewMetadataCmp( viewToMetadata.get(a), viewToMetadata.get(b), ) }) // reverse order to get drop order .reverse(), ) // Finally emit all drop views for (const view of droppedViewsWithDependencies) { await this.queryRunner.dropView(view) } this.queryRunner.loadedViews = this.queryRunner.loadedViews.filter( (view) => !droppedViewsWithDependencies.has(view), ) } /** * Drops all columns that exist in the table, but does not exist in the metadata (left old). * We drop their keys too, since it should be safe. */ protected async dropRemovedColumns(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue // find columns that exist in the database but does not exist in the metadata const droppedTableColumns = table.columns.filter((tableColumn) => { return !metadata.columns.find( (columnMetadata) => columnMetadata.isVirtualProperty || columnMetadata.databaseName === tableColumn.name, ) }) if (droppedTableColumns.length === 0) continue this.connection.logger.logSchemaBuild( `columns dropped in ${table.name}: ` + droppedTableColumns.map((column) => column.name).join(", "), ) // drop columns from the database await this.queryRunner.dropColumns(table, droppedTableColumns) } } /** * Adds columns from metadata which does not exist in the table. * Columns are created without keys. */ protected async addNewColumns(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue // find which columns are new const newColumnMetadatas = metadata.columns.filter( (columnMetadata) => { return ( !columnMetadata.isVirtualProperty && !table.columns.find( (tableColumn) => tableColumn.name === columnMetadata.databaseName, ) ) }, ) if (newColumnMetadatas.length === 0) continue // create columns in the database const newTableColumnOptions = this.metadataColumnsToTableColumnOptions(newColumnMetadatas) const newTableColumns = newTableColumnOptions.map( (option) => new TableColumn(option), ) if (newTableColumns.length === 0) continue this.connection.logger.logSchemaBuild( `new columns added: ` + newColumnMetadatas .map((column) => column.databaseName) .join(", "), ) await this.queryRunner.addColumns(table, newTableColumns) } } /** * Updates composite primary keys. */ protected async updatePrimaryKeys(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const primaryMetadataColumns = metadata.columns.filter( (column) => column.isPrimary, ) const primaryTableColumns = table.columns.filter( (column) => column.isPrimary, ) if ( primaryTableColumns.length !== primaryMetadataColumns.length && primaryMetadataColumns.length > 1 ) { const changedPrimaryColumns = primaryMetadataColumns.map( (primaryMetadataColumn) => { return new TableColumn( TableUtils.createTableColumnOptions( primaryMetadataColumn, this.connection.driver, ), ) }, ) await this.queryRunner.updatePrimaryKeys( table, changedPrimaryColumns, ) } } } /** * Update all exist columns which metadata has changed. * Still don't create keys. Also we don't touch foreign keys of the changed columns. */ protected async updateExistColumns(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const changedColumns = this.connection.driver.findChangedColumns( table.columns, metadata.columns, ) if (changedColumns.length === 0) continue // drop all foreign keys that point to this column for (const changedColumn of changedColumns) { await this.dropColumnReferencedForeignKeys( this.getTablePath(metadata), changedColumn.databaseName, ) } // drop all composite indices related to this column for (const changedColumn of changedColumns) { await this.dropColumnCompositeIndices( this.getTablePath(metadata), changedColumn.databaseName, ) } // drop all composite uniques related to this column // Mysql does not support unique constraints. if ( !( DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql" || this.connection.driver.options.type === "spanner" ) ) { for (const changedColumn of changedColumns) { await this.dropColumnCompositeUniques( this.getTablePath(metadata), changedColumn.databaseName, ) } } // generate a map of new/old columns const newAndOldTableColumns = changedColumns.map( (changedColumn) => { const oldTableColumn = table.columns.find( (column) => column.name === changedColumn.databaseName, )! const newTableColumnOptions = TableUtils.createTableColumnOptions( changedColumn, this.connection.driver, ) const newTableColumn = new TableColumn( newTableColumnOptions, ) return { oldColumn: oldTableColumn, newColumn: newTableColumn, } }, ) if (newAndOldTableColumns.length === 0) continue this.connection.logger.logSchemaBuild( `columns changed in "${table.name}". updating: ` + changedColumns .map((column) => column.databaseName) .join(", "), ) await this.queryRunner.changeColumns(table, newAndOldTableColumns) } } /** * Creates composite indices which are missing in db yet. */ protected async createNewIndices(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const newIndices = metadata.indices .filter( (indexMetadata) => !table.indices.find( (tableIndex) => tableIndex.name === indexMetadata.name, ) && indexMetadata.synchronize === true, ) .map((indexMetadata) => TableIndex.create(indexMetadata)) if (newIndices.length === 0) continue this.connection.logger.logSchemaBuild( `adding new indices ${newIndices .map((index) => `"${index.name}"`) .join(", ")} in table "${table.name}"`, ) await this.queryRunner.createIndices(table, newIndices) } } /** * Creates indices for materialized views. */ protected async createNewViewIndices(): Promise { // Only PostgreSQL supports indices for materialized views. if ( this.connection.options.type !== "postgres" || !DriverUtils.isPostgresFamily(this.connection.driver) ) { return } const postgresQueryRunner: PostgresQueryRunner = ( this.queryRunner ) for (const metadata of this.viewEntityToSyncMetadatas) { // check if view does not exist yet const view = this.queryRunner.loadedViews.find((view) => { const viewExpression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery() const metadataExpression = typeof metadata.expression === "string" ? metadata.expression.trim() : metadata.expression!(this.connection).getQuery() return ( this.getTablePath(view) === this.getTablePath(metadata) && viewExpression === metadataExpression ) }) if (!view || !view.materialized) continue const newIndices = metadata.indices .filter( (indexMetadata) => !view.indices.find( (tableIndex) => tableIndex.name === indexMetadata.name, ) && indexMetadata.synchronize === true, ) .map((indexMetadata) => TableIndex.create(indexMetadata)) if (newIndices.length === 0) continue this.connection.logger.logSchemaBuild( `adding new indices ${newIndices .map((index) => `"${index.name}"`) .join(", ")} in view "${view.name}"`, ) await postgresQueryRunner.createViewIndices(view, newIndices) } } protected async createNewChecks(): Promise { // Mysql does not support check constraints if ( DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql" ) return for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const newChecks = metadata.checks .filter( (checkMetadata) => !table.checks.find( (tableCheck) => tableCheck.name === checkMetadata.name, ), ) .map((checkMetadata) => TableCheck.create(checkMetadata)) if (newChecks.length === 0) continue this.connection.logger.logSchemaBuild( `adding new check constraints: ${newChecks .map((index) => `"${index.name}"`) .join(", ")} in table "${table.name}"`, ) await this.queryRunner.createCheckConstraints(table, newChecks) } } /** * Creates composite uniques which are missing in db yet. */ protected async createCompositeUniqueConstraints(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const compositeUniques = metadata.uniques .filter( (uniqueMetadata) => uniqueMetadata.columns.length > 1 && !table.uniques.find( (tableUnique) => tableUnique.name === uniqueMetadata.name, ), ) .map((uniqueMetadata) => TableUnique.create(uniqueMetadata)) if (compositeUniques.length === 0) continue this.connection.logger.logSchemaBuild( `adding new unique constraints: ${compositeUniques .map((unique) => `"${unique.name}"`) .join(", ")} in table "${table.name}"`, ) await this.queryRunner.createUniqueConstraints( table, compositeUniques, ) } } /** * Creates exclusions which are missing in db yet. */ protected async createNewExclusions(): Promise { // Only PostgreSQL supports exclusion constraints if (!(this.connection.driver.options.type === "postgres")) return for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const newExclusions = metadata.exclusions .filter( (exclusionMetadata) => !table.exclusions.find( (tableExclusion) => tableExclusion.name === exclusionMetadata.name, ), ) .map((exclusionMetadata) => TableExclusion.create(exclusionMetadata), ) if (newExclusions.length === 0) continue this.connection.logger.logSchemaBuild( `adding new exclusion constraints: ${newExclusions .map((exclusion) => `"${exclusion.name}"`) .join(", ")} in table "${table.name}"`, ) await this.queryRunner.createExclusionConstraints( table, newExclusions, ) } } /** * Creates foreign keys which does not exist in the table yet. */ protected async createForeignKeys(): Promise { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === this.getTablePath(metadata), ) if (!table) continue const newKeys = metadata.foreignKeys.filter((foreignKey) => { return !table.foreignKeys.find( (dbForeignKey) => dbForeignKey.name === foreignKey.name && this.getTablePath(dbForeignKey) === this.getTablePath( foreignKey.referencedEntityMetadata, ), ) }) if (newKeys.length === 0) continue const dbForeignKeys = newKeys.map((foreignKeyMetadata) => TableForeignKey.create( foreignKeyMetadata, this.connection.driver, ), ) this.connection.logger.logSchemaBuild( `creating a foreign keys: ${newKeys .map((key) => key.name) .join(", ")} on table "${table.name}"`, ) await this.queryRunner.createForeignKeys(table, dbForeignKeys) } } /** * Drops all foreign keys where given column of the given table is being used. */ protected async dropColumnReferencedForeignKeys( tablePath: string, columnName: string, ): Promise { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === tablePath, ) if (!table) return const tablesWithFK: Table[] = [] const columnForeignKey = table.foreignKeys.find( (foreignKey) => foreignKey.columnNames.indexOf(columnName) !== -1, ) if (columnForeignKey) { const clonedTable = table.clone() clonedTable.foreignKeys = [columnForeignKey] tablesWithFK.push(clonedTable) table.removeForeignKey(columnForeignKey) } for (const loadedTable of this.queryRunner.loadedTables) { const dependForeignKeys = loadedTable.foreignKeys.filter( (foreignKey) => { return ( this.getTablePath(foreignKey) === tablePath && foreignKey.referencedColumnNames.indexOf(columnName) !== -1 ) }, ) if (dependForeignKeys.length > 0) { const clonedTable = loadedTable.clone() clonedTable.foreignKeys = dependForeignKeys tablesWithFK.push(clonedTable) dependForeignKeys.forEach((dependForeignKey) => loadedTable.removeForeignKey(dependForeignKey), ) } } if (tablesWithFK.length > 0) { for (const tableWithFK of tablesWithFK) { this.connection.logger.logSchemaBuild( `dropping related foreign keys of ${ tableWithFK.name }: ${tableWithFK.foreignKeys .map((foreignKey) => foreignKey.name) .join(", ")}`, ) await this.queryRunner.dropForeignKeys( tableWithFK, tableWithFK.foreignKeys, ) } } } /** * Drops all composite indices, related to given column. */ protected async dropColumnCompositeIndices( tablePath: string, columnName: string, ): Promise { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === tablePath, ) if (!table) return const relatedIndices = table.indices.filter( (index) => index.columnNames.length > 1 && index.columnNames.indexOf(columnName) !== -1, ) if (relatedIndices.length === 0) return this.connection.logger.logSchemaBuild( `dropping related indices of "${tablePath}"."${columnName}": ${relatedIndices .map((index) => index.name) .join(", ")}`, ) await this.queryRunner.dropIndices(table, relatedIndices) } /** * Drops all composite uniques, related to given column. */ protected async dropColumnCompositeUniques( tablePath: string, columnName: string, ): Promise { const table = this.queryRunner.loadedTables.find( (table) => this.getTablePath(table) === tablePath, ) if (!table) return const relatedUniques = table.uniques.filter( (unique) => unique.columnNames.length > 1 && unique.columnNames.indexOf(columnName) !== -1, ) if (relatedUniques.length === 0) return this.connection.logger.logSchemaBuild( `dropping related unique constraints of "${tablePath}"."${columnName}": ${relatedUniques .map((unique) => unique.name) .join(", ")}`, ) await this.queryRunner.dropUniqueConstraints(table, relatedUniques) } /** * Creates new columns from the given column metadatas. */ protected metadataColumnsToTableColumnOptions( columns: ColumnMetadata[], ): TableColumnOptions[] { return columns.map((columnMetadata) => TableUtils.createTableColumnOptions( columnMetadata, this.connection.driver, ), ) } /** * Creates typeorm service table for storing user defined Views and generate columns. */ protected async createTypeormMetadataTable(queryRunner: QueryRunner) { const schema = this.currentSchema const database = this.currentDatabase const typeormMetadataTable = this.connection.driver.buildTableName( this.connection.metadataTableName, schema, database, ) // Spanner requires at least one primary key in a table. // Since we don't have unique column in "typeorm_metadata" table // and we should avoid breaking changes, we mark all columns as primary for Spanner driver. const isPrimary = this.connection.driver.options.type === "spanner" await queryRunner.createTable( new Table({ database: database, schema: schema, name: typeormMetadataTable, columns: [ { name: "type", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes .metadataType, }), isNullable: false, isPrimary, }, { name: "database", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes .metadataDatabase, }), isNullable: true, isPrimary, }, { name: "schema", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes .metadataSchema, }), isNullable: true, isPrimary, }, { name: "table", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes .metadataTable, }), isNullable: true, isPrimary, }, { name: "name", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes .metadataName, }), isNullable: true, isPrimary, }, { name: "value", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes .metadataValue, }), isNullable: true, isPrimary, }, ], }), true, ) } }