/* 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;