2016-03-02 17:07:05 +01:00
|
|
|
// SQLite3_DDL
|
|
|
|
//
|
|
|
|
// All of the SQLite3 specific DDL helpers for renaming/dropping
|
|
|
|
// columns and changing datatypes.
|
|
|
|
// -------
|
|
|
|
|
2019-06-17 02:14:17 +02:00
|
|
|
const Bluebird = require('bluebird');
|
2019-06-04 00:37:17 +02:00
|
|
|
const {
|
2019-03-13 22:58:59 +01:00
|
|
|
assign,
|
|
|
|
uniqueId,
|
|
|
|
find,
|
|
|
|
identity,
|
|
|
|
map,
|
|
|
|
omit,
|
|
|
|
invert,
|
|
|
|
fromPairs,
|
2019-06-04 00:37:17 +02:00
|
|
|
} = require('lodash');
|
2016-03-02 17:07:05 +01:00
|
|
|
|
|
|
|
// 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(client, tableCompiler, pragma, connection) {
|
2018-07-09 08:10:34 -04:00
|
|
|
this.client = client;
|
2016-03-02 17:07:05 +01:00
|
|
|
this.tableCompiler = tableCompiler;
|
2016-05-18 19:59:24 +10:00
|
|
|
this.pragma = pragma;
|
2019-03-13 22:58:59 +01:00
|
|
|
this.tableNameRaw = this.tableCompiler.tableNameRaw;
|
2016-05-18 19:59:24 +10:00
|
|
|
this.alteredName = uniqueId('_knex_temp_alter');
|
2018-07-09 08:10:34 -04:00
|
|
|
this.connection = connection;
|
2019-03-13 22:58:59 +01:00
|
|
|
this.formatter =
|
|
|
|
client && client.config && client.config.wrapIdentifier
|
|
|
|
? client.config.wrapIdentifier
|
|
|
|
: (value) => value;
|
2016-03-02 17:07:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
assign(SQLite3_DDL.prototype, {
|
2019-03-13 22:58:59 +01:00
|
|
|
tableName() {
|
|
|
|
return this.formatter(this.tableNameRaw, (value) => value);
|
|
|
|
},
|
|
|
|
|
2019-06-18 00:07:24 +02:00
|
|
|
getColumn: async function(column) {
|
2019-03-13 22:58:59 +01:00
|
|
|
const currentCol = find(this.pragma, (col) => {
|
|
|
|
return (
|
|
|
|
this.client.wrapIdentifier(col.name) ===
|
|
|
|
this.client.wrapIdentifier(column)
|
|
|
|
);
|
|
|
|
});
|
2018-07-09 08:10:34 -04:00
|
|
|
if (!currentCol)
|
|
|
|
throw new Error(
|
2019-03-13 22:58:59 +01:00
|
|
|
`The column ${column} is not in the ${this.tableName()} table`
|
2018-07-09 08:10:34 -04:00
|
|
|
);
|
2016-03-02 17:07:05 +01:00
|
|
|
return currentCol;
|
2019-06-18 00:07:24 +02:00
|
|
|
},
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
getTableSql() {
|
2019-03-13 22:58:59 +01:00
|
|
|
this.trx.disableProcessing();
|
|
|
|
return this.trx
|
|
|
|
.raw(
|
|
|
|
`SELECT name, sql FROM sqlite_master WHERE type="table" AND name="${this.tableName()}"`
|
|
|
|
)
|
|
|
|
.then((result) => {
|
|
|
|
this.trx.enableProcessing();
|
|
|
|
return result;
|
|
|
|
});
|
2016-03-02 17:07:05 +01:00
|
|
|
},
|
|
|
|
|
2019-06-18 00:07:24 +02:00
|
|
|
renameTable: async function() {
|
2018-07-09 08:10:34 -04:00
|
|
|
return this.trx.raw(
|
2019-03-13 22:58:59 +01:00
|
|
|
`ALTER TABLE "${this.tableName()}" RENAME TO "${this.alteredName}"`
|
2018-07-09 08:10:34 -04:00
|
|
|
);
|
2019-06-18 00:07:24 +02:00
|
|
|
},
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
dropOriginal() {
|
2019-03-13 22:58:59 +01:00
|
|
|
return this.trx.raw(`DROP TABLE "${this.tableName()}"`);
|
2016-03-02 17:07:05 +01:00
|
|
|
},
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
dropTempTable() {
|
|
|
|
return this.trx.raw(`DROP TABLE "${this.alteredName}"`);
|
2016-03-02 17:07:05 +01:00
|
|
|
},
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
copyData() {
|
2018-07-09 08:10:34 -04:00
|
|
|
return this.trx
|
2019-03-13 22:58:59 +01:00
|
|
|
.raw(`SELECT * FROM "${this.tableName()}"`)
|
2016-03-02 17:07:05 +01:00
|
|
|
.bind(this)
|
|
|
|
.then(this.insertChunked(20, this.alteredName));
|
|
|
|
},
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
reinsertData(iterator) {
|
2016-03-02 17:07:05 +01:00
|
|
|
return function() {
|
2018-07-09 08:10:34 -04:00
|
|
|
return this.trx
|
|
|
|
.raw(`SELECT * FROM "${this.alteredName}"`)
|
2016-03-02 17:07:05 +01:00
|
|
|
.bind(this)
|
2019-03-13 22:58:59 +01:00
|
|
|
.then(this.insertChunked(20, this.tableName(), iterator));
|
2016-03-02 17:07:05 +01:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
insertChunked(amount, target, iterator) {
|
|
|
|
iterator = iterator || identity;
|
2016-03-02 17:07:05 +01:00
|
|
|
return function(result) {
|
2016-05-17 01:01:34 +10:00
|
|
|
let batch = [];
|
|
|
|
const ddl = this;
|
2019-06-17 02:14:17 +02:00
|
|
|
return Bluebird.reduce(
|
2018-07-09 08:10:34 -04:00
|
|
|
result,
|
|
|
|
function(memo, row) {
|
|
|
|
memo++;
|
|
|
|
batch.push(row);
|
|
|
|
if (memo % 20 === 0 || memo === result.length) {
|
|
|
|
return ddl.trx
|
|
|
|
.queryBuilder()
|
|
|
|
.table(target)
|
|
|
|
.insert(map(batch, iterator))
|
|
|
|
.then(function() {
|
|
|
|
batch = [];
|
|
|
|
})
|
2019-06-17 02:14:17 +02:00
|
|
|
.then(() => memo);
|
2018-07-09 08:10:34 -04:00
|
|
|
}
|
|
|
|
return memo;
|
|
|
|
},
|
|
|
|
0
|
|
|
|
);
|
2016-03-02 17:07:05 +01:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
createTempTable(createTable) {
|
2016-03-02 17:07:05 +01:00
|
|
|
return function() {
|
2018-07-09 08:10:34 -04:00
|
|
|
return this.trx.raw(
|
2019-03-13 22:58:59 +01:00
|
|
|
createTable.sql.replace(this.tableName(), this.alteredName)
|
2018-07-09 08:10:34 -04:00
|
|
|
);
|
2016-03-02 17:07:05 +01:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2018-07-09 08:10:34 -04:00
|
|
|
_doReplace(sql, from, to) {
|
2016-05-17 01:01:34 +10:00
|
|
|
const matched = sql.match(/^CREATE TABLE (\S+) \((.*)\)/);
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2016-05-17 01:01:34 +10:00
|
|
|
const tableName = matched[1];
|
|
|
|
const defs = matched[2];
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2018-07-09 08:10:34 -04:00
|
|
|
if (!defs) {
|
|
|
|
throw new Error('No column definitions in this statement!');
|
|
|
|
}
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2018-07-09 08:10:34 -04:00
|
|
|
let parens = 0,
|
|
|
|
args = [],
|
|
|
|
ptr = 0;
|
2016-05-17 01:01:34 +10:00
|
|
|
let i = 0;
|
|
|
|
const x = defs.length;
|
|
|
|
for (i = 0; i < x; i++) {
|
2016-03-02 17:07:05 +01:00
|
|
|
switch (defs[i]) {
|
|
|
|
case '(':
|
|
|
|
parens++;
|
2016-05-17 01:01:34 +10:00
|
|
|
break;
|
2016-03-02 17:07:05 +01:00
|
|
|
case ')':
|
|
|
|
parens--;
|
2016-05-17 01:01:34 +10:00
|
|
|
break;
|
2016-03-02 17:07:05 +01:00
|
|
|
case ',':
|
|
|
|
if (parens === 0) {
|
|
|
|
args.push(defs.slice(ptr, i));
|
|
|
|
ptr = i + 1;
|
|
|
|
}
|
2016-05-17 01:01:34 +10:00
|
|
|
break;
|
2016-03-02 17:07:05 +01:00
|
|
|
case ' ':
|
|
|
|
if (ptr === i) {
|
|
|
|
ptr = i + 1;
|
|
|
|
}
|
2016-05-17 01:01:34 +10:00
|
|
|
break;
|
2016-03-02 17:07:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
args.push(defs.slice(ptr, i));
|
|
|
|
|
2018-10-03 05:02:37 +02:00
|
|
|
// Backwards compatible for double quoted sqlite databases
|
|
|
|
// Detect CREATE TABLE "accounts" ("id"...)
|
|
|
|
// The "from" and "to" field use backsticks, because this is the default notation for
|
|
|
|
// SQlite3 since Knex 0.14.
|
|
|
|
// e.g. from: `about`
|
|
|
|
//
|
|
|
|
// We have to replace the from+to field with double slashes in case you created your SQlite3
|
|
|
|
// database with Knex < 0.14.
|
|
|
|
if (sql.match(/CREATE\sTABLE\s".*"\s\("/)) {
|
|
|
|
from = from.replace(/[`]/g, '"');
|
|
|
|
to = to.replace(/[`]/g, '"');
|
|
|
|
}
|
|
|
|
|
2018-07-09 08:10:34 -04:00
|
|
|
args = args.map(function(item) {
|
2016-05-17 01:01:34 +10:00
|
|
|
let split = item.split(' ');
|
2016-03-02 17:07:05 +01:00
|
|
|
|
|
|
|
if (split[0] === from) {
|
|
|
|
// column definition
|
|
|
|
if (to) {
|
|
|
|
split[0] = to;
|
|
|
|
return split.join(' ');
|
|
|
|
}
|
|
|
|
return ''; // for deletions
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip constraint name
|
2018-07-09 08:10:34 -04:00
|
|
|
const idx = /constraint/i.test(split[0]) ? 2 : 0;
|
2016-03-02 17:07:05 +01:00
|
|
|
|
|
|
|
// primary key and unique constraints have one or more
|
|
|
|
// columns from this table listed between (); replace
|
|
|
|
// one if it matches
|
|
|
|
if (/primary|unique/i.test(split[idx])) {
|
2018-07-09 08:10:34 -04:00
|
|
|
return item.replace(/\(.*\)/, (columns) => columns.replace(from, to));
|
2016-03-02 17:07:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// foreign keys have one or more columns from this table
|
|
|
|
// listed between (); replace one if it matches
|
|
|
|
// foreign keys also have a 'references' clause
|
|
|
|
// which may reference THIS table; if it does, replace
|
|
|
|
// column references in that too!
|
|
|
|
if (/foreign/.test(split[idx])) {
|
|
|
|
split = item.split(/ references /i);
|
|
|
|
// the quoted column names save us from having to do anything
|
|
|
|
// other than a straight replace here
|
|
|
|
split[0] = split[0].replace(from, to);
|
|
|
|
|
|
|
|
if (split[1].slice(0, tableName.length) === tableName) {
|
2018-07-09 08:10:34 -04:00
|
|
|
split[1] = split[1].replace(/\(.*\)/, (columns) =>
|
|
|
|
columns.replace(from, to)
|
|
|
|
);
|
2016-03-02 17:07:05 +01:00
|
|
|
}
|
|
|
|
return split.join(' references ');
|
|
|
|
}
|
|
|
|
|
|
|
|
return item;
|
|
|
|
});
|
2018-07-09 08:10:34 -04:00
|
|
|
return sql
|
|
|
|
.replace(/\(.*\)/, () => `(${args.join(', ')})`)
|
|
|
|
.replace(/,\s*([,)])/, '$1');
|
2016-03-02 17:07:05 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
// Boy, this is quite a method.
|
2019-06-18 00:07:24 +02:00
|
|
|
renameColumn: async function(from, to) {
|
2018-07-09 08:10:34 -04:00
|
|
|
return this.client.transaction(
|
2019-06-18 00:07:24 +02:00
|
|
|
async (trx) => {
|
2018-07-09 08:10:34 -04:00
|
|
|
this.trx = trx;
|
2019-06-18 00:07:24 +02:00
|
|
|
const column = await this.getColumn(from);
|
|
|
|
const sql = await this.getTableSql(column);
|
|
|
|
const a = this.client.wrapIdentifier(from);
|
|
|
|
const b = this.client.wrapIdentifier(to);
|
|
|
|
const createTable = sql[0];
|
|
|
|
const newSql = this._doReplace(createTable.sql, a, b);
|
|
|
|
if (sql === newSql) {
|
|
|
|
throw new Error('Unable to find the column to change');
|
|
|
|
}
|
|
|
|
const { from: mappedFrom, to: mappedTo } = invert(
|
|
|
|
this.client.postProcessResponse(
|
|
|
|
invert({
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
return Bluebird.bind(this)
|
|
|
|
.then(this.createTempTable(createTable))
|
|
|
|
.then(this.copyData)
|
|
|
|
.then(this.dropOriginal)
|
|
|
|
.then(function() {
|
|
|
|
return this.trx.raw(newSql);
|
|
|
|
})
|
|
|
|
.then(
|
|
|
|
this.reinsertData(function(row) {
|
|
|
|
row[mappedTo] = row[mappedFrom];
|
|
|
|
return omit(row, mappedFrom);
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.then(this.dropTempTable);
|
2018-07-09 08:10:34 -04:00
|
|
|
},
|
|
|
|
{ connection: this.connection }
|
|
|
|
);
|
2019-06-18 00:07:24 +02:00
|
|
|
},
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2019-06-18 00:07:24 +02:00
|
|
|
dropColumn: async function(columns) {
|
2018-07-09 08:10:34 -04:00
|
|
|
return this.client.transaction(
|
|
|
|
(trx) => {
|
|
|
|
this.trx = trx;
|
2019-06-17 02:14:17 +02:00
|
|
|
return Bluebird.all(columns.map((column) => this.getColumn(column)))
|
2018-07-09 08:10:34 -04:00
|
|
|
.bind(this)
|
|
|
|
.then(this.getTableSql)
|
|
|
|
.then(function(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');
|
|
|
|
}
|
2019-03-13 22:58:59 +01:00
|
|
|
const mappedColumns = Object.keys(
|
|
|
|
this.client.postProcessResponse(
|
|
|
|
fromPairs(columns.map((column) => [column, column]))
|
|
|
|
)
|
|
|
|
);
|
2019-06-17 02:14:17 +02:00
|
|
|
return Bluebird.bind(this)
|
2018-07-09 08:10:34 -04:00
|
|
|
.then(this.createTempTable(createTable))
|
|
|
|
.then(this.copyData)
|
|
|
|
.then(this.dropOriginal)
|
|
|
|
.then(function() {
|
|
|
|
return this.trx.raw(newSql);
|
|
|
|
})
|
2019-03-13 22:58:59 +01:00
|
|
|
.then(this.reinsertData((row) => omit(row, ...mappedColumns)))
|
2018-07-09 08:10:34 -04:00
|
|
|
.then(this.dropTempTable);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
{ connection: this.connection }
|
|
|
|
);
|
2019-06-18 00:07:24 +02:00
|
|
|
},
|
2018-07-09 08:10:34 -04:00
|
|
|
});
|
2016-03-02 17:07:05 +01:00
|
|
|
|
2019-06-04 00:37:17 +02:00
|
|
|
module.exports = SQLite3_DDL;
|