401 lines
11 KiB
JavaScript

// SQLite3_DDL
//
// All of the SQLite3 specific DDL helpers for renaming/dropping
// columns and changing datatypes.
// -------
const identity = require('lodash/identity');
const { nanonum } = require('../../../util/nanoid');
const {
copyData,
dropOriginal,
renameTable,
getTableSql,
isForeignCheckEnabled,
setForeignCheck,
executeForeignCheck,
} = require('./internal/sqlite-ddl-operations');
const { parseCreateTable, parseCreateIndex } = require('./internal/parser');
const {
compileCreateTable,
compileCreateIndex,
} = require('./internal/compiler');
const { isEqualId, includesId } = require('./internal/utils');
// 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.
class SQLite3_DDL {
constructor(client, tableCompiler, pragma, connection) {
this.client = client;
this.tableCompiler = tableCompiler;
this.pragma = pragma;
this.tableNameRaw = this.tableCompiler.tableNameRaw;
this.alteredName = `_knex_temp_alter${nanonum(3)}`;
this.connection = connection;
this.formatter = (value) =>
this.client.customWrapIdentifier(value, identity);
this.wrap = (value) => this.client.wrapIdentifierImpl(value);
}
tableName() {
return this.formatter(this.tableNameRaw);
}
getTableSql() {
const tableName = this.tableName();
return this.client.transaction(
async (trx) => {
trx.disableProcessing();
const result = await trx.raw(getTableSql(tableName));
trx.enableProcessing();
return {
createTable: result.filter((create) => create.type === 'table')[0]
.sql,
createIndices: result
.filter((create) => create.type === 'index')
.map((create) => create.sql),
};
},
{ connection: this.connection }
);
}
async isForeignCheckEnabled() {
const result = await this.client
.raw(isForeignCheckEnabled())
.connection(this.connection);
return result[0].foreign_keys === 1;
}
async setForeignCheck(enable) {
await this.client.raw(setForeignCheck(enable)).connection(this.connection);
}
renameTable(trx) {
return trx.raw(renameTable(this.alteredName, this.tableName()));
}
dropOriginal(trx) {
return trx.raw(dropOriginal(this.tableName()));
}
copyData(trx, columns) {
return trx.raw(copyData(this.tableName(), this.alteredName, columns));
}
async alterColumn(columns) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.map((column) => {
const newColumnInfo = columns.find((c) => isEqualId(c.name, column.name));
if (newColumnInfo) {
column.type = newColumnInfo.type;
column.constraints.default =
newColumnInfo.defaultTo !== null
? {
name: null,
value: newColumnInfo.defaultTo,
expression: false,
}
: null;
column.constraints.notnull = newColumnInfo.notNull
? { name: null, conflict: null }
: null;
column.constraints.null = newColumnInfo.notNull
? null
: column.constraints.null;
}
return column;
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.generateAlterCommands(newTable, createIndices);
}
async dropColumn(columns) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.filter(
(parsedColumn) =>
parsedColumn.expression || !includesId(columns, parsedColumn.name)
);
if (parsedTable.columns.length === 0) {
throw new Error('Unable to drop last column from table');
}
parsedTable.constraints = parsedTable.constraints.filter((constraint) => {
if (constraint.type === 'PRIMARY KEY' || constraint.type === 'UNIQUE') {
return constraint.columns.every(
(constraintColumn) =>
constraintColumn.expression ||
!includesId(columns, constraintColumn.name)
);
} else if (constraint.type === 'FOREIGN KEY') {
return (
constraint.columns.every(
(constraintColumnName) => !includesId(columns, constraintColumnName)
) &&
(constraint.references.table !== parsedTable.table ||
constraint.references.columns.every(
(referenceColumnName) => !includesId(columns, referenceColumnName)
))
);
} else {
return true;
}
});
const newColumns = parsedTable.columns.map((column) => column.name);
const newTable = compileCreateTable(parsedTable, this.wrap);
const newIndices = [];
for (const createIndex of createIndices) {
const parsedIndex = parseCreateIndex(createIndex);
parsedIndex.columns = parsedIndex.columns.filter(
(parsedColumn) =>
parsedColumn.expression || !includesId(columns, parsedColumn.name)
);
if (parsedIndex.columns.length > 0) {
newIndices.push(compileCreateIndex(parsedIndex, this.wrap));
}
}
return this.alter(newTable, newIndices, newColumns);
}
async dropForeign(columns, foreignKeyName) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
if (!foreignKeyName) {
parsedTable.columns = parsedTable.columns.map((column) => ({
...column,
references: includesId(columns, column.name) ? null : column.references,
}));
}
parsedTable.constraints = parsedTable.constraints.filter((constraint) => {
if (constraint.type === 'FOREIGN KEY') {
if (foreignKeyName) {
return (
!constraint.name || !isEqualId(constraint.name, foreignKeyName)
);
}
return constraint.columns.every(
(constraintColumnName) => !includesId(columns, constraintColumnName)
);
} else {
return true;
}
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.alter(newTable, createIndices);
}
async dropPrimary(constraintName) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.map((column) => ({
...column,
primary: null,
}));
parsedTable.constraints = parsedTable.constraints.filter((constraint) => {
if (constraint.type === 'PRIMARY KEY') {
if (constraintName) {
return (
!constraint.name || !isEqualId(constraint.name, constraintName)
);
} else {
return false;
}
} else {
return true;
}
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.alter(newTable, createIndices);
}
async primary(columns, constraintName) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.map((column) => ({
...column,
primary: null,
}));
parsedTable.constraints = parsedTable.constraints.filter(
(constraint) => constraint.type !== 'PRIMARY KEY'
);
parsedTable.constraints.push({
type: 'PRIMARY KEY',
name: constraintName || null,
columns: columns.map((column) => ({
name: column,
expression: false,
collation: null,
order: null,
})),
conflict: null,
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.alter(newTable, createIndices);
}
async foreign(foreignInfo) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.constraints.push({
type: 'FOREIGN KEY',
name: foreignInfo.keyName || null,
columns: foreignInfo.column,
references: {
table: foreignInfo.inTable,
columns: foreignInfo.references,
delete: foreignInfo.onDelete || null,
update: foreignInfo.onUpdate || null,
match: null,
deferrable: null,
},
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.generateAlterCommands(newTable, createIndices);
}
async setNullable(column, isNullable) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
const parsedColumn = parsedTable.columns.find((c) =>
isEqualId(column, c.name)
);
if (!parsedColumn) {
throw new Error(
`.setNullable: Column ${column} does not exist in table ${this.tableName()}.`
);
}
parsedColumn.constraints.notnull = isNullable
? null
: { name: null, conflict: null };
parsedColumn.constraints.null = isNullable
? parsedColumn.constraints.null
: null;
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.generateAlterCommands(newTable, createIndices);
}
async alter(newSql, createIndices, columns) {
const wasForeignCheckEnabled = await this.isForeignCheckEnabled();
if (wasForeignCheckEnabled) {
await this.setForeignCheck(false);
}
try {
await this.client.transaction(
async (trx) => {
await trx.raw(newSql);
await this.copyData(trx, columns);
await this.dropOriginal(trx);
await this.renameTable(trx);
for (const createIndex of createIndices) {
await trx.raw(createIndex);
}
if (wasForeignCheckEnabled) {
const foreignViolations = await trx.raw(executeForeignCheck());
if (foreignViolations.length > 0) {
throw new Error('FOREIGN KEY constraint failed');
}
}
},
{ connection: this.connection }
);
} finally {
if (wasForeignCheckEnabled) {
await this.setForeignCheck(true);
}
}
}
async generateAlterCommands(newSql, createIndices, columns) {
const sql = [];
const pre = [];
const post = [];
let check = null;
sql.push(newSql);
sql.push(copyData(this.tableName(), this.alteredName, columns));
sql.push(dropOriginal(this.tableName()));
sql.push(renameTable(this.alteredName, this.tableName()));
for (const createIndex of createIndices) {
sql.push(createIndex);
}
const isForeignCheckEnabled = await this.isForeignCheckEnabled();
if (isForeignCheckEnabled) {
pre.push(setForeignCheck(false));
post.push(setForeignCheck(true));
check = executeForeignCheck();
}
return { pre, sql, check, post };
}
}
module.exports = SQLite3_DDL;