2025-03-23 21:00:08 +01:00

440 lines
13 KiB
JavaScript

/* eslint max-len:0 */
// Table Compiler
// -------
const {
pushAdditional,
pushQuery,
unshiftQuery,
} = require('./internal/helpers');
const helpers = require('../util/helpers');
const groupBy = require('lodash/groupBy');
const indexOf = require('lodash/indexOf');
const isEmpty = require('lodash/isEmpty');
const tail = require('lodash/tail');
const { normalizeArr } = require('../util/helpers');
class TableCompiler {
constructor(client, tableBuilder) {
this.client = client;
this.tableBuilder = tableBuilder;
this._commonBuilder = this.tableBuilder;
this.method = tableBuilder._method;
this.schemaNameRaw = tableBuilder._schemaName;
this.tableNameRaw = tableBuilder._tableName;
this.tableNameLikeRaw = tableBuilder._tableNameLike;
this.single = tableBuilder._single;
this.grouped = groupBy(tableBuilder._statements, 'grouping');
this.formatter = client.formatter(tableBuilder);
this.bindings = [];
this.formatter.bindings = this.bindings;
this.bindingsHolder = this;
this.sequence = [];
this._formatting = client.config && client.config.formatting;
this.checksCount = 0;
}
// Convert the tableCompiler toSQL
toSQL() {
this[this.method]();
return this.sequence;
}
// Column Compilation
// -------
// If this is a table "creation", we need to first run through all
// of the columns to build them into a single string,
// and then run through anything else and push it to the query sequence.
create(ifNot, like) {
const columnBuilders = this.getColumns();
const columns = columnBuilders.map((col) => col.toSQL());
const columnTypes = this.getColumnTypes(columns);
if (this.createAlterTableMethods) {
this.alterTableForCreate(columnTypes);
}
this.createQuery(columnTypes, ifNot, like);
this.columnQueries(columns);
delete this.single.comment;
this.alterTable();
}
// Only create the table if it doesn't exist.
createIfNot() {
this.create(true);
}
createLike() {
this.create(false, true);
}
createLikeIfNot() {
this.create(true, true);
}
// If we're altering the table, we need to one-by-one
// go through and handle each of the queries associated
// with altering the table's schema.
alter() {
const addColBuilders = this.getColumns();
const addColumns = addColBuilders.map((col) => col.toSQL());
const alterColBuilders = this.getColumns('alter');
const alterColumns = alterColBuilders.map((col) => col.toSQL());
const addColumnTypes = this.getColumnTypes(addColumns);
const alterColumnTypes = this.getColumnTypes(alterColumns);
this.addColumns(addColumnTypes);
this.alterColumns(alterColumnTypes, alterColBuilders);
this.columnQueries(addColumns);
this.columnQueries(alterColumns);
this.alterTable();
}
foreign(foreignData) {
if (foreignData.inTable && foreignData.references) {
const keyName = foreignData.keyName
? this.formatter.wrap(foreignData.keyName)
: this._indexCommand('foreign', this.tableNameRaw, foreignData.column);
const column = this.formatter.columnize(foreignData.column);
const references = this.formatter.columnize(foreignData.references);
const inTable = this.formatter.wrap(foreignData.inTable);
const onUpdate = foreignData.onUpdate
? (this.lowerCase ? ' on update ' : ' ON UPDATE ') +
foreignData.onUpdate
: '';
const onDelete = foreignData.onDelete
? (this.lowerCase ? ' on delete ' : ' ON DELETE ') +
foreignData.onDelete
: '';
const deferrable = foreignData.deferrable
? this.lowerCase
? ` deferrable initially ${foreignData.deferrable.toLowerCase()} `
: ` DEFERRABLE INITIALLY ${foreignData.deferrable.toUpperCase()} `
: '';
if (this.lowerCase) {
this.pushQuery(
(!this.forCreate ? `alter table ${this.tableName()} add ` : '') +
'constraint ' +
keyName +
' ' +
'foreign key (' +
column +
') references ' +
inTable +
' (' +
references +
')' +
onUpdate +
onDelete +
deferrable
);
} else {
this.pushQuery(
(!this.forCreate ? `ALTER TABLE ${this.tableName()} ADD ` : '') +
'CONSTRAINT ' +
keyName +
' ' +
'FOREIGN KEY (' +
column +
') REFERENCES ' +
inTable +
' (' +
references +
')' +
onUpdate +
onDelete +
deferrable
);
}
}
}
// Get all of the column sql & bindings individually for building the table queries.
getColumnTypes(columns) {
return columns.reduce(
function (memo, columnSQL) {
const column = columnSQL[0];
memo.sql.push(column.sql);
memo.bindings.concat(column.bindings);
return memo;
},
{ sql: [], bindings: [] }
);
}
// Adds all of the additional queries from the "column"
columnQueries(columns) {
const queries = columns.reduce(function (memo, columnSQL) {
const column = tail(columnSQL);
if (!isEmpty(column)) return memo.concat(column);
return memo;
}, []);
for (const q of queries) {
this.pushQuery(q);
}
}
// All of the columns to "add" for the query
addColumns(columns, prefix) {
prefix = prefix || this.addColumnsPrefix;
if (columns.sql.length > 0) {
const columnSql = columns.sql.map((column) => {
return prefix + column;
});
this.pushQuery({
sql:
(this.lowerCase ? 'alter table ' : 'ALTER TABLE ') +
this.tableName() +
' ' +
columnSql.join(', '),
bindings: columns.bindings,
});
}
}
alterColumns(columns, colBuilders) {
if (columns.sql.length > 0) {
this.addColumns(columns, this.alterColumnsPrefix, colBuilders);
}
}
// Compile the columns as needed for the current create or alter table
getColumns(method) {
const columns = this.grouped.columns || [];
method = method || 'add';
const queryContext = this.tableBuilder.queryContext();
return columns
.filter((column) => column.builder._method === method)
.map((column) => {
// pass queryContext down to columnBuilder but do not overwrite it if already set
if (
queryContext !== undefined &&
column.builder.queryContext() === undefined
) {
column.builder.queryContext(queryContext);
}
return this.client.columnCompiler(this, column.builder);
});
}
tableName() {
const name = this.schemaNameRaw
? `${this.schemaNameRaw}.${this.tableNameRaw}`
: this.tableNameRaw;
return this.formatter.wrap(name);
}
tableNameLike() {
const name = this.schemaNameRaw
? `${this.schemaNameRaw}.${this.tableNameLikeRaw}`
: this.tableNameLikeRaw;
return this.formatter.wrap(name);
}
// Generate all of the alter column statements necessary for the query.
alterTable() {
const alterTable = this.grouped.alterTable || [];
for (let i = 0, l = alterTable.length; i < l; i++) {
const statement = alterTable[i];
if (this[statement.method]) {
this[statement.method].apply(this, statement.args);
} else {
this.client.logger.error(`Debug: ${statement.method} does not exist`);
}
}
for (const item in this.single) {
if (typeof this[item] === 'function') this[item](this.single[item]);
}
}
alterTableForCreate(columnTypes) {
this.forCreate = true;
const savedSequence = this.sequence;
const alterTable = this.grouped.alterTable || [];
this.grouped.alterTable = [];
for (let i = 0, l = alterTable.length; i < l; i++) {
const statement = alterTable[i];
if (indexOf(this.createAlterTableMethods, statement.method) < 0) {
this.grouped.alterTable.push(statement);
continue;
}
if (this[statement.method]) {
this.sequence = [];
this[statement.method].apply(this, statement.args);
columnTypes.sql.push(this.sequence[0].sql);
} else {
this.client.logger.error(`Debug: ${statement.method} does not exist`);
}
}
this.sequence = savedSequence;
this.forCreate = false;
}
// Drop the index on the current table.
dropIndex(value) {
this.pushQuery(`drop index${value}`);
}
dropUnique() {
throw new Error('Method implemented in the dialect driver');
}
dropForeign() {
throw new Error('Method implemented in the dialect driver');
}
dropColumn() {
const columns = helpers.normalizeArr.apply(null, arguments);
const drops = (Array.isArray(columns) ? columns : [columns]).map(
(column) => {
return this.dropColumnPrefix + this.formatter.wrap(column);
}
);
this.pushQuery(
(this.lowerCase ? 'alter table ' : 'ALTER TABLE ') +
this.tableName() +
' ' +
drops.join(', ')
);
}
//Default implementation of setNullable. Overwrite on dialect-specific tablecompiler when needed
//(See postgres/mssql for reference)
_setNullableState(column, nullable) {
const tableName = this.tableName();
const columnName = this.formatter.columnize(column);
const alterColumnPrefix = this.alterColumnsPrefix;
return this.pushQuery({
sql: 'SELECT 1',
output: () => {
return this.client
.queryBuilder()
.from(this.tableNameRaw)
.columnInfo(column)
.then((columnInfo) => {
if (isEmpty(columnInfo)) {
throw new Error(
`.setNullable: Column ${columnName} does not exist in table ${tableName}.`
);
}
const nullableType = nullable ? 'null' : 'not null';
const columnType =
columnInfo.type +
(columnInfo.maxLength ? `(${columnInfo.maxLength})` : '');
const defaultValue =
columnInfo.defaultValue !== null &&
columnInfo.defaultValue !== void 0
? `default '${columnInfo.defaultValue}'`
: '';
const sql = `alter table ${tableName} ${alterColumnPrefix} ${columnName} ${columnType} ${nullableType} ${defaultValue}`;
return this.client.raw(sql);
});
},
});
}
setNullable(column) {
return this._setNullableState(column, true);
}
dropNullable(column) {
return this._setNullableState(column, false);
}
dropChecks(checkConstraintNames) {
if (checkConstraintNames === undefined) return '';
checkConstraintNames = normalizeArr(checkConstraintNames);
const tableName = this.tableName();
const sql = `alter table ${tableName} ${checkConstraintNames
.map((constraint) => `drop constraint ${constraint}`)
.join(', ')}`;
this.pushQuery(sql);
}
check(checkPredicate, bindings, constraintName) {
const tableName = this.tableName();
let checkConstraint = constraintName;
if (!checkConstraint) {
this.checksCount++;
checkConstraint = tableName + '_' + this.checksCount;
}
const sql = `alter table ${tableName} add constraint ${checkConstraint} check(${checkPredicate})`;
this.pushQuery(sql);
}
_addChecks() {
if (this.grouped.checks) {
return (
', ' +
this.grouped.checks
.map((c) => {
return `${
c.args[2] ? 'constraint ' + c.args[2] + ' ' : ''
}check (${this.client.raw(c.args[0], c.args[1])})`;
})
.join(', ')
);
}
return '';
}
// If no name was specified for this index, we will create one using a basic
// convention of the table name, followed by the columns, followed by an
// index type, such as primary or index, which makes the index unique.
_indexCommand(type, tableName, columns) {
if (!Array.isArray(columns)) columns = columns ? [columns] : [];
const table = tableName.replace(/\.|-/g, '_');
const indexName = (
table +
'_' +
columns.join('_') +
'_' +
type
).toLowerCase();
return this.formatter.wrap(indexName);
}
_getPrimaryKeys() {
return (this.grouped.alterTable || [])
.filter((a) => a.method === 'primary')
.flatMap((a) => a.args)
.flat();
}
_canBeAddPrimaryKey(options) {
return options.primaryKey && this._getPrimaryKeys().length === 0;
}
_getIncrementsColumnNames() {
return this.grouped.columns
.filter((c) => c.builder._type === 'increments')
.map((c) => c.builder._args[0]);
}
_getBigIncrementsColumnNames() {
return this.grouped.columns
.filter((c) => c.builder._type === 'bigincrements')
.map((c) => c.builder._args[0]);
}
}
TableCompiler.prototype.pushQuery = pushQuery;
TableCompiler.prototype.pushAdditional = pushAdditional;
TableCompiler.prototype.unshiftQuery = unshiftQuery;
TableCompiler.prototype.lowerCase = true;
TableCompiler.prototype.createAlterTableMethods = null;
TableCompiler.prototype.addColumnsPrefix = 'add column ';
TableCompiler.prototype.alterColumnsPrefix = 'alter column ';
TableCompiler.prototype.modifyColumnPrefix = 'modify column ';
TableCompiler.prototype.dropColumnPrefix = 'drop column ';
module.exports = TableCompiler;