knex
Advanced tools
Comparing version 0.95.10 to 0.95.11
@@ -15,4 +15,21 @@ const Formatter = require('../../formatter'); | ||
} | ||
/** | ||
* Returns its argument with single quotes escaped, so it can be included into a single-quoted string. | ||
* | ||
* For example, it converts "has'quote" to "has''quote". | ||
* | ||
* This assumes QUOTED_IDENTIFIER ON so it is only ' that need escaping, | ||
* never ", because " cannot be used to quote a string when that's on; | ||
* otherwise we'd need to be aware of whether the string is quoted with " or '. | ||
* | ||
* This assumption is consistent with the SQL Knex generates. | ||
* @param {string} string | ||
* @returns {string} | ||
*/ | ||
escapingStringDelimiters(string) { | ||
return (string || '').replace(/'/g, "''"); | ||
} | ||
} | ||
module.exports = MSSQL_Formatter; |
@@ -34,2 +34,26 @@ // MSSQL Query Compiler | ||
with() { | ||
// WITH RECURSIVE is a syntax error: | ||
// SQL Server does not syntactically distinguish recursive and non-recursive CTEs. | ||
// So mark all statements as non-recursive, generate the SQL, then restore. | ||
// This approach ensures any changes in base class with() get propagated here. | ||
const undoList = []; | ||
if (this.grouped.with) { | ||
for (const stmt of this.grouped.with) { | ||
if (stmt.recursive) { | ||
undoList.push(stmt); | ||
stmt.recursive = false; | ||
} | ||
} | ||
} | ||
const result = super.with(); | ||
// Restore the recursive markings, in case this same query gets cloned and passed to other drivers. | ||
for (const stmt of undoList) { | ||
stmt.recursive = true; | ||
} | ||
return result; | ||
} | ||
select() { | ||
@@ -433,3 +457,4 @@ const sql = this.with(); | ||
let sql = `select [COLUMN_NAME], [COLUMN_DEFAULT], [DATA_TYPE], [CHARACTER_MAXIMUM_LENGTH], [IS_NULLABLE] from information_schema.columns where table_name = ? and table_catalog = ?`; | ||
// GOTCHA: INFORMATION_SCHEMA.COLUMNS must be capitalized to work when the database has a case-sensitive collation. [#4573] | ||
let sql = `select [COLUMN_NAME], [COLUMN_DEFAULT], [DATA_TYPE], [CHARACTER_MAXIMUM_LENGTH], [IS_NULLABLE] from INFORMATION_SCHEMA.COLUMNS where table_name = ? and table_catalog = ?`; | ||
const bindings = [table, this.client.database()]; | ||
@@ -436,0 +461,0 @@ |
@@ -95,3 +95,7 @@ // MSSQL Column Compiler | ||
comment(comment) { | ||
comment(/** @type {string} */ comment) { | ||
if (!comment) { | ||
return; | ||
} | ||
// XXX: This is a byte limit, not character, so we cannot definitively say they'll exceed the limit without database collation info. | ||
@@ -105,2 +109,21 @@ // (Yes, even if the column has its own collation, the sqlvariant still uses the database collation.) | ||
} | ||
// See: https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-addextendedproperty-transact-sql?view=sql-server-ver15#b-adding-an-extended-property-to-a-column-in-a-table | ||
const value = this.formatter.escapingStringDelimiters(comment); | ||
const level0name = this.tableCompiler.schemaNameRaw || 'dbo'; | ||
const level1name = this.formatter.escapingStringDelimiters( | ||
this.tableCompiler.tableNameRaw | ||
); | ||
const level2name = this.formatter.escapingStringDelimiters( | ||
this.args[0] || this.defaults('columnName') | ||
); | ||
const args = `N'MS_Description', N'${value}', N'Schema', N'${level0name}', N'Table', N'${level1name}', N'Column', N'${level2name}'`; | ||
this.pushAdditional(function () { | ||
const isAlreadyDefined = `EXISTS(SELECT * FROM sys.fn_listextendedproperty(N'MS_Description', N'Schema', N'${level0name}', N'Table', N'${level1name}', N'Column', N'${level2name}'))`; | ||
this.pushQuery( | ||
`IF ${isAlreadyDefined}\n EXEC sys.sp_updateextendedproperty ${args}\nELSE\n EXEC sys.sp_addextendedproperty ${args}` | ||
); | ||
}); | ||
return ''; | ||
@@ -107,0 +130,0 @@ } |
@@ -27,12 +27,35 @@ /* eslint max-len:0 */ | ||
this.pushQuery(sql); | ||
if (this.single.comment) { | ||
const { comment } = this.single; | ||
// XXX: This is a byte limit, not character, so we cannot definitively say they'll exceed the limit without database collation info. | ||
if (comment.length > 7500 / 2) | ||
this.client.logger.warn( | ||
'Your comment might be longer than the max comment length for MSSQL of 7,500 bytes.' | ||
); | ||
this.comment(this.single.comment); | ||
} | ||
} | ||
this.pushQuery(sql); | ||
comment(/** @type {string} */ comment) { | ||
if (!comment) { | ||
return; | ||
} | ||
// XXX: This is a byte limit, not character, so we cannot definitively say they'll exceed the limit without server collation info. | ||
// When I checked in SQL Server 2019, the ctext column in sys.syscomments is defined as a varbinary(8000), so it doesn't even have its own defined collation. | ||
if (comment.length > 7500 / 2) { | ||
this.client.logger.warn( | ||
'Your comment might be longer than the max comment length for MSSQL of 7,500 bytes.' | ||
); | ||
} | ||
// See: https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-addextendedproperty-transact-sql?view=sql-server-ver15#f-adding-an-extended-property-to-a-table | ||
const value = this.formatter.escapingStringDelimiters(comment); | ||
const level0name = this.formatter.escapingStringDelimiters( | ||
this.schemaNameRaw || 'dbo' | ||
); | ||
const level1name = this.formatter.escapingStringDelimiters( | ||
this.tableNameRaw | ||
); | ||
const args = `N'MS_Description', N'${value}', N'Schema', N'${level0name}', N'Table', N'${level1name}'`; | ||
const isAlreadyDefined = `EXISTS(SELECT * FROM sys.fn_listextendedproperty(N'MS_Description', N'Schema', N'${level0name}', N'Table', N'${level1name}', NULL, NULL))`; | ||
this.pushQuery( | ||
`IF ${isAlreadyDefined}\n EXEC sys.sp_updateextendedproperty ${args}\nELSE\n EXEC sys.sp_addextendedproperty ${args}` | ||
); | ||
} | ||
@@ -133,5 +156,2 @@ | ||
// Compiles the comment on the table. | ||
comment() {} | ||
changeType() {} | ||
@@ -138,0 +158,0 @@ |
@@ -13,2 +13,3 @@ // Oracledb Client | ||
const QueryCompiler = require('./query/oracledb-querycompiler'); | ||
const TableCompiler = require('./schema/oracledb-tablecompiler'); | ||
const ColumnCompiler = require('./schema/oracledb-columncompiler'); | ||
@@ -62,2 +63,6 @@ const { BlobHelper, ReturningHelper, isConnectionError } = require('./utils'); | ||
tableCompiler() { | ||
return new TableCompiler(this, ...arguments); | ||
} | ||
columnCompiler() { | ||
@@ -64,0 +69,0 @@ return new ColumnCompiler(this, ...arguments); |
@@ -204,2 +204,25 @@ const clone = require('lodash/clone'); | ||
with() { | ||
// WITH RECURSIVE is a syntax error in Oracle SQL. | ||
// So mark all statements as non-recursive, generate the SQL, then restore. | ||
// This approach ensures any changes in base class with() get propagated here. | ||
const undoList = []; | ||
if (this.grouped.with) { | ||
for (const stmt of this.grouped.with) { | ||
if (stmt.recursive) { | ||
undoList.push(stmt); | ||
stmt.recursive = false; | ||
} | ||
} | ||
} | ||
const result = super.with(); | ||
// Restore the recursive markings, in case this same query gets cloned and passed to other drivers. | ||
for (const stmt of undoList) { | ||
stmt.recursive = true; | ||
} | ||
return result; | ||
} | ||
_addReturningToSqlAndConvert(sql, outBinding, tableName, returning) { | ||
@@ -206,0 +229,0 @@ const self = this; |
@@ -24,2 +24,12 @@ /* eslint max-len: 0 */ | ||
_setNullableState(column, isNullable) { | ||
const constraintAction = isNullable ? 'drop not null' : 'set not null'; | ||
const sql = `alter table ${this.tableName()} alter column ${this.formatter.wrap( | ||
column | ||
)} ${constraintAction}`; | ||
return this.pushQuery({ | ||
sql: sql, | ||
}); | ||
} | ||
compileAdd(builder) { | ||
@@ -26,0 +36,0 @@ const table = this.formatter.wrap(builder); |
@@ -7,15 +7,8 @@ // SQLite3_DDL | ||
const find = require('lodash/find'); | ||
const fromPairs = require('lodash/fromPairs'); | ||
const isEmpty = require('lodash/isEmpty'); | ||
const negate = require('lodash/negate'); | ||
const omit = require('lodash/omit'); | ||
const identity = require('lodash/identity'); | ||
const { nanonum } = require('../../../util/nanoid'); | ||
const { COMMA_NO_PAREN_REGEX } = require('../../../constants'); | ||
const { | ||
createNewTable, | ||
copyAllData, | ||
copyData, | ||
dropOriginal, | ||
copyData, | ||
renameTable, | ||
@@ -29,2 +22,3 @@ getTableSql, | ||
} = require('./internal/compiler'); | ||
const { isEqualId, includesId } = require('./internal/utils'); | ||
@@ -51,16 +45,2 @@ // So altering the schema in SQLite3 is a major pain. | ||
async getColumn(column) { | ||
const currentCol = find(this.pragma, (col) => { | ||
return ( | ||
this.client.wrapIdentifier(col.name).toLowerCase() === | ||
this.client.wrapIdentifier(column).toLowerCase() | ||
); | ||
}); | ||
if (!currentCol) | ||
throw new Error( | ||
`The column ${column} is not in the ${this.tableName()} table` | ||
); | ||
return currentCol; | ||
} | ||
getTableSql() { | ||
@@ -89,12 +69,4 @@ const tableName = this.tableName(); | ||
async copyData(iterator) { | ||
const commands = await copyData( | ||
this.trx, | ||
iterator, | ||
this.tableName(), | ||
this.alteredName | ||
); | ||
for (const command of commands) { | ||
await this.trx.raw(command); | ||
} | ||
copyData(columns) { | ||
return this.trx.raw(copyData(this.tableName(), this.alteredName, columns)); | ||
} | ||
@@ -108,143 +80,3 @@ | ||
_doReplace(sql, from, to) { | ||
const oneLineSql = sql.replace(/\s+/g, ' '); | ||
const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); | ||
const tableName = matched[1]; | ||
const defs = matched[2]; | ||
if (!defs) { | ||
throw new Error('No column definitions in this statement!'); | ||
} | ||
let parens = 0, | ||
args = [], | ||
ptr = 0; | ||
let i = 0; | ||
const x = defs.length; | ||
for (i = 0; i < x; i++) { | ||
switch (defs[i]) { | ||
case '(': | ||
parens++; | ||
break; | ||
case ')': | ||
parens--; | ||
break; | ||
case ',': | ||
if (parens === 0) { | ||
args.push(defs.slice(ptr, i)); | ||
ptr = i + 1; | ||
} | ||
break; | ||
case ' ': | ||
if (ptr === i) { | ||
ptr = i + 1; | ||
} | ||
break; | ||
} | ||
} | ||
args.push(defs.slice(ptr, i)); | ||
const fromIdentifier = from.replace(/[`"'[\]]/g, ''); | ||
args = args.map((item) => { | ||
let split = item.trim().split(' '); | ||
// SQLite supports all quoting mechanisms prevalent in all major dialects of SQL | ||
// and preserves the original quoting in sqlite_master. | ||
// | ||
// Also, identifiers are never case sensitive, not even when quoted. | ||
// | ||
// Ref: https://www.sqlite.org/lang_keywords.html | ||
const fromMatchCandidates = [ | ||
new RegExp(`\`${fromIdentifier}\``, 'i'), | ||
new RegExp(`"${fromIdentifier}"`, 'i'), | ||
new RegExp(`'${fromIdentifier}'`, 'i'), | ||
new RegExp(`\\[${fromIdentifier}\\]`, 'i'), | ||
]; | ||
if (fromIdentifier.match(/^\S+$/)) { | ||
fromMatchCandidates.push(new RegExp(`\\b${fromIdentifier}\\b`, 'i')); | ||
} | ||
const doesMatchFromIdentifier = (target) => | ||
fromMatchCandidates.some((c) => target.match(c)); | ||
const replaceFromIdentifier = (target) => | ||
fromMatchCandidates.reduce( | ||
(result, candidate) => result.replace(candidate, to), | ||
target | ||
); | ||
if (doesMatchFromIdentifier(split[0])) { | ||
// 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; | ||
// 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])) { | ||
const ret = item.replace(/\(.*\)/, replaceFromIdentifier); | ||
// If any member columns are dropped then uniqueness/pk constraint | ||
// can not be retained | ||
if (ret !== item && isEmpty(to)) return ''; | ||
return ret; | ||
} | ||
// 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 | ||
const replacedKeySpec = replaceFromIdentifier(split[0]); | ||
if (split[0] !== replacedKeySpec) { | ||
// If we are removing one or more columns of a foreign | ||
// key, then we should not retain the key at all | ||
if (isEmpty(to)) return ''; | ||
else split[0] = replacedKeySpec; | ||
} | ||
if (split[1].slice(0, tableName.length) === tableName) { | ||
// self-referential foreign key | ||
const replacedKeyTargetSpec = split[1].replace( | ||
/\(.*\)/, | ||
replaceFromIdentifier | ||
); | ||
if (split[1] !== replacedKeyTargetSpec) { | ||
// If we are removing one or more columns of a foreign | ||
// key, then we should not retain the key at all | ||
if (isEmpty(to)) return ''; | ||
else split[1] = replacedKeyTargetSpec; | ||
} | ||
} | ||
return split.join(' references '); | ||
} | ||
return item; | ||
}); | ||
args = args.filter(negate(isEmpty)); | ||
if (args.length === 0) { | ||
throw new Error('Unable to drop last column from table'); | ||
} | ||
return oneLineSql | ||
.replace(/\(.*\)/, () => `(${args.join(', ')})`) | ||
.replace(/,\s*([,)])/, '$1'); | ||
} | ||
async alterColumn(columns) { | ||
alterColumn(columns) { | ||
return this.client.transaction( | ||
@@ -259,3 +91,5 @@ async (trx) => { | ||
parsedTable.columns = parsedTable.columns.map((column) => { | ||
const newColumnInfo = columns.find((c) => c.name === column.name); | ||
const newColumnInfo = columns.find((c) => | ||
isEqualId(c.name, column.name) | ||
); | ||
@@ -288,5 +122,3 @@ if (newColumnInfo) { | ||
return this.alter(newTable, createIndices, (row) => { | ||
return row; | ||
}); | ||
return this.alter(newTable, createIndices); | ||
}, | ||
@@ -297,44 +129,68 @@ { connection: this.connection } | ||
async dropColumn(columns) { | ||
dropColumn(columns) { | ||
return this.client.transaction( | ||
(trx) => { | ||
async (trx) => { | ||
this.trx = trx; | ||
return Promise.all(columns.map((column) => this.getColumn(column))) | ||
.then(() => this.getTableSql()) | ||
.then(({ createTable, createIndices }) => { | ||
let newSql = createTable.slice(); | ||
columns.forEach((column) => { | ||
const a = this.client.wrapIdentifier(column); | ||
newSql = this._doReplace(newSql, a, ''); | ||
}); | ||
if (createTable === newSql) { | ||
throw new Error('Unable to find the column to change'); | ||
} | ||
const mappedColumns = Object.keys( | ||
this.client.postProcessResponse( | ||
fromPairs(columns.map((column) => [column, column])) | ||
) | ||
); | ||
const newIndices = []; | ||
for (const createIndex of createIndices) { | ||
const parsedIndex = parseCreateIndex(createIndex); | ||
const { createTable, createIndices } = await this.getTableSql(); | ||
parsedIndex.columns = parsedIndex.columns.filter( | ||
(newColumn) => | ||
newColumn.expression || | ||
!columns.some( | ||
(column) => newColumn.name === this.formatter(column) | ||
) | ||
const parsedTable = parseCreateTable(createTable); | ||
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) | ||
); | ||
if (parsedIndex.columns.length > 0) { | ||
newIndices.push(compileCreateIndex(parsedIndex, this.wrap)); | ||
} | ||
} 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; | ||
} | ||
} | ||
); | ||
return this.alter(newSql, newIndices, (row) => | ||
omit(row, ...mappedColumns) | ||
); | ||
}); | ||
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); | ||
}, | ||
@@ -345,3 +201,3 @@ { connection: this.connection } | ||
async dropForeign(columns, foreignKeyName) { | ||
dropForeign(columns, foreignKeyName) { | ||
return this.client.transaction( | ||
@@ -358,3 +214,3 @@ async (trx) => { | ||
...column, | ||
references: columns.includes(column.name) | ||
references: includesId(columns, column.name) | ||
? null | ||
@@ -367,10 +223,17 @@ : column.references, | ||
(constraint) => { | ||
if (foreignKeyName) { | ||
return constraint.name !== foreignKeyName; | ||
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; | ||
} | ||
return ( | ||
constraint.columns.some((column) => columns.includes(column)) === | ||
false | ||
); | ||
} | ||
@@ -381,5 +244,3 @@ ); | ||
return this.alter(newTable, createIndices, (row) => { | ||
return row; | ||
}); | ||
return this.alter(newTable, createIndices); | ||
}, | ||
@@ -390,3 +251,3 @@ { connection: this.connection } | ||
async dropPrimary(constraintName) { | ||
dropPrimary(constraintName) { | ||
return this.client.transaction( | ||
@@ -398,35 +259,29 @@ async (trx) => { | ||
const oneLineSql = createTable.replace(/\s+/g, ' '); | ||
const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); | ||
const parsedTable = parseCreateTable(createTable); | ||
const defs = matched[2]; | ||
parsedTable.columns = parsedTable.columns.map((column) => ({ | ||
...column, | ||
primary: null, | ||
})); | ||
if (!defs) { | ||
throw new Error('No column definitions in this statement!'); | ||
} | ||
const updatedDefs = defs | ||
.split(COMMA_NO_PAREN_REGEX) | ||
.map((line) => line.trim()) | ||
.filter((defLine) => { | ||
if ( | ||
defLine.startsWith('constraint') === false && | ||
defLine.includes('primary key') === false | ||
) | ||
return true; | ||
if (constraintName) { | ||
if (defLine.includes(constraintName)) return false; | ||
return true; | ||
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; | ||
} | ||
}) | ||
.join(', '); | ||
} | ||
); | ||
const newSql = oneLineSql.replace(defs, updatedDefs); | ||
const newTable = compileCreateTable(parsedTable, this.wrap); | ||
return this.alter(newSql, createIndices, (row) => { | ||
return row; | ||
}); | ||
return this.alter(newTable, createIndices); | ||
}, | ||
@@ -437,3 +292,3 @@ { connection: this.connection } | ||
async primary(columns, constraintName) { | ||
primary(columns, constraintName) { | ||
return this.client.transaction( | ||
@@ -445,33 +300,28 @@ async (trx) => { | ||
const oneLineSql = createTable.replace(/\s+/g, ' '); | ||
const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); | ||
const parsedTable = parseCreateTable(createTable); | ||
const columnDefinitions = matched[2]; | ||
parsedTable.columns = parsedTable.columns.map((column) => ({ | ||
...column, | ||
primary: null, | ||
})); | ||
if (!columnDefinitions) { | ||
throw new Error('No column definitions in this statement!'); | ||
} | ||
parsedTable.constraints = parsedTable.constraints.filter( | ||
(constraint) => constraint.type !== 'PRIMARY KEY' | ||
); | ||
const primaryKeyDef = `primary key(${columns.join(',')})`; | ||
const constraintDef = constraintName | ||
? `constraint ${constraintName} ${primaryKeyDef}` | ||
: primaryKeyDef; | ||
parsedTable.constraints.push({ | ||
type: 'PRIMARY KEY', | ||
name: constraintName || null, | ||
columns: columns.map((column) => ({ | ||
name: column, | ||
expression: false, | ||
collation: null, | ||
order: null, | ||
})), | ||
conflict: null, | ||
}); | ||
const newColumnDefinitions = [ | ||
...columnDefinitions | ||
.split(COMMA_NO_PAREN_REGEX) | ||
.map((line) => line.trim()) | ||
.filter((line) => line.startsWith('primary') === false) | ||
.map((line) => line.replace(/primary key/i, '')), | ||
constraintDef, | ||
].join(', '); | ||
const newTable = compileCreateTable(parsedTable, this.wrap); | ||
const newSQL = oneLineSql.replace( | ||
columnDefinitions, | ||
newColumnDefinitions | ||
); | ||
return this.alter(newSQL, createIndices, (row) => { | ||
return row; | ||
}); | ||
return this.alter(newTable, createIndices); | ||
}, | ||
@@ -482,3 +332,3 @@ { connection: this.connection } | ||
async foreign(foreignInfo) { | ||
foreign(foreignInfo) { | ||
return this.client.transaction( | ||
@@ -490,46 +340,21 @@ async (trx) => { | ||
const oneLineSql = createTable.replace(/\s+/g, ' '); | ||
const matched = oneLineSql.match(/^CREATE TABLE\s+(\S+)\s*\((.*)\)/); | ||
const parsedTable = parseCreateTable(createTable); | ||
const columnDefinitions = matched[2]; | ||
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, | ||
}, | ||
}); | ||
if (!columnDefinitions) { | ||
throw new Error('No column definitions in this statement!'); | ||
} | ||
const newTable = compileCreateTable(parsedTable, this.wrap); | ||
const newColumnDefinitions = columnDefinitions | ||
.split(COMMA_NO_PAREN_REGEX) | ||
.map((line) => line.trim()); | ||
let newForeignSQL = ''; | ||
if (foreignInfo.keyName) { | ||
newForeignSQL += `CONSTRAINT ${foreignInfo.keyName}`; | ||
} | ||
newForeignSQL += ` FOREIGN KEY (${foreignInfo.column.join(', ')}) `; | ||
newForeignSQL += ` REFERENCES ${foreignInfo.inTable} (${foreignInfo.references})`; | ||
if (foreignInfo.onUpdate) { | ||
newForeignSQL += ` ON UPDATE ${foreignInfo.onUpdate}`; | ||
} | ||
if (foreignInfo.onDelete) { | ||
newForeignSQL += ` ON DELETE ${foreignInfo.onDelete}`; | ||
} | ||
newColumnDefinitions.push(newForeignSQL); | ||
const newSQL = oneLineSql.replace( | ||
columnDefinitions, | ||
newColumnDefinitions.join(', ') | ||
); | ||
return await this.generateAlterCommands( | ||
newSQL, | ||
createIndices, | ||
(row) => { | ||
return row; | ||
} | ||
); | ||
return this.generateAlterCommands(newTable, createIndices); | ||
}, | ||
@@ -540,12 +365,5 @@ { connection: this.connection } | ||
/** | ||
* @fixme | ||
* | ||
* There's a bunch of overlap between renameColumn/dropColumn/dropForeign/primary/foreign. | ||
* It'll be helpful to refactor this file heavily to combine/optimize some of these calls | ||
*/ | ||
async alter(newSql, createIndices, mapRow) { | ||
async alter(newSql, createIndices, columns) { | ||
await this.createNewTable(newSql); | ||
await this.copyData(mapRow); | ||
await this.copyData(columns); | ||
await this.dropOriginal(); | ||
@@ -559,7 +377,7 @@ await this.renameTable(); | ||
async generateAlterCommands(newSql, createIndices, mapRow) { | ||
generateAlterCommands(newSql, createIndices, columns) { | ||
const result = []; | ||
result.push(createNewTable(newSql, this.tableName(), this.alteredName)); | ||
result.push(copyAllData(this.tableName(), this.alteredName)); | ||
result.push(copyData(this.tableName(), this.alteredName, columns)); | ||
result.push(dropOriginal(this.tableName())); | ||
@@ -566,0 +384,0 @@ result.push(renameTable(this.alteredName, this.tableName())); |
@@ -5,3 +5,4 @@ const { tokenize } = require('./tokenizer'); | ||
const TOKENS = { | ||
keyword: /(?:ABORT|ACTION|ADD|AFTER|ALL|ALTER|ALWAYS|ANALYZE|AND|AS|ASC|ATTACH|AUTOINCREMENT|BEFORE|BEGIN|BETWEEN|BY|CASCADE|CASE|CAST|CHECK|COLLATE|COLUMN|COMMIT|CONFLICT|CONSTRAINT|CREATE|CROSS|CURRENT|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|DATABASE|DEFAULT|DEFERRED|DEFERRABLE|DELETE|DESC|DETACH|DISTINCT|DO|DROP|END|EACH|ELSE|ESCAPE|EXCEPT|EXCLUSIVE|EXCLUDE|EXISTS|EXPLAIN|FAIL|FILTER|FIRST|FOLLOWING|FOR|FOREIGN|FROM|FULL|GENERATED|GLOB|GROUP|GROUPS|HAVING|IF|IGNORE|IMMEDIATE|IN|INDEX|INDEXED|INITIALLY|INNER|INSERT|INSTEAD|INTERSECT|INTO|IS|ISNULL|JOIN|KEY|LAST|LEFT|LIKE|LIMIT|MATCH|MATERIALIZED|NATURAL|NO|NOT|NOTHING|NOTNULL|NULL|NULLS|OF|OFFSET|ON|OR|ORDER|OTHERS|OUTER|OVER|PARTITION|PLAN|PRAGMA|PRECEDING|PRIMARY|QUERY|RAISE|RANGE|RECURSIVE|REFERENCES|REGEXP|REINDEX|RELEASE|RENAME|REPLACE|RESTRICT|RETURNING|RIGHT|ROLLBACK|ROW|ROWS|SAVEPOINT|SELECT|SET|TABLE|TEMP|TEMPORARY|THEN|TIES|TO|TRANSACTION|TRIGGER|UNBOUNDED|UNION|UNIQUE|UPDATE|USING|VACUUM|VALUES|VIEW|VIRTUAL|WHEN|WHERE|WINDOW|WITH|WITHOUT)(?=\s+|-|\(|\)|;|\+|\*|\/|%|==|=|<=|<>|<<|<|>=|>>|>|!=|,|&|~|\|\||\||\.)/, | ||
keyword: | ||
/(?:ABORT|ACTION|ADD|AFTER|ALL|ALTER|ALWAYS|ANALYZE|AND|AS|ASC|ATTACH|AUTOINCREMENT|BEFORE|BEGIN|BETWEEN|BY|CASCADE|CASE|CAST|CHECK|COLLATE|COLUMN|COMMIT|CONFLICT|CONSTRAINT|CREATE|CROSS|CURRENT|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|DATABASE|DEFAULT|DEFERRED|DEFERRABLE|DELETE|DESC|DETACH|DISTINCT|DO|DROP|END|EACH|ELSE|ESCAPE|EXCEPT|EXCLUSIVE|EXCLUDE|EXISTS|EXPLAIN|FAIL|FILTER|FIRST|FOLLOWING|FOR|FOREIGN|FROM|FULL|GENERATED|GLOB|GROUP|GROUPS|HAVING|IF|IGNORE|IMMEDIATE|IN|INDEX|INDEXED|INITIALLY|INNER|INSERT|INSTEAD|INTERSECT|INTO|IS|ISNULL|JOIN|KEY|LAST|LEFT|LIKE|LIMIT|MATCH|MATERIALIZED|NATURAL|NO|NOT|NOTHING|NOTNULL|NULL|NULLS|OF|OFFSET|ON|OR|ORDER|OTHERS|OUTER|OVER|PARTITION|PLAN|PRAGMA|PRECEDING|PRIMARY|QUERY|RAISE|RANGE|RECURSIVE|REFERENCES|REGEXP|REINDEX|RELEASE|RENAME|REPLACE|RESTRICT|RETURNING|RIGHT|ROLLBACK|ROW|ROWS|SAVEPOINT|SELECT|SET|TABLE|TEMP|TEMPORARY|THEN|TIES|TO|TRANSACTION|TRIGGER|UNBOUNDED|UNION|UNIQUE|UPDATE|USING|VACUUM|VALUES|VIEW|VIRTUAL|WHEN|WHERE|WINDOW|WITH|WITHOUT)(?=\s+|-|\(|\)|;|\+|\*|\/|%|==|=|<=|<>|<<|<|>=|>>|>|!=|,|&|~|\|\||\||\.)/, | ||
id: /"[^"]*(?:""[^"]*)*"|`[^`]*(?:``[^`]*)*`|\[[^[\]]*\]|[a-z_][a-z0-9_$]*/, | ||
@@ -595,6 +596,4 @@ string: /'[^']*(?:''[^']*)*'/, | ||
function identifier(ctx) { | ||
return t({ type: 'id' }, (v) => | ||
/^["`[][^]*["`\]]$/.test(v.text) | ||
? v.text.substring(1, v.text.length - 1) | ||
: v.text | ||
return a([t({ type: 'id' }), t({ type: 'string' })], (v) => | ||
/^["`['][^]*["`\]']$/.test(v) ? v.substring(1, v.length - 1) : v | ||
)(ctx); | ||
@@ -620,2 +619,3 @@ } | ||
t({ type: 'string' }), | ||
t({ type: 'id' }), | ||
t({ type: 'blob' }), | ||
@@ -622,0 +622,0 @@ t({ text: 'NULL' }), |
@@ -1,16 +0,1 @@ | ||
const identity = require('lodash/identity'); | ||
const chunk = require('lodash/chunk'); | ||
function insertChunked(trx, chunkSize, target, iterator, existingData) { | ||
const result = []; | ||
iterator = iterator || identity; | ||
const chunked = chunk(existingData, chunkSize); | ||
for (const batch of chunked) { | ||
result.push( | ||
trx.queryBuilder().table(target).insert(batch.map(iterator)).toQuery() | ||
); | ||
} | ||
return result; | ||
} | ||
function createNewTable(sql, tablename, alteredName) { | ||
@@ -20,12 +5,10 @@ return sql.replace(tablename, alteredName); | ||
// ToDo To be removed | ||
async function copyData(trx, iterator, tableName, alteredName) { | ||
const existingData = await trx.raw(`SELECT * FROM "${tableName}"`); | ||
return insertChunked(trx, 20, alteredName, iterator, existingData); | ||
function copyData(sourceTable, targetTable, columns) { | ||
return `INSERT INTO ${targetTable} SELECT ${ | ||
columns === undefined | ||
? '*' | ||
: columns.map((column) => `"${column}"`).join(', ') | ||
} FROM ${sourceTable};`; | ||
} | ||
function copyAllData(sourceTable, targetTable) { | ||
return `INSERT INTO ${targetTable} SELECT * FROM ${sourceTable};`; | ||
} | ||
function dropOriginal(tableName) { | ||
@@ -44,8 +27,7 @@ return `DROP TABLE "${tableName}"`; | ||
module.exports = { | ||
copyAllData, | ||
createNewTable, | ||
copyData, | ||
dropOriginal, | ||
copyData, | ||
renameTable, | ||
getTableSql, | ||
}; |
@@ -84,2 +84,8 @@ const filter = require('lodash/filter'); | ||
columns = Array.isArray(columns) ? columns : [columns]; | ||
columns = columns.map((column) => | ||
this.client.customWrapIdentifier(column, identity) | ||
); | ||
indexName = this.client.customWrapIdentifier(indexName, identity); | ||
this.pushQuery({ | ||
@@ -99,2 +105,4 @@ sql: `PRAGMA table_info(${this.tableName()})`, | ||
constraintName = this.client.customWrapIdentifier(constraintName, identity); | ||
this.pushQuery({ | ||
@@ -151,4 +159,7 @@ sql: `PRAGMA table_info(${this.tableName()})`, | ||
columns = this.formatter.columnize(columns); | ||
columns = Array.isArray(columns) ? columns : [columns]; | ||
columns = columns.map((column) => | ||
this.client.customWrapIdentifier(column, identity) | ||
); | ||
constraintName = this.client.customWrapIdentifier(constraintName, identity); | ||
@@ -176,3 +187,3 @@ if (this.method !== 'create' && this.method !== 'createIfNot') { | ||
* @param {string | undefined} foreignInfo.keyName - Name of the foreign key constraint | ||
* @param {string} foreignInfo.references - What column it references in the other table | ||
* @param {string | string[]} foreignInfo.references - What column it references in the other table | ||
* @param {string} foreignInfo.inTable - What table is referenced in this constraint | ||
@@ -186,8 +197,18 @@ * @param {string} [foreignInfo.onUpdate] - What to do on updates | ||
if (this.method !== 'create' && this.method !== 'createIfNot') { | ||
foreignInfo.column = this.formatter.columnize(foreignInfo.column); | ||
foreignInfo.column = Array.isArray(foreignInfo.column) | ||
? foreignInfo.column | ||
: [foreignInfo.column]; | ||
foreignInfo.inTable = this.formatter.columnize(foreignInfo.inTable); | ||
foreignInfo.references = this.formatter.columnize(foreignInfo.references); | ||
foreignInfo.column = foreignInfo.column.map((column) => | ||
this.client.customWrapIdentifier(column, identity) | ||
); | ||
foreignInfo.inTable = this.client.customWrapIdentifier( | ||
foreignInfo.inTable, | ||
identity | ||
); | ||
foreignInfo.references = Array.isArray(foreignInfo.references) | ||
? foreignInfo.references | ||
: [foreignInfo.references]; | ||
foreignInfo.references = foreignInfo.references.map((column) => | ||
this.client.customWrapIdentifier(column, identity) | ||
); | ||
@@ -252,5 +273,15 @@ this.pushQuery({ | ||
_setNullableState(column, isNullable) { | ||
const fnCalled = isNullable ? '.setNullable' : '.dropNullable'; | ||
throw new Error(`${fnCalled} is not supported for SQLite.`); | ||
} | ||
dropColumn() { | ||
const compiler = this; | ||
const columns = values(arguments); | ||
const columnsWrapped = columns.map((column) => | ||
this.client.customWrapIdentifier(column, identity) | ||
); | ||
this.pushQuery({ | ||
@@ -261,3 +292,3 @@ sql: `PRAGMA table_info(${this.tableName()})`, | ||
.ddl(compiler, pragma, this.connection) | ||
.dropColumn(columns); | ||
.dropColumn(columnsWrapped); | ||
}, | ||
@@ -264,0 +295,0 @@ }); |
@@ -241,3 +241,3 @@ const { KnexTimeoutError } = require('../util/timeout'); | ||
for (const query of queries) { | ||
results.push(await this.query(query)); | ||
results.push(await this.queryArray([query])); | ||
} | ||
@@ -244,0 +244,0 @@ return results; |
@@ -72,3 +72,12 @@ const { parse } = require('pg-connection-string'); | ||
for (const [key, value] of parsed.searchParams.entries()) { | ||
connection[key] = value; | ||
const isMySQLLike = ['mysql:', 'mariadb:'].includes(parsed.protocol); | ||
if (isMySQLLike) { | ||
try { | ||
connection[key] = JSON.parse(value); | ||
} catch (err) { | ||
connection[key] = value; | ||
} | ||
} else { | ||
connection[key] = value; | ||
} | ||
} | ||
@@ -75,0 +84,0 @@ } |
@@ -86,5 +86,4 @@ // Migrator | ||
migrations.map(async (migration) => { | ||
const migrationContents = await this.config.migrationSource.getMigration( | ||
migration | ||
); | ||
const migrationContents = | ||
await this.config.migrationSource.getMigration(migration); | ||
return !this._useTransaction(migrationContents); | ||
@@ -105,65 +104,60 @@ }) | ||
// Runs the next migration that has not yet been run | ||
up(config) { | ||
async up(config) { | ||
this._disableProcessing(); | ||
this.config = getMergedConfig(config, this.config, this.knex.client.logger); | ||
return migrationListResolver | ||
.listAllAndCompleted(this.config, this.knex) | ||
.then((value) => { | ||
if (!this.config.disableMigrationsListValidation) { | ||
validateMigrationList(this.config.migrationSource, value); | ||
} | ||
return value; | ||
}) | ||
.then(([all, completed]) => { | ||
const newMigrations = getNewMigrations( | ||
this.config.migrationSource, | ||
all, | ||
completed | ||
); | ||
const allAndCompleted = await migrationListResolver.listAllAndCompleted( | ||
this.config, | ||
this.knex | ||
); | ||
let migrationToRun; | ||
const name = this.config.name; | ||
if (name) { | ||
if (!completed.includes(name)) { | ||
migrationToRun = newMigrations.find((migration) => { | ||
return ( | ||
this.config.migrationSource.getMigrationName(migration) === name | ||
); | ||
}); | ||
if (!migrationToRun) { | ||
throw new Error(`Migration "${name}" not found.`); | ||
} | ||
} | ||
} else { | ||
migrationToRun = newMigrations[0]; | ||
} | ||
if (!this.config.disableMigrationsListValidation) { | ||
validateMigrationList(this.config.migrationSource, allAndCompleted); | ||
} | ||
const [all, completed] = allAndCompleted; | ||
return { | ||
migrationToRun, | ||
useTransaction: | ||
!migrationToRun || | ||
this._useTransaction( | ||
this.config.migrationSource.getMigration(migrationToRun) | ||
), | ||
}; | ||
}) | ||
.then(({ migrationToRun, useTransaction }) => { | ||
const migrationsToRun = []; | ||
if (migrationToRun) { | ||
migrationsToRun.push(migrationToRun); | ||
const newMigrations = getNewMigrations( | ||
this.config.migrationSource, | ||
all, | ||
completed | ||
); | ||
let migrationToRun; | ||
const name = this.config.name; | ||
if (name) { | ||
if (!completed.includes(name)) { | ||
migrationToRun = newMigrations.find((migration) => { | ||
return ( | ||
this.config.migrationSource.getMigrationName(migration) === name | ||
); | ||
}); | ||
if (!migrationToRun) { | ||
throw new Error(`Migration "${name}" not found.`); | ||
} | ||
} | ||
} else { | ||
migrationToRun = newMigrations[0]; | ||
} | ||
const transactionForAll = | ||
!this.config.disableTransactions && | ||
(!migrationToRun || useTransaction); | ||
const useTransaction = | ||
!migrationToRun || | ||
this._useTransaction( | ||
await this.config.migrationSource.getMigration(migrationToRun) | ||
); | ||
if (transactionForAll) { | ||
return this.knex.transaction((trx) => { | ||
return this._runBatch(migrationsToRun, 'up', trx); | ||
}); | ||
} else { | ||
return this._runBatch(migrationsToRun, 'up'); | ||
} | ||
const migrationsToRun = []; | ||
if (migrationToRun) { | ||
migrationsToRun.push(migrationToRun); | ||
} | ||
const transactionForAll = | ||
!this.config.disableTransactions && (!migrationToRun || useTransaction); | ||
if (transactionForAll) { | ||
return await this.knex.transaction((trx) => { | ||
return this._runBatch(migrationsToRun, 'up', trx); | ||
}); | ||
} else { | ||
return await this._runBatch(migrationsToRun, 'up'); | ||
} | ||
} | ||
@@ -437,5 +431,4 @@ | ||
async _validateMigrationStructure(migration) { | ||
const migrationName = this.config.migrationSource.getMigrationName( | ||
migration | ||
); | ||
const migrationName = | ||
this.config.migrationSource.getMigrationName(migration); | ||
// maybe promise | ||
@@ -507,5 +500,4 @@ const migrationContent = await this.config.migrationSource.getMigration( | ||
this._activeMigration.fileName = name; | ||
const migrationContent = this.config.migrationSource.getMigration( | ||
migration | ||
); | ||
const migrationContent = | ||
this.config.migrationSource.getMigration(migration); | ||
@@ -512,0 +504,0 @@ // We're going to run each of the migrations in the current "up". |
@@ -21,3 +21,3 @@ const assert = require('assert'); | ||
partitionBy(column) { | ||
partitionBy(column, direction) { | ||
assert( | ||
@@ -32,3 +32,3 @@ Array.isArray(column) || typeof column === 'string', | ||
} else { | ||
this.partitions.push(column); | ||
this.partitions.push({ column: column, order: direction }); | ||
} | ||
@@ -38,3 +38,3 @@ return this; | ||
orderBy(column) { | ||
orderBy(column, direction) { | ||
assert( | ||
@@ -49,3 +49,3 @@ Array.isArray(column) || typeof column === 'string', | ||
} else { | ||
this.order.push(column); | ||
this.order.push({ column: column, order: direction }); | ||
} | ||
@@ -52,0 +52,0 @@ return this; |
@@ -117,9 +117,13 @@ // Builder | ||
with(alias, statement) { | ||
validateWithArgs(alias, statement, 'with'); | ||
return this.withWrapped(alias, statement); | ||
with(alias, statementOrColumnList, nothingOrStatement) { | ||
validateWithArgs(alias, statementOrColumnList, nothingOrStatement, 'with'); | ||
return this.withWrapped(alias, statementOrColumnList, nothingOrStatement); | ||
} | ||
// Helper for compiling any advanced `with` queries. | ||
withWrapped(alias, query) { | ||
withWrapped(alias, statementOrColumnList, nothingOrStatement) { | ||
const [query, columnList] = | ||
typeof nothingOrStatement === 'undefined' | ||
? [statementOrColumnList, undefined] | ||
: [nothingOrStatement, statementOrColumnList]; | ||
this._statements.push({ | ||
@@ -129,2 +133,3 @@ grouping: 'with', | ||
alias: alias, | ||
columnList, | ||
value: query, | ||
@@ -138,10 +143,19 @@ }); | ||
withRecursive(alias, statement) { | ||
validateWithArgs(alias, statement, 'withRecursive'); | ||
return this.withRecursiveWrapped(alias, statement); | ||
withRecursive(alias, statementOrColumnList, nothingOrStatement) { | ||
validateWithArgs( | ||
alias, | ||
statementOrColumnList, | ||
nothingOrStatement, | ||
'withRecursive' | ||
); | ||
return this.withRecursiveWrapped( | ||
alias, | ||
statementOrColumnList, | ||
nothingOrStatement | ||
); | ||
} | ||
// Helper for compiling any advanced `withRecursive` queries. | ||
withRecursiveWrapped(alias, query) { | ||
this.withWrapped(alias, query); | ||
withRecursiveWrapped(alias, statementOrColumnList, nothingOrStatement) { | ||
this.withWrapped(alias, statementOrColumnList, nothingOrStatement); | ||
this._statements[this._statements.length - 1].recursive = true; | ||
@@ -1253,5 +1267,6 @@ return this; | ||
Array.isArray(second) || | ||
typeof second === 'string', | ||
typeof second === 'string' || | ||
typeof second === 'object', | ||
`The second argument to an analytic function must be either a function, a raw, | ||
an array of string or a single string.` | ||
an array of string or object, an object or a single string.` | ||
); | ||
@@ -1261,4 +1276,6 @@ | ||
assert( | ||
Array.isArray(third) || typeof third === 'string', | ||
'The third argument to an analytic function must be either a string or an array of string.' | ||
Array.isArray(third) || | ||
typeof third === 'string' || | ||
typeof third === 'object', | ||
'The third argument to an analytic function must be either a string, an array of string or object or an object.' | ||
); | ||
@@ -1280,5 +1297,5 @@ } | ||
} else { | ||
const order = typeof second === 'string' ? [second] : second; | ||
const order = !Array.isArray(second) ? [second] : second; | ||
let partitions = third || []; | ||
partitions = typeof partitions === 'string' ? [partitions] : partitions; | ||
partitions = !Array.isArray(partitions) ? [partitions] : partitions; | ||
analytic = { | ||
@@ -1398,15 +1415,42 @@ grouping: 'columns', | ||
const validateWithArgs = function (alias, statement, method) { | ||
const isValidStatementArg = (statement) => | ||
typeof statement === 'function' || | ||
statement instanceof Builder || | ||
(statement && statement.isRawInstance); | ||
const validateWithArgs = function ( | ||
alias, | ||
statementOrColumnList, | ||
nothingOrStatement, | ||
method | ||
) { | ||
const [query, columnList] = | ||
typeof nothingOrStatement === 'undefined' | ||
? [statementOrColumnList, undefined] | ||
: [nothingOrStatement, statementOrColumnList]; | ||
if (typeof alias !== 'string') { | ||
throw new Error(`${method}() first argument must be a string`); | ||
} | ||
if ( | ||
typeof statement === 'function' || | ||
statement instanceof Builder || | ||
(statement && statement.isRawInstance) | ||
) { | ||
if (isValidStatementArg(query) && typeof columnList === 'undefined') { | ||
// Validated as two-arg variant (alias, statement). | ||
return; | ||
} | ||
// Attempt to interpret as three-arg variant (alias, columnList, statement). | ||
const isNonEmptyNameList = | ||
Array.isArray(columnList) && | ||
columnList.length > 0 && | ||
columnList.every((it) => typeof it === 'string'); | ||
if (!isNonEmptyNameList) { | ||
throw new Error( | ||
`${method}() second argument must be a statement or non-empty column name list.` | ||
); | ||
} | ||
if (isValidStatementArg(query)) { | ||
return; | ||
} | ||
throw new Error( | ||
`${method}() second argument must be a function / QueryBuilder or a raw` | ||
`${method}() third argument must be a function / QueryBuilder or a raw when its second argument is a column name list` | ||
); | ||
@@ -1413,0 +1457,0 @@ }; |
@@ -1038,3 +1038,5 @@ // Query Compiler | ||
map(stmt.partitions, function (partition) { | ||
return self.formatter.columnize(partition); | ||
if (isString(partition)) { | ||
return self.formatter.columnize(partition); | ||
} else return self.formatter.columnize(partition.column) + (partition.order ? ' ' + partition.order : ''); | ||
}).join(', ') + ' '; | ||
@@ -1045,3 +1047,5 @@ } | ||
sql += map(stmt.order, function (order) { | ||
return self.formatter.columnize(order); | ||
if (isString(order)) { | ||
return self.formatter.columnize(order); | ||
} else return self.formatter.columnize(order.column) + (order.order ? ' ' + order.order : ''); | ||
}).join(', '); | ||
@@ -1088,2 +1092,12 @@ } | ||
); | ||
const columnList = statement.columnList | ||
? '(' + | ||
columnize_( | ||
statement.columnList, | ||
this.builder, | ||
this.client, | ||
this.bindingsHolder | ||
) + | ||
')' | ||
: ''; | ||
return ( | ||
@@ -1097,2 +1111,3 @@ (val && | ||
) + | ||
columnList + | ||
' as (' + | ||
@@ -1099,0 +1114,0 @@ val + |
@@ -287,2 +287,22 @@ // TableBuilder | ||
setNullable(column) { | ||
this._statements.push({ | ||
grouping: 'alterTable', | ||
method: 'setNullable', | ||
args: [column], | ||
}); | ||
return this; | ||
}, | ||
dropNullable(column) { | ||
this._statements.push({ | ||
grouping: 'alterTable', | ||
method: 'dropNullable', | ||
args: [column], | ||
}); | ||
return this; | ||
}, | ||
// TODO: changeType | ||
@@ -289,0 +309,0 @@ }; |
@@ -289,2 +289,45 @@ /* eslint max-len:0 */ | ||
//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); | ||
} | ||
// If no name was specified for this index, we will create one using a basic | ||
@@ -314,4 +357,5 @@ // convention of the table name, followed by the columns, followed by an | ||
TableCompiler.prototype.alterColumnsPrefix = 'alter column '; | ||
TableCompiler.prototype.modifyColumnPrefix = 'modify column '; | ||
TableCompiler.prototype.dropColumnPrefix = 'drop column '; | ||
module.exports = TableCompiler; |
{ | ||
"name": "knex", | ||
"version": "0.95.10", | ||
"version": "0.95.11", | ||
"description": "A batteries-included SQL query & schema builder for Postgres, MySQL and SQLite3 and the Browser", | ||
@@ -29,2 +29,3 @@ "main": "knex", | ||
"test:mysql2": "cross-env DB=mysql2 npm run test:db", | ||
"test:oracledb": "cross-env DB=oracledb npm run test:db", | ||
"test:sqlite": "cross-env DB=sqlite3 npm run test:db", | ||
@@ -41,2 +42,4 @@ "test:postgres": "cross-env DB=postgres npm run test:db", | ||
"db:stop:mysql": "docker-compose -f scripts/docker-compose.yml down", | ||
"db:start:oracle": "docker-compose -f scripts/docker-compose.yml up --build -d oracledbxe && docker-compose -f scripts/docker-compose.yml up waitoracledbxe", | ||
"db:stop:oracle": "docker-compose -f scripts/docker-compose.yml down", | ||
"stress:init": "docker-compose -f scripts/stress-test/docker-compose.yml up --no-start && docker-compose -f scripts/stress-test/docker-compose.yml start", | ||
@@ -85,3 +88,3 @@ "stress:test": "node scripts/stress-test/knex-stress-test.js | grep -A 5 -B 60 -- '- STATS '", | ||
"devDependencies": { | ||
"@types/node": "^16.6.2", | ||
"@types/node": "^16.7.10", | ||
"chai": "^4.3.4", | ||
@@ -93,6 +96,6 @@ "chai-as-promised": "^7.1.1", | ||
"cross-env": "^7.0.3", | ||
"dtslint": "4.1.4", | ||
"dtslint": "4.1.6", | ||
"eslint": "^7.32.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.24.1", | ||
"eslint-plugin-import": "^2.24.2", | ||
"husky": "^4.3.8", | ||
@@ -102,3 +105,3 @@ "jake": "^8.1.1", | ||
"lint-staged": "^11.1.2", | ||
"mocha": "^9.1.0", | ||
"mocha": "^9.1.1", | ||
"mock-fs": "^4.13.0", | ||
@@ -123,3 +126,3 @@ "mysql": "^2.18.1", | ||
"tsd": "^0.17.0", | ||
"typescript": "4.3.5" | ||
"typescript": "4.4.2" | ||
}, | ||
@@ -126,0 +129,0 @@ "buildDependencies": [ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
728937
158
19164