UNPKG

typeorm

Version:

Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.

930 lines (928 loc) • 46.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RdbmsSchemaBuilder = void 0; const Table_1 = require("./table/Table"); const TableColumn_1 = require("./table/TableColumn"); const TableForeignKey_1 = require("./table/TableForeignKey"); const TableIndex_1 = require("./table/TableIndex"); const TableUtils_1 = require("./util/TableUtils"); const TableUnique_1 = require("./table/TableUnique"); const TableCheck_1 = require("./table/TableCheck"); const TableExclusion_1 = require("./table/TableExclusion"); const View_1 = require("./view/View"); const ViewUtils_1 = require("./util/ViewUtils"); const DriverUtils_1 = require("../driver/DriverUtils"); /** * 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 */ class RdbmsSchemaBuilder { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(connection) { this.connection = connection; this["@instanceof"] = Symbol.for("RdbmsSchemaBuilder"); } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Creates complete schemas for the given entity metadatas. */ async build() { 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) { if (this.viewEntityToSyncMetadatas.length > 0 || this.hasGeneratedColumns()) { await this.createTypeormMetadataTable(queryRunner); } } /** * Returns sql queries to be executed by schema builder. */ async log() { 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. */ get entityToSyncMetadatas() { 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. */ get viewEntityToSyncMetadatas() { return (this.connection.entityMetadatas .filter((metadata) => metadata.tableType === "view" && metadata.synchronize) // sort views in creation order by dependencies .sort(ViewUtils_1.ViewUtils.viewMetadataCmp)); } /** * Checks if there are at least one generated column. */ hasGeneratedColumns() { 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. */ async executeSchemaSyncOperationsInProperOrder() { 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.changeTableComment(); 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(); } getTablePath(target) { 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. */ async dropOldForeignKeys() { 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 */ async renameTables() { // 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. */ async renameColumns() { 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); } } async dropOldIndices() { 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 = 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); } } } async dropOldChecks() { // Mysql does not support check constraints if (DriverUtils_1.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); } } async dropCompositeUniqueConstraints() { 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); } } async dropOldExclusions() { // 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); } } /** * change table comment */ async changeTableComment() { for (const metadata of this.entityToSyncMetadatas) { const table = this.queryRunner.loadedTables.find((table) => this.getTablePath(table) === this.getTablePath(metadata)); if (!table) continue; if (DriverUtils_1.DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "postgres") { const newComment = metadata.comment; await this.queryRunner.changeTableComment(table, newComment); } } } /** * 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. */ async createNewTables() { 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_1.Table.create(metadata, this.connection.driver); await this.queryRunner.createTable(table, false, false); this.queryRunner.loadedTables.push(table); } } async createViews() { 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_1.View.create(metadata, this.connection.driver); await this.queryRunner.createView(view, true); this.queryRunner.loadedViews.push(view); } } async dropOldViews() { const droppedViews = []; 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) => { // 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 = 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_1.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. */ async dropRemovedColumns() { 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. */ async addNewColumns() { 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_1.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. */ async updatePrimaryKeys() { 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_1.TableColumn(TableUtils_1.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. */ async updateExistColumns() { 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_1.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_1.TableUtils.createTableColumnOptions(changedColumn, this.connection.driver); const newTableColumn = new TableColumn_1.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. */ async createNewIndices() { 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_1.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. */ async createNewViewIndices() { // Only PostgreSQL supports indices for materialized views. if (this.connection.options.type !== "postgres" || !DriverUtils_1.DriverUtils.isPostgresFamily(this.connection.driver)) { return; } const 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_1.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); } } async createNewChecks() { // Mysql does not support check constraints if (DriverUtils_1.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_1.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. */ async createCompositeUniqueConstraints() { 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_1.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. */ async createNewExclusions() { // 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_1.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. */ async createForeignKeys() { 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_1.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. */ async dropColumnReferencedForeignKeys(tablePath, columnName) { const table = this.queryRunner.loadedTables.find((table) => this.getTablePath(table) === tablePath); if (!table) return; const tablesWithFK = []; 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. */ async dropColumnCompositeIndices(tablePath, columnName) { 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. */ async dropColumnCompositeUniques(tablePath, columnName) { 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. */ metadataColumnsToTableColumnOptions(columns) { return columns.map((columnMetadata) => TableUtils_1.TableUtils.createTableColumnOptions(columnMetadata, this.connection.driver)); } /** * Creates typeorm service table for storing user defined Views and generate columns. */ async createTypeormMetadataTable(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_1.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); } } exports.RdbmsSchemaBuilder = RdbmsSchemaBuilder; //# sourceMappingURL=RdbmsSchemaBuilder.js.map