261 lines
7.2 KiB
JavaScript
Raw Normal View History

2016-03-02 17:07:05 +01:00
// SQLite3_DDL
//
// All of the SQLite3 specific DDL helpers for renaming/dropping
// columns and changing datatypes.
// -------
import Promise from 'bluebird';
import { assign, uniqueId, find, identity, map, omit } from '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) {
this.client = client;
2016-03-02 17:07:05 +01:00
this.tableCompiler = tableCompiler;
this.pragma = pragma;
this.tableName = this.tableCompiler.tableNameRaw;
this.alteredName = uniqueId('_knex_temp_alter');
this.connection = connection;
2016-03-02 17:07:05 +01:00
}
assign(SQLite3_DDL.prototype, {
getColumn: Promise.method(function(column) {
const currentCol = find(this.pragma, { name: column });
if (!currentCol)
throw new Error(
`The column ${column} is not in the ${this.tableName} table`
);
2016-03-02 17:07:05 +01:00
return currentCol;
}),
getTableSql() {
return this.trx.raw(
`SELECT name, sql FROM sqlite_master WHERE type="table" AND name="${
this.tableName
}"`
);
2016-03-02 17:07:05 +01:00
},
renameTable: Promise.method(function() {
return this.trx.raw(
`ALTER TABLE "${this.tableName}" RENAME TO "${this.alteredName}"`
);
2016-03-02 17:07:05 +01:00
}),
dropOriginal() {
return this.trx.raw(`DROP TABLE "${this.tableName}"`);
2016-03-02 17:07:05 +01:00
},
dropTempTable() {
return this.trx.raw(`DROP TABLE "${this.alteredName}"`);
2016-03-02 17:07:05 +01:00
},
copyData() {
return this.trx
.raw(`SELECT * FROM "${this.tableName}"`)
2016-03-02 17:07:05 +01:00
.bind(this)
.then(this.insertChunked(20, this.alteredName));
},
reinsertData(iterator) {
2016-03-02 17:07:05 +01:00
return function() {
return this.trx
.raw(`SELECT * FROM "${this.alteredName}"`)
2016-03-02 17:07:05 +01:00
.bind(this)
.then(this.insertChunked(20, this.tableName, iterator));
};
},
insertChunked(amount, target, iterator) {
iterator = iterator || identity;
2016-03-02 17:07:05 +01:00
return function(result) {
let batch = [];
const ddl = this;
return Promise.reduce(
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 = [];
})
.thenReturn(memo);
}
return memo;
},
0
);
2016-03-02 17:07:05 +01:00
};
},
createTempTable(createTable) {
2016-03-02 17:07:05 +01:00
return function() {
return this.trx.raw(
createTable.sql.replace(this.tableName, this.alteredName)
);
2016-03-02 17:07:05 +01:00
};
},
_doReplace(sql, from, to) {
const matched = sql.match(/^CREATE TABLE (\S+) \((.*)\)/);
2016-03-02 17:07:05 +01:00
const tableName = matched[1];
const defs = matched[2];
2016-03-02 17:07:05 +01:00
if (!defs) {
throw new Error('No column definitions in this statement!');
}
2016-03-02 17:07:05 +01:00
let parens = 0,
args = [],
ptr = 0;
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++;
break;
2016-03-02 17:07:05 +01:00
case ')':
parens--;
break;
2016-03-02 17:07:05 +01:00
case ',':
if (parens === 0) {
args.push(defs.slice(ptr, i));
ptr = i + 1;
}
break;
2016-03-02 17:07:05 +01:00
case ' ':
if (ptr === i) {
ptr = i + 1;
}
break;
2016-03-02 17:07:05 +01:00
}
}
args.push(defs.slice(ptr, i));
args = args.map(function(item) {
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
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])) {
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) {
split[1] = split[1].replace(/\(.*\)/, (columns) =>
columns.replace(from, to)
);
2016-03-02 17:07:05 +01:00
}
return split.join(' references ');
}
return item;
});
return sql
.replace(/\(.*\)/, () => `(${args.join(', ')})`)
.replace(/,\s*([,)])/, '$1');
2016-03-02 17:07:05 +01:00
},
// Boy, this is quite a method.
renameColumn: Promise.method(function(from, to) {
return this.client.transaction(
(trx) => {
this.trx = trx;
return this.getColumn(from)
.bind(this)
.then(this.getTableSql)
.then(function(sql) {
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');
}
return Promise.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[to] = row[from];
return omit(row, from);
})
)
.then(this.dropTempTable);
});
},
{ connection: this.connection }
);
2016-03-02 17:07:05 +01:00
}),
dropColumn: Promise.method(function(columns) {
return this.client.transaction(
(trx) => {
this.trx = trx;
return Promise.all(columns.map((column) => this.getColumn(column)))
.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');
}
return Promise.bind(this)
.then(this.createTempTable(createTable))
.then(this.copyData)
.then(this.dropOriginal)
.then(function() {
return this.trx.raw(newSql);
})
.then(this.reinsertData((row) => omit(row, ...columns)))
.then(this.dropTempTable);
});
},
{ connection: this.connection }
);
}),
});
2016-03-02 17:07:05 +01:00
export default SQLite3_DDL;