diff --git a/lib/dialects/sqlite3/schema/ddl.js b/lib/dialects/sqlite3/schema/ddl.js index 8348ebf9e4..47039a7b42 100644 --- a/lib/dialects/sqlite3/schema/ddl.js +++ b/lib/dialects/sqlite3/schema/ddl.js @@ -20,6 +20,19 @@ const { reinsertData, renameTable, getTableSql, + isForeignKeys, + setForeignKeys, + checkForeignKeys, + trxBegin, + trxCommit, + createKeysErrorTable, + createCheckErrorTable, + dropKeysErrorTable, + dropCheckErrorTable, + createKeysErrorTrigger, + createCheckErrorTrigger, + rollbackOnKeysError, + rollbackOnCheckError, } = require('./internal/sqlite-ddl-operations'); // So altering the schema in SQLite3 is a major pain. @@ -58,50 +71,52 @@ class SQLite3_DDL { } getTableSql() { - this.trx.disableProcessing(); - return this.trx.raw(getTableSql(this.tableName())).then((result) => { - this.trx.enableProcessing(); - return result; - }); + return this.client.transaction( + async (trx) => { + trx.disableProcessing(); + const result = await trx.raw(getTableSql(this.tableName())); + trx.enableProcessing(); + return result; + }, + { connection: this.connection } + ); } async renameTable() { - return this.trx.raw(renameTable(this.tableName(), this.alteredName)); + return this.client + .raw(renameTable(this.tableName(), this.alteredName)) + .connection(this.connection); } - dropOriginal() { - return this.trx.raw(dropOriginal(this.tableName())); + dropOriginal(trx) { + return trx.raw(dropOriginal(this.tableName())); } - dropTempTable() { - return this.trx.raw(dropTempTable(this.alteredName)); + dropTempTable(trx) { + return trx.raw(dropTempTable(this.alteredName)); } - async copyData() { - const commands = await copyData( - this.trx, - this.tableName(), - this.alteredName - ); + async copyData(trx) { + const commands = await copyData(trx, this.tableName(), this.alteredName); for (const command of commands) { - await this.trx.raw(command); + await trx.raw(command); } } - async reinsertData(iterator) { + async reinsertData(trx, iterator) { const commands = await reinsertData( - this.trx, + trx, iterator, this.tableName(), this.alteredName ); for (const command of commands) { - await this.trx.raw(command); + await trx.raw(command); } } - createTempTable(createTable) { - return this.trx.raw( + createTempTable(trx, createTable) { + return trx.raw( createTempTable(createTable, this.tableName(), this.alteredName) ); } @@ -247,229 +262,191 @@ class SQLite3_DDL { } async dropColumn(columns) { - return this.client.transaction( - (trx) => { - this.trx = trx; - return Promise.all(columns.map((column) => this.getColumn(column))) - .then(() => this.getTableSql()) - .then((sql) => { - const createTable = sql[0]; - let newSql = createTable.sql; - columns.forEach((column) => { - const a = this.client.wrapIdentifier(column); - newSql = this._doReplace(newSql, a, ''); - }); - if (sql === newSql) { - throw new Error('Unable to find the column to change'); - } - const mappedColumns = Object.keys( - this.client.postProcessResponse( - fromPairs(columns.map((column) => [column, column])) - ) - ); - return this.reinsertMapped(createTable, newSql, (row) => - omit(row, ...mappedColumns) - ); - }); - }, - { connection: this.connection } - ); + return Promise.all(columns.map((column) => this.getColumn(column))) + .then(() => this.getTableSql()) + .then((sql) => { + const createTable = sql[0]; + let newSql = createTable.sql; + columns.forEach((column) => { + const a = this.client.wrapIdentifier(column); + newSql = this._doReplace(newSql, a, ''); + }); + if (sql === newSql) { + throw new Error('Unable to find the column to change'); + } + const mappedColumns = Object.keys( + this.client.postProcessResponse( + fromPairs(columns.map((column) => [column, column])) + ) + ); + return this.reinsertMapped(createTable, newSql, (row) => + omit(row, ...mappedColumns) + ); + }); } async dropForeign(columns, indexName) { - return this.client.transaction( - async (trx) => { - this.trx = trx; + const sql = await this.getTableSql(); - const sql = await this.getTableSql(); + const createTable = sql[0]; - const createTable = sql[0]; + const oneLineSql = createTable.sql.replace(/\s+/g, ' '); + const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); - const oneLineSql = createTable.sql.replace(/\s+/g, ' '); - const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); + const defs = matched[2]; - const defs = matched[2]; + if (!defs) { + throw new Error('No column definitions in this statement!'); + } - if (!defs) { - throw new Error('No column definitions in this statement!'); + const updatedDefs = defs + .split(COMMA_NO_PAREN_REGEX) + .map((line) => line.trim()) + .filter((defLine) => { + if ( + defLine.startsWith('constraint') === false && + defLine.includes('foreign key') === false + ) + return true; + + if (indexName) { + if (defLine.includes(indexName)) return false; + return true; + } else { + const matched = defLine.match(/\(`([^)]+)`\)/); + const columnNames = matched[1].split(', '); + + const unknownColumnIncludedCheck = (col) => !columns.includes(col); + return columnNames.every(unknownColumnIncludedCheck) === false; } + }) + .join(', '); - const updatedDefs = defs - .split(COMMA_NO_PAREN_REGEX) - .map((line) => line.trim()) - .filter((defLine) => { - if ( - defLine.startsWith('constraint') === false && - defLine.includes('foreign key') === false - ) - return true; - - if (indexName) { - if (defLine.includes(indexName)) return false; - return true; - } else { - const matched = defLine.match(/\(`([^)]+)`\)/); - const columnNames = matched[1].split(', '); - - const unknownColumnIncludedCheck = (col) => - !columns.includes(col); - return columnNames.every(unknownColumnIncludedCheck) === false; - } - }) - .join(', '); - - const newSql = oneLineSql.replace(defs, updatedDefs); + const newSql = oneLineSql.replace(defs, updatedDefs); - return this.reinsertMapped(createTable, newSql, (row) => { - return row; - }); - }, - { connection: this.connection } - ); + return this.reinsertMapped(createTable, newSql, (row) => { + return row; + }); } async dropPrimary(constraintName) { - return this.client.transaction( - async (trx) => { - this.trx = trx; + const sql = await this.getTableSql(); - const sql = await this.getTableSql(); + const createTable = sql[0]; - const createTable = sql[0]; + const oneLineSql = createTable.sql.replace(/\s+/g, ' '); + const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); - const oneLineSql = createTable.sql.replace(/\s+/g, ' '); - const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); + const defs = matched[2]; - const defs = matched[2]; + if (!defs) { + throw new Error('No column definitions in this statement!'); + } - if (!defs) { - throw new Error('No column definitions in this statement!'); + const updatedDefs = defs + .split(COMMA_NO_PAREN_REGEX) + .map((line) => line.trim()) + .filter((defLine) => { + if ( + defLine.startsWith('constraint') === false && + defLine.includes('primary key') === false + ) + return true; + + if (constraintName) { + if (defLine.includes(constraintName)) return false; + return true; + } else { + return true; } + }) + .join(', '); - const updatedDefs = defs - .split(COMMA_NO_PAREN_REGEX) - .map((line) => line.trim()) - .filter((defLine) => { - if ( - defLine.startsWith('constraint') === false && - defLine.includes('primary key') === false - ) - return true; - - if (constraintName) { - if (defLine.includes(constraintName)) return false; - return true; - } else { - return true; - } - }) - .join(', '); - - const newSql = oneLineSql.replace(defs, updatedDefs); + const newSql = oneLineSql.replace(defs, updatedDefs); - return this.reinsertMapped(createTable, newSql, (row) => { - return row; - }); - }, - { connection: this.connection } - ); + return this.reinsertMapped(createTable, newSql, (row) => { + return row; + }); } async primary(columns, constraintName) { - return this.client.transaction( - async (trx) => { - this.trx = trx; + const tableInfo = (await this.getTableSql())[0]; + const currentSQL = tableInfo.sql; - const tableInfo = (await this.getTableSql())[0]; - const currentSQL = tableInfo.sql; + const oneLineSQL = currentSQL.replace(/\s+/g, ' '); + const matched = oneLineSQL.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); - const oneLineSQL = currentSQL.replace(/\s+/g, ' '); - const matched = oneLineSQL.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); + const columnDefinitions = matched[2]; - const columnDefinitions = matched[2]; + if (!columnDefinitions) { + throw new Error('No column definitions in this statement!'); + } - if (!columnDefinitions) { - throw new Error('No column definitions in this statement!'); - } + const primaryKeyDef = `primary key(${columns.join(',')})`; + const constraintDef = constraintName + ? `constraint ${constraintName} ${primaryKeyDef}` + : primaryKeyDef; - const primaryKeyDef = `primary key(${columns.join(',')})`; - const constraintDef = constraintName - ? `constraint ${constraintName} ${primaryKeyDef}` - : primaryKeyDef; - - const newColumnDefinitions = [ - ...columnDefinitions - .split(COMMA_NO_PAREN_REGEX) - .map((line) => line.trim()) - .filter((line) => line.startsWith('primary') === false) - .map((line) => line.replace(/primary key/i, '')), - constraintDef, - ].join(', '); - - const newSQL = oneLineSQL.replace( - columnDefinitions, - newColumnDefinitions - ); + const newColumnDefinitions = [ + ...columnDefinitions + .split(COMMA_NO_PAREN_REGEX) + .map((line) => line.trim()) + .filter((line) => line.startsWith('primary') === false) + .map((line) => line.replace(/primary key/i, '')), + constraintDef, + ].join(', '); - return this.reinsertMapped(tableInfo, newSQL, (row) => { - return row; - }); - }, - { connection: this.connection } - ); + const newSQL = oneLineSQL.replace(columnDefinitions, newColumnDefinitions); + + return this.reinsertMapped(tableInfo, newSQL, (row) => { + return row; + }); } async foreign(foreignInfo) { - return this.client.transaction( - async (trx) => { - this.trx = trx; + const tableInfo = (await this.getTableSql())[0]; + const currentSQL = tableInfo.sql; - const tableInfo = (await this.getTableSql())[0]; - const currentSQL = tableInfo.sql; + const oneLineSQL = currentSQL.replace(/\s+/g, ' '); + const matched = oneLineSQL.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); - const oneLineSQL = currentSQL.replace(/\s+/g, ' '); - const matched = oneLineSQL.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); + const columnDefinitions = matched[2]; - const columnDefinitions = matched[2]; - - if (!columnDefinitions) { - throw new Error('No column definitions in this statement!'); - } - - const newColumnDefinitions = columnDefinitions - .split(COMMA_NO_PAREN_REGEX) - .map((line) => line.trim()); + if (!columnDefinitions) { + throw new Error('No column definitions in this statement!'); + } - let newForeignSQL = ''; + const newColumnDefinitions = columnDefinitions + .split(COMMA_NO_PAREN_REGEX) + .map((line) => line.trim()); - if (foreignInfo.keyName) { - newForeignSQL += `CONSTRAINT ${foreignInfo.keyName}`; - } + let newForeignSQL = ''; - newForeignSQL += ` FOREIGN KEY (${foreignInfo.column.join(', ')}) `; - newForeignSQL += ` REFERENCES ${foreignInfo.inTable} (${foreignInfo.references})`; + if (foreignInfo.keyName) { + newForeignSQL += `CONSTRAINT ${foreignInfo.keyName}`; + } - if (foreignInfo.onUpdate) { - newForeignSQL += ` ON UPDATE ${foreignInfo.onUpdate}`; - } + newForeignSQL += ` FOREIGN KEY (${foreignInfo.column.join(', ')}) `; + newForeignSQL += ` REFERENCES ${foreignInfo.inTable} (${foreignInfo.references})`; - if (foreignInfo.onDelete) { - newForeignSQL += ` ON DELETE ${foreignInfo.onDelete}`; - } + if (foreignInfo.onUpdate) { + newForeignSQL += ` ON UPDATE ${foreignInfo.onUpdate}`; + } - newColumnDefinitions.push(newForeignSQL); + if (foreignInfo.onDelete) { + newForeignSQL += ` ON DELETE ${foreignInfo.onDelete}`; + } - const newSQL = oneLineSQL.replace( - columnDefinitions, - newColumnDefinitions.join(', ') - ); + newColumnDefinitions.push(newForeignSQL); - return await this.generateReinsertCommands(tableInfo, newSQL, (row) => { - return row; - }); - }, - { connection: this.connection } + const newSQL = oneLineSQL.replace( + columnDefinitions, + newColumnDefinitions.join(', ') ); + + return await this.generateReinsertCommands(tableInfo, newSQL, (row) => { + return row; + }); } /** @@ -479,18 +456,59 @@ class SQLite3_DDL { * It'll be helpful to refactor this file heavily to combine/optimize some of these calls */ - reinsertMapped(createTable, newSql, mapRow) { - return Promise.resolve() - .then(() => this.createTempTable(createTable)) - .then(() => this.copyData()) - .then(() => this.dropOriginal()) - .then(() => this.trx.raw(newSql)) - .then(() => this.reinsertData(mapRow)) - .then(() => this.dropTempTable()); + async reinsertMapped(createTable, newSql, mapRow) { + const wasForeignKeys = + (await this.client.raw(isForeignKeys()).connection(this.connection))[0] + .foreign_keys === 1; + + if (wasForeignKeys) { + await this.client.raw(setForeignKeys(false)).connection(this.connection); + } + + try { + await this.client.transaction( + async (trx) => { + await this.createTempTable(trx, createTable); + await this.copyData(trx); + await this.dropOriginal(trx); + await trx.raw(newSql); + await this.reinsertData(trx, mapRow); + await this.dropTempTable(trx); + + if (wasForeignKeys) { + const foreignKeysViolations = await trx.raw(checkForeignKeys()); + + if (foreignKeysViolations.length > 0) { + throw new Error('FOREIGN KEY constraint failed'); + } + } + }, + { connection: this.connection } + ); + } finally { + if (wasForeignKeys) { + await this.client.raw(setForeignKeys(true)).connection(this.connection); + } + } } async generateReinsertCommands(createTable, newSql, mapRow) { const result = []; + + const wasForeignKeys = + (await this.client.raw(isForeignKeys()).connection(this.connection))[0] + .foreign_keys === 1; + + if (wasForeignKeys) { + result.push(setForeignKeys(false)); + result.push(createKeysErrorTable()); + result.push(createCheckErrorTable()); + result.push(createKeysErrorTrigger()); + result.push(createCheckErrorTrigger()); + } + + result.push(trxBegin()); + result.push( createTempTable(createTable, this.tableName(), this.alteredName) ); @@ -502,6 +520,20 @@ class SQLite3_DDL { result.push(copyAllData(this.alteredName, this.tableName())); result.push(dropTempTable(this.alteredName)); + + if (wasForeignKeys) { + result.push(rollbackOnKeysError()); + result.push(rollbackOnCheckError()); + } + + result.push(trxCommit()); + + if (wasForeignKeys) { + result.push(dropKeysErrorTable()); + result.push(dropCheckErrorTable()); + result.push(setForeignKeys(true)); + } + return result; } } diff --git a/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js b/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js index 63dd9fbcc6..02d464eeab 100644 --- a/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js +++ b/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js @@ -49,6 +49,58 @@ function getTableSql(tableName) { return `SELECT name, sql FROM sqlite_master WHERE type="table" AND name="${tableName}"`; } +function isForeignKeys() { + return `PRAGMA foreign_keys`; +} + +function setForeignKeys(enable) { + return `PRAGMA foreign_keys = ${enable ? 'ON' : 'OFF'}`; +} + +function checkForeignKeys() { + return `PRAGMA foreign_key_check`; +} + +function trxBegin() { + return `BEGIN`; +} + +function trxCommit() { + return `COMMIT`; +} + +function createKeysErrorTable() { + return `CREATE TEMP TABLE foreign_keys_error(c)`; +} + +function createCheckErrorTable() { + return `CREATE TEMP TABLE foreign_check_error(c)`; +} + +function dropKeysErrorTable() { + return `DROP TABLE temp.foreign_keys_error`; +} + +function dropCheckErrorTable() { + return `DROP TABLE temp.foreign_check_error`; +} + +function createKeysErrorTrigger() { + return `CREATE TEMP TRIGGER foreign_keys_trigger BEFORE INSERT ON temp.foreign_keys_error WHEN NEW.c IS NOT 0 BEGIN SELECT RAISE(ROLLBACK, "FOREIGN KEY constraint failed"); END`; +} + +function createCheckErrorTrigger() { + return `CREATE TEMP TRIGGER foreign_check_trigger BEFORE INSERT ON temp.foreign_check_error WHEN NEW.c IS 0 BEGIN SELECT RAISE(ROLLBACK, "FOREIGN KEY mismatch"); END`; +} + +function rollbackOnKeysError() { + return `INSERT INTO temp.foreign_keys_error SELECT COUNT(*) FROM pragma_foreign_key_check`; +} + +function rollbackOnCheckError() { + return `INSERT INTO temp.foreign_check_error SELECT COUNT(*) FROM temp.foreign_keys_error`; +} + module.exports = { copyAllData, copyData, @@ -58,4 +110,17 @@ module.exports = { reinsertData, renameTable, getTableSql, + isForeignKeys, + setForeignKeys, + checkForeignKeys, + trxBegin, + trxCommit, + createKeysErrorTable, + createCheckErrorTable, + dropKeysErrorTable, + dropCheckErrorTable, + createKeysErrorTrigger, + createCheckErrorTrigger, + rollbackOnKeysError, + rollbackOnCheckError, }; diff --git a/lib/execution/runner.js b/lib/execution/runner.js index 1f736181b3..2a095fec90 100644 --- a/lib/execution/runner.js +++ b/lib/execution/runner.js @@ -226,8 +226,17 @@ class Runner { } const results = []; + const errors = []; for (const query of queries) { - results.push(await this.query(query)); + try { + results.push(await this.query(query)); + } catch (e) { + errors.push(e); + } + } + + if (errors.length > 0) { + throw errors[0]; } return results; }