mirror of
https://github.com/knex/knex.git
synced 2025-07-05 08:01:09 +00:00
401 lines
11 KiB
JavaScript
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;
|