'use strict'; // SQLite3_DDL // // All of the SQLite3 specific DDL helpers for renaming/dropping // columns and changing datatypes. // ------- module.exports = function(client) { var _ = require('lodash'); var Promise = require('../../../promise'); // So altering the schema in SQLite3 is a major pain. // We have our own object to deal with the renaming and altering the types // for sqlite3 things. function SQLite3_DDL(runner, tableCompiler, pragma) { this.tableCompiler = tableCompiler; this.pragma = pragma; this.runner = runner; this.formatter = new client.Formatter(); this.tableName = this.tableCompiler.tableNameRaw; this.alteredName = '_knex_temp_alter' + _.uniqueId(); } SQLite3_DDL.prototype.getColumn = Promise.method(function(column) { var currentCol = _.findWhere(this.pragma, {name: column}); if (!currentCol) throw new Error('The column ' + column + ' is not in the ' + this.tableName + ' table'); return currentCol; }); SQLite3_DDL.prototype.ensureTransaction = Promise.method(function() { if (!this.runner.transaction) { return this.runner.beginTransaction(); } }); SQLite3_DDL.prototype.commitTransaction = Promise.method(function() { if (!this.runner.transaction) { return this.runner.commitTransaction(); } }); SQLite3_DDL.prototype.rollbackTransaction = function(e) { if (this.runner.transaction) throw e; return this.runner.rollbackTransaction().throw(e); }; SQLite3_DDL.prototype.getTableSql = function() { return this.runner.query({sql: 'SELECT name, sql FROM sqlite_master WHERE type="table" AND name="' + this.tableName + '"'}); }; SQLite3_DDL.prototype.renameTable = Promise.method(function() { return this.runner.query({sql: 'ALTER TABLE "' + this.tableName + '" RENAME TO "' + this.alteredName + '"'}); }); SQLite3_DDL.prototype.dropOriginal = function() { return this.runner.query({sql: 'DROP TABLE "' + this.tableName + '"'}); }; SQLite3_DDL.prototype.dropTempTable = function() { return this.runner.query({sql: 'DROP TABLE "' + this.alteredName + '"'}); }; SQLite3_DDL.prototype.copyData = function() { return this.runner.query({sql: 'SELECT * FROM "' + this.tableName + '"'}) .bind(this) .then(this.insertChunked(20, this.alteredName)); }; SQLite3_DDL.prototype.reinsertData = function(iterator) { return function() { return this.runner.query({sql: 'SELECT * FROM "' + this.alteredName + '"'}) .bind(this) .then(this.insertChunked(20, this.tableName, iterator)); }; }; SQLite3_DDL.prototype.insertChunked = function(amount, target, iterator) { iterator = iterator || function(noop) { return noop; }; return function(result) { var batch = []; var ddl = this; return Promise.reduce(result, function(memo, row) { memo++; batch.push(row); if (memo % 20 === 0 || memo === result.length) { return new client.QueryBuilder() .connection(ddl.runner.connection) .table(target) .insert(_.map(batch, iterator)) .then(function() { batch = []; }) .thenReturn(memo); } return memo; }, 0); }; }; SQLite3_DDL.prototype.createTempTable = function(createTable) { return function() { return this.runner.query({sql: createTable.sql.replace(this.tableName, this.alteredName)}); }; }; // Boy, this is quite a method. SQLite3_DDL.prototype.renameColumn = Promise.method(function(from, to) { var currentCol; return this.ensureTransaction() .bind(this) .then(function() { return this.getColumn(from); }) .tap(function(col) { currentCol = col; }) .then(this.getTableSql) .then(function(sql) { var createTable = sql[0]; var a = this.formatter.wrap(from) + ' ' + currentCol.type; var b = this.formatter.wrap(to) + ' ' + currentCol.type; if (createTable.sql.indexOf(a) === -1) { throw new Error('Unable to find the column to change'); } return Promise.bind(this) .then(this.createTempTable(createTable)) .then(this.copyData) .then(this.dropOriginal) .then(function() { return this.runner.query({sql: createTable.sql.replace(a, b)}); }) .then(this.reinsertData(function(row) { row[to] = row[from]; return _.omit(row, from); })) .then(this.dropTempTable); }) .tap(this.commitTransaction) .catch(this.rollbackTransaction); }); SQLite3_DDL.prototype.dropColumn = Promise.method(function(column) { var currentCol; return this.ensureTransaction() .bind(this) .then(function() { return this.getColumn(column); }) .tap(function(col) { currentCol = col; }) .then(this.getTableSql) .then(function(sql) { var createTable = sql[0]; var a = this.formatter.wrap(column) + ' ' + currentCol.type + ', '; if (createTable.sql.indexOf(a) === -1) { throw new Error('Unable to find the column to change'); } return Promise.bind(this) .then(this.createTempTable(createTable)) .then(this.copyData) .then(this.dropOriginal) .then(function() { return this.runner.query({sql: createTable.sql.replace(a, '')}); }) .then(this.reinsertData(function(row) { return _.omit(row, column); })) .then(this.dropTempTable); }) .tap(this.commitTransaction) .catch(this.rollbackTransaction); }); client.SQLite3_DDL = SQLite3_DDL; };