Change endpoint from persons to people

This commit is contained in:
xfarrow
2025-03-23 21:00:08 +01:00
parent 4ae263662c
commit d005193f63
7158 changed files with 700476 additions and 735 deletions

View File

@ -0,0 +1,500 @@
// MSSQL Client
// -------
const map = require('lodash/map');
const isNil = require('lodash/isNil');
const Client = require('../../client');
const MSSQL_Formatter = require('./mssql-formatter');
const Transaction = require('./transaction');
const QueryCompiler = require('./query/mssql-querycompiler');
const SchemaCompiler = require('./schema/mssql-compiler');
const TableCompiler = require('./schema/mssql-tablecompiler');
const ViewCompiler = require('./schema/mssql-viewcompiler');
const ColumnCompiler = require('./schema/mssql-columncompiler');
const QueryBuilder = require('../../query/querybuilder');
const { setHiddenProperty } = require('../../util/security');
const debug = require('debug')('knex:mssql');
const SQL_INT4 = { MIN: -2147483648, MAX: 2147483647 };
const SQL_BIGINT_SAFE = { MIN: -9007199254740991, MAX: 9007199254740991 };
// Always initialize with the "QueryBuilder" and "QueryCompiler" objects, which
// extend the base 'lib/query/builder' and 'lib/query/compiler', respectively.
class Client_MSSQL extends Client {
constructor(config = {}) {
super(config);
}
/**
* @param {import('knex').Config} options
*/
_generateConnection() {
const settings = this.connectionSettings;
settings.options = settings.options || {};
/** @type {import('tedious').ConnectionConfig} */
const cfg = {
authentication: {
type: settings.type || 'default',
options: {
userName: settings.userName || settings.user,
password: settings.password,
domain: settings.domain,
token: settings.token,
clientId: settings.clientId,
clientSecret: settings.clientSecret,
tenantId: settings.tenantId,
msiEndpoint: settings.msiEndpoint,
},
},
server: settings.server || settings.host,
options: {
database: settings.database,
encrypt: settings.encrypt || false,
port: settings.port || 1433,
connectTimeout: settings.connectionTimeout || settings.timeout || 15000,
requestTimeout: !isNil(settings.requestTimeout)
? settings.requestTimeout
: 15000,
rowCollectionOnDone: false,
rowCollectionOnRequestCompletion: false,
useColumnNames: false,
tdsVersion: settings.options.tdsVersion || '7_4',
appName: settings.options.appName || 'knex',
trustServerCertificate: false,
...settings.options,
},
};
if (cfg.authentication.options.password) {
setHiddenProperty(cfg.authentication.options);
}
// tedious always connect via tcp when port is specified
if (cfg.options.instanceName) delete cfg.options.port;
if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000;
if (cfg.options.requestTimeout === Infinity) cfg.options.requestTimeout = 0;
if (cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0;
if (settings.debug) {
cfg.options.debug = {
packet: true,
token: true,
data: true,
payload: true,
};
}
return cfg;
}
_driver() {
const tds = require('tedious');
return tds;
}
formatter() {
return new MSSQL_Formatter(this, ...arguments);
}
transaction() {
return new Transaction(this, ...arguments);
}
queryCompiler() {
return new QueryCompiler(this, ...arguments);
}
schemaCompiler() {
return new SchemaCompiler(this, ...arguments);
}
tableCompiler() {
return new TableCompiler(this, ...arguments);
}
viewCompiler() {
return new ViewCompiler(this, ...arguments);
}
queryBuilder() {
const b = new QueryBuilder(this);
return b;
}
columnCompiler() {
return new ColumnCompiler(this, ...arguments);
}
wrapIdentifierImpl(value) {
if (value === '*') {
return '*';
}
return `[${value.replace(/[[\]]+/g, '')}]`;
}
// Get a raw connection, called by the `pool` whenever a new
// connection needs to be added to the pool.
acquireRawConnection() {
return new Promise((resolver, rejecter) => {
debug('connection::connection new connection requested');
const Driver = this._driver();
const settings = Object.assign({}, this._generateConnection());
const connection = new Driver.Connection(settings);
connection.connect((err) => {
if (err) {
debug('connection::connect error: %s', err.message);
return rejecter(err);
}
debug('connection::connect connected to server');
connection.connected = true;
connection.on('error', (e) => {
debug('connection::error message=%s', e.message);
connection.__knex__disposed = e;
connection.connected = false;
});
connection.once('end', () => {
connection.connected = false;
connection.__knex__disposed = 'Connection to server was terminated.';
debug('connection::end connection ended.');
});
return resolver(connection);
});
});
}
validateConnection(connection) {
return connection && connection.connected;
}
// Used to explicitly close a connection, called internally by the pool
// when a connection times out or the pool is shutdown.
destroyRawConnection(connection) {
debug('connection::destroy');
return new Promise((resolve) => {
connection.once('end', () => {
resolve();
});
connection.close();
});
}
// Position the bindings for the query.
positionBindings(sql) {
let questionCount = -1;
return sql.replace(/\\?\?/g, (match) => {
if (match === '\\?') {
return '?';
}
questionCount += 1;
return `@p${questionCount}`;
});
}
_chomp(connection) {
if (connection.state.name === 'LoggedIn') {
const nextRequest = this.requestQueue.pop();
if (nextRequest) {
debug(
'connection::query executing query, %d more in queue',
this.requestQueue.length
);
connection.execSql(nextRequest);
}
}
}
_enqueueRequest(request, connection) {
this.requestQueue.push(request);
this._chomp(connection);
}
_makeRequest(query, callback) {
const Driver = this._driver();
const sql = typeof query === 'string' ? query : query.sql;
let rowCount = 0;
if (!sql) throw new Error('The query is empty');
debug('request::request sql=%s', sql);
const request = new Driver.Request(sql, (err, remoteRowCount) => {
if (err) {
debug('request::error message=%s', err.message);
return callback(err);
}
rowCount = remoteRowCount;
debug('request::callback rowCount=%d', rowCount);
});
request.on('prepared', () => {
debug('request %s::request prepared', this.id);
});
request.on('done', (rowCount, more) => {
debug('request::done rowCount=%d more=%s', rowCount, more);
});
request.on('doneProc', (rowCount, more) => {
debug(
'request::doneProc id=%s rowCount=%d more=%s',
request.id,
rowCount,
more
);
});
request.on('doneInProc', (rowCount, more) => {
debug(
'request::doneInProc id=%s rowCount=%d more=%s',
request.id,
rowCount,
more
);
});
request.once('requestCompleted', () => {
debug('request::completed id=%s', request.id);
return callback(null, rowCount);
});
request.on('error', (err) => {
debug('request::error id=%s message=%s', request.id, err.message);
return callback(err);
});
return request;
}
// Grab a connection, run the query via the MSSQL streaming interface,
// and pass that through to the stream we've sent back to the client.
_stream(connection, query, /** @type {NodeJS.ReadWriteStream} */ stream) {
return new Promise((resolve, reject) => {
const request = this._makeRequest(query, (err) => {
if (err) {
stream.emit('error', err);
return reject(err);
}
resolve();
});
request.on('row', (row) => {
stream.write(
row.reduce(
(prev, curr) => ({
...prev,
[curr.metadata.colName]: curr.value,
}),
{}
)
);
});
request.on('error', (err) => {
stream.emit('error', err);
reject(err);
});
request.once('requestCompleted', () => {
stream.end();
resolve();
});
this._assignBindings(request, query.bindings);
this._enqueueRequest(request, connection);
});
}
_assignBindings(request, bindings) {
if (Array.isArray(bindings)) {
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
this._setReqInput(request, i, binding);
}
}
}
_scaleForBinding(binding) {
if (binding % 1 === 0) {
throw new Error(`The binding value ${binding} must be a decimal number.`);
}
return { scale: 10 };
}
_typeForBinding(binding) {
const Driver = this._driver();
if (
this.connectionSettings.options &&
this.connectionSettings.options.mapBinding
) {
const result = this.connectionSettings.options.mapBinding(binding);
if (result) {
return [result.value, result.type];
}
}
switch (typeof binding) {
case 'string':
return [binding, Driver.TYPES.NVarChar];
case 'boolean':
return [binding, Driver.TYPES.Bit];
case 'number': {
if (binding % 1 !== 0) {
return [binding, Driver.TYPES.Float];
}
if (binding < SQL_INT4.MIN || binding > SQL_INT4.MAX) {
if (binding < SQL_BIGINT_SAFE.MIN || binding > SQL_BIGINT_SAFE.MAX) {
throw new Error(
`Bigint must be safe integer or must be passed as string, saw ${binding}`
);
}
return [binding, Driver.TYPES.BigInt];
}
return [binding, Driver.TYPES.Int];
}
default: {
if (binding instanceof Date) {
return [binding, Driver.TYPES.DateTime];
}
if (binding instanceof Buffer) {
return [binding, Driver.TYPES.VarBinary];
}
return [binding, Driver.TYPES.NVarChar];
}
}
}
// Runs the query on the specified connection, providing the bindings
// and any other necessary prep work.
_query(connection, query) {
return new Promise((resolve, reject) => {
const rows = [];
const request = this._makeRequest(query, (err, count) => {
if (err) {
return reject(err);
}
query.response = rows;
process.nextTick(() => this._chomp(connection));
resolve(query);
});
request.on('row', (row) => {
debug('request::row');
rows.push(row);
});
this._assignBindings(request, query.bindings);
this._enqueueRequest(request, connection);
});
}
// sets a request input parameter. Detects bigints and decimals and sets type appropriately.
_setReqInput(req, i, inputBinding) {
const [binding, tediousType] = this._typeForBinding(inputBinding);
const bindingName = 'p'.concat(i);
let options;
if (typeof binding === 'number' && binding % 1 !== 0) {
options = this._scaleForBinding(binding);
}
debug(
'request::binding pos=%d type=%s value=%s',
i,
tediousType.name,
binding
);
if (Buffer.isBuffer(binding)) {
options = {
length: 'max',
};
}
req.addParameter(bindingName, tediousType, binding, options);
}
// Process the response as returned from the query.
processResponse(query, runner) {
if (query == null) return;
let { response } = query;
const { method } = query;
if (query.output) {
return query.output.call(runner, response);
}
response = response.map((row) =>
row.reduce((columns, r) => {
const colName = r.metadata.colName;
if (columns[colName]) {
if (!Array.isArray(columns[colName])) {
columns[colName] = [columns[colName]];
}
columns[colName].push(r.value);
} else {
columns[colName] = r.value;
}
return columns;
}, {})
);
if (query.output) return query.output.call(runner, response);
switch (method) {
case 'select':
return response;
case 'first':
return response[0];
case 'pluck':
return map(response, query.pluck);
case 'insert':
case 'del':
case 'update':
case 'counter':
if (query.returning) {
if (query.returning === '@@rowcount') {
return response[0][''];
}
}
return response;
default:
return response;
}
}
}
Object.assign(Client_MSSQL.prototype, {
requestQueue: [],
dialect: 'mssql',
driverName: 'mssql',
});
module.exports = Client_MSSQL;

View File

@ -0,0 +1,34 @@
const Formatter = require('../../formatter');
class MSSQL_Formatter extends Formatter {
// Accepts a string or array of columns to wrap as appropriate.
columnizeWithPrefix(prefix, target) {
const columns = typeof target === 'string' ? [target] : target;
let str = '',
i = -1;
while (++i < columns.length) {
if (i > 0) str += ', ';
str += prefix + this.wrap(columns[i]);
}
return str;
}
/**
* 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;

View File

@ -0,0 +1,601 @@
// MSSQL Query Compiler
// ------
const QueryCompiler = require('../../../query/querycompiler');
const compact = require('lodash/compact');
const identity = require('lodash/identity');
const isEmpty = require('lodash/isEmpty');
const Raw = require('../../../raw.js');
const {
columnize: columnize_,
} = require('../../../formatter/wrappingFormatter');
const components = [
'comments',
'columns',
'join',
'lock',
'where',
'union',
'group',
'having',
'order',
'limit',
'offset',
];
class QueryCompiler_MSSQL extends QueryCompiler {
constructor(client, builder, formatter) {
super(client, builder, formatter);
const { onConflict } = this.single;
if (onConflict) {
throw new Error('.onConflict() is not supported for mssql.');
}
this._emptyInsertValue = 'default values';
}
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() {
const sql = this.with();
const statements = components.map((component) => this[component](this));
return sql + compact(statements).join(' ');
}
//#region Insert
// Compiles an "insert" query, allowing for multiple
// inserts using a single query statement.
insert() {
if (
this.single.options &&
this.single.options.includeTriggerModifications
) {
return this.insertWithTriggers();
} else {
return this.standardInsert();
}
}
insertWithTriggers() {
const insertValues = this.single.insert || [];
const { returning } = this.single;
let sql =
this.with() +
`${this._buildTempTable(returning)}insert into ${this.tableName} `;
const returningSql = returning
? this._returning('insert', returning, true) + ' '
: '';
if (Array.isArray(insertValues)) {
if (insertValues.length === 0) {
return '';
}
} else if (typeof insertValues === 'object' && isEmpty(insertValues)) {
return {
sql:
sql +
returningSql +
this._emptyInsertValue +
this._buildReturningSelect(returning),
returning,
};
}
sql += this._buildInsertData(insertValues, returningSql);
if (returning) {
sql += this._buildReturningSelect(returning);
}
return {
sql,
returning,
};
}
_buildInsertData(insertValues, returningSql) {
let sql = '';
const insertData = this._prepInsert(insertValues);
if (typeof insertData === 'string') {
sql += insertData;
} else {
if (insertData.columns.length) {
sql += `(${this.formatter.columnize(insertData.columns)}`;
sql +=
`) ${returningSql}values (` +
this._buildInsertValues(insertData) +
')';
} else if (insertValues.length === 1 && insertValues[0]) {
sql += returningSql + this._emptyInsertValue;
} else {
return '';
}
}
return sql;
}
standardInsert() {
const insertValues = this.single.insert || [];
let sql = this.with() + `insert into ${this.tableName} `;
const { returning } = this.single;
const returningSql = returning
? this._returning('insert', returning) + ' '
: '';
if (Array.isArray(insertValues)) {
if (insertValues.length === 0) {
return '';
}
} else if (typeof insertValues === 'object' && isEmpty(insertValues)) {
return {
sql: sql + returningSql + this._emptyInsertValue,
returning,
};
}
sql += this._buildInsertData(insertValues, returningSql);
return {
sql,
returning,
};
}
//#endregion
//#region Update
// Compiles an `update` query, allowing for a return value.
update() {
if (
this.single.options &&
this.single.options.includeTriggerModifications
) {
return this.updateWithTriggers();
} else {
return this.standardUpdate();
}
}
updateWithTriggers() {
const top = this.top();
const withSQL = this.with();
const updates = this._prepUpdate(this.single.update);
const join = this.join();
const where = this.where();
const order = this.order();
const { returning } = this.single;
const declaredTemp = this._buildTempTable(returning);
return {
sql:
withSQL +
declaredTemp +
`update ${top ? top + ' ' : ''}${this.tableName}` +
' set ' +
updates.join(', ') +
(returning ? ` ${this._returning('update', returning, true)}` : '') +
(join ? ` from ${this.tableName} ${join}` : '') +
(where ? ` ${where}` : '') +
(order ? ` ${order}` : '') +
(!returning
? this._returning('rowcount', '@@rowcount')
: this._buildReturningSelect(returning)),
returning: returning || '@@rowcount',
};
}
_formatGroupsItemValue(value, nulls) {
const column = super._formatGroupsItemValue(value);
// MSSQL dont support 'is null' syntax in order by,
// so we override this function and add MSSQL specific syntax.
if (nulls && !(value instanceof Raw)) {
const collNulls = `IIF(${column} is null,`;
if (nulls === 'first') {
return `${collNulls}0,1)`;
} else if (nulls === 'last') {
return `${collNulls}1,0)`;
}
}
return column;
}
standardUpdate() {
const top = this.top();
const withSQL = this.with();
const updates = this._prepUpdate(this.single.update);
const join = this.join();
const where = this.where();
const order = this.order();
const { returning } = this.single;
return {
sql:
withSQL +
`update ${top ? top + ' ' : ''}${this.tableName}` +
' set ' +
updates.join(', ') +
(returning ? ` ${this._returning('update', returning)}` : '') +
(join ? ` from ${this.tableName} ${join}` : '') +
(where ? ` ${where}` : '') +
(order ? ` ${order}` : '') +
(!returning ? this._returning('rowcount', '@@rowcount') : ''),
returning: returning || '@@rowcount',
};
}
//#endregion
//#region Delete
// Compiles a `delete` query.
del() {
if (
this.single.options &&
this.single.options.includeTriggerModifications
) {
return this.deleteWithTriggers();
} else {
return this.standardDelete();
}
}
deleteWithTriggers() {
// Make sure tableName is processed by the formatter first.
const withSQL = this.with();
const { tableName } = this;
const wheres = this.where();
const joins = this.join();
const { returning } = this.single;
const returningStr = returning
? ` ${this._returning('del', returning, true)}`
: '';
const deleteSelector = joins ? `${tableName}${returningStr} ` : '';
return {
sql:
withSQL +
`${this._buildTempTable(
returning
)}delete ${deleteSelector}from ${tableName}` +
(!joins ? returningStr : '') +
(joins ? ` ${joins}` : '') +
(wheres ? ` ${wheres}` : '') +
(!returning
? this._returning('rowcount', '@@rowcount')
: this._buildReturningSelect(returning)),
returning: returning || '@@rowcount',
};
}
standardDelete() {
// Make sure tableName is processed by the formatter first.
const withSQL = this.with();
const { tableName } = this;
const wheres = this.where();
const joins = this.join();
const { returning } = this.single;
const returningStr = returning
? ` ${this._returning('del', returning)}`
: '';
// returning needs to be before "from" when using join
const deleteSelector = joins ? `${tableName}${returningStr} ` : '';
return {
sql:
withSQL +
`delete ${deleteSelector}from ${tableName}` +
(!joins ? returningStr : '') +
(joins ? ` ${joins}` : '') +
(wheres ? ` ${wheres}` : '') +
(!returning ? this._returning('rowcount', '@@rowcount') : ''),
returning: returning || '@@rowcount',
};
}
//#endregion
// Compiles the columns in the query, specifying if an item was distinct.
columns() {
let distinctClause = '';
if (this.onlyUnions()) return '';
const top = this.top();
const hints = this._hintComments();
const columns = this.grouped.columns || [];
let i = -1,
sql = [];
if (columns) {
while (++i < columns.length) {
const stmt = columns[i];
if (stmt.distinct) distinctClause = 'distinct ';
if (stmt.distinctOn) {
distinctClause = this.distinctOn(stmt.value);
continue;
}
if (stmt.type === 'aggregate') {
sql.push(...this.aggregate(stmt));
} else if (stmt.type === 'aggregateRaw') {
sql.push(this.aggregateRaw(stmt));
} else if (stmt.type === 'analytic') {
sql.push(this.analytic(stmt));
} else if (stmt.type === 'json') {
sql.push(this.json(stmt));
} else if (stmt.value && stmt.value.length > 0) {
sql.push(this.formatter.columnize(stmt.value));
}
}
}
if (sql.length === 0) sql = ['*'];
const select = this.onlyJson() ? '' : 'select ';
return (
`${select}${hints}${distinctClause}` +
(top ? top + ' ' : '') +
sql.join(', ') +
(this.tableName ? ` from ${this.tableName}` : '')
);
}
_returning(method, value, withTrigger) {
switch (method) {
case 'update':
case 'insert':
return value
? `output ${this.formatter.columnizeWithPrefix('inserted.', value)}${
withTrigger ? ' into #out' : ''
}`
: '';
case 'del':
return value
? `output ${this.formatter.columnizeWithPrefix('deleted.', value)}${
withTrigger ? ' into #out' : ''
}`
: '';
case 'rowcount':
return value ? ';select @@rowcount' : '';
}
}
_buildTempTable(values) {
// If value is nothing then return an empty string
if (values && values.length > 0) {
let selections = '';
// Build values that will be returned from this procedure
if (Array.isArray(values)) {
selections = values
.map((value) => `[t].${this.formatter.columnize(value)}`)
.join(',');
} else {
selections = `[t].${this.formatter.columnize(values)}`;
}
// Force #out to be correctly populated with the correct column structure.
let sql = `select top(0) ${selections} into #out `;
sql += `from ${this.tableName} as t `;
sql += `left join ${this.tableName} on 0=1;`;
return sql;
}
return '';
}
_buildReturningSelect(values) {
// If value is nothing then return an empty string
if (values && values.length > 0) {
let selections = '';
// Build columns to return
if (Array.isArray(values)) {
selections = values
.map((value) => `${this.formatter.columnize(value)}`)
.join(',');
} else {
selections = this.formatter.columnize(values);
}
// Get the returned values
let sql = `; select ${selections} from #out; `;
// Drop the temp table to prevent memory leaks
sql += `drop table #out;`;
return sql;
}
return '';
}
// Compiles a `truncate` query.
truncate() {
return `truncate table ${this.tableName}`;
}
forUpdate() {
// this doesn't work exacltly as it should, one should also mention index while locking
// https://stackoverflow.com/a/9818448/360060
return 'with (UPDLOCK)';
}
forShare() {
// http://www.sqlteam.com/article/introduction-to-locking-in-sql-server
return 'with (HOLDLOCK)';
}
// Compiles a `columnInfo` query.
columnInfo() {
const column = this.single.columnInfo;
let schema = this.single.schema;
// The user may have specified a custom wrapIdentifier function in the config. We
// need to run the identifiers through that function, but not format them as
// identifiers otherwise.
const table = this.client.customWrapIdentifier(this.single.table, identity);
if (schema) {
schema = this.client.customWrapIdentifier(schema, identity);
}
// 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()];
if (schema) {
sql += ' and table_schema = ?';
bindings.push(schema);
} else {
sql += ` and table_schema = 'dbo'`;
}
return {
sql,
bindings: bindings,
output(resp) {
const out = resp.reduce((columns, val) => {
columns[val[0].value] = {
defaultValue: val[1].value,
type: val[2].value,
maxLength: val[3].value,
nullable: val[4].value === 'YES',
};
return columns;
}, {});
return (column && out[column]) || out;
},
};
}
top() {
const noLimit = !this.single.limit && this.single.limit !== 0;
const noOffset = !this.single.offset;
if (noLimit || !noOffset) return '';
return `top (${this._getValueOrParameterFromAttribute('limit')})`;
}
limit() {
return '';
}
offset() {
const noLimit = !this.single.limit && this.single.limit !== 0;
const noOffset = !this.single.offset;
if (noOffset) return '';
let offset = `offset ${
noOffset ? '0' : this._getValueOrParameterFromAttribute('offset')
} rows`;
if (!noLimit) {
offset += ` fetch next ${this._getValueOrParameterFromAttribute(
'limit'
)} rows only`;
}
return offset;
}
whereLike(statement) {
return `${this._columnClause(
statement
)} collate SQL_Latin1_General_CP1_CS_AS ${this._not(
statement,
'like '
)}${this._valueClause(statement)}`;
}
whereILike(statement) {
return `${this._columnClause(
statement
)} collate SQL_Latin1_General_CP1_CI_AS ${this._not(
statement,
'like '
)}${this._valueClause(statement)}`;
}
jsonExtract(params) {
// JSON_VALUE return NULL if we query object or array
// JSON_QUERY return NULL if we query literal/single value
return this._jsonExtract(
params.singleValue ? 'JSON_VALUE' : 'JSON_QUERY',
params
);
}
jsonSet(params) {
return this._jsonSet('JSON_MODIFY', params);
}
jsonInsert(params) {
return this._jsonSet('JSON_MODIFY', params);
}
jsonRemove(params) {
const jsonCol = `JSON_MODIFY(${columnize_(
params.column,
this.builder,
this.client,
this.bindingsHolder
)},${this.client.parameter(
params.path,
this.builder,
this.bindingsHolder
)}, NULL)`;
return params.alias
? this.client.alias(jsonCol, this.formatter.wrap(params.alias))
: jsonCol;
}
whereJsonPath(statement) {
return this._whereJsonPath('JSON_VALUE', statement);
}
whereJsonSupersetOf(statement) {
throw new Error(
'Json superset where clause not actually supported by MSSQL'
);
}
whereJsonSubsetOf(statement) {
throw new Error('Json subset where clause not actually supported by MSSQL');
}
_getExtracts(statement, operator) {
const column = columnize_(
statement.column,
this.builder,
this.client,
this.bindingsHolder
);
return (
Array.isArray(statement.values) ? statement.values : [statement.values]
)
.map(function (value) {
return (
'JSON_VALUE(' +
column +
',' +
this.client.parameter(value, this.builder, this.bindingsHolder) +
')'
);
}, this)
.join(operator);
}
onJsonPathEquals(clause) {
return this._onJsonPathEquals('JSON_VALUE', clause);
}
}
// Set the QueryBuilder & QueryCompiler on the client object,
// in case anyone wants to modify things to suit their own purposes.
module.exports = QueryCompiler_MSSQL;

View File

@ -0,0 +1,185 @@
// MSSQL Column Compiler
// -------
const ColumnCompiler = require('../../../schema/columncompiler');
const { toNumber } = require('../../../util/helpers');
const { formatDefault } = require('../../../formatter/formatterUtils');
const { operator: operator_ } = require('../../../formatter/wrappingFormatter');
class ColumnCompiler_MSSQL extends ColumnCompiler {
constructor(client, tableCompiler, columnBuilder) {
super(client, tableCompiler, columnBuilder);
this.modifiers = ['nullable', 'defaultTo', 'first', 'after', 'comment'];
this._addCheckModifiers();
}
// Types
// ------
double(precision, scale) {
return 'float';
}
floating(precision, scale) {
// ignore precicion / scale which is mysql specific stuff
return `float`;
}
integer() {
// mssql does not support length
return 'int';
}
tinyint() {
// mssql does not support length
return 'tinyint';
}
varchar(length) {
return `nvarchar(${toNumber(length, 255)})`;
}
timestamp({ useTz = false } = {}) {
return useTz ? 'datetimeoffset' : 'datetime2';
}
bit(length) {
if (length > 1) {
this.client.logger.warn('Bit field is exactly 1 bit length for MSSQL');
}
return 'bit';
}
binary(length) {
return length ? `varbinary(${toNumber(length)})` : 'varbinary(max)';
}
// Modifiers
// ------
first() {
this.client.logger.warn('Column first modifier not available for MSSQL');
return '';
}
after(column) {
this.client.logger.warn('Column after modifier not available for MSSQL');
return '';
}
defaultTo(value, { constraintName } = {}) {
const formattedValue = formatDefault(value, this.type, this.client);
constraintName =
typeof constraintName !== 'undefined'
? constraintName
: `${
this.tableCompiler.tableNameRaw
}_${this.getColumnName()}_default`.toLowerCase();
if (this.columnBuilder._method === 'alter') {
this.pushAdditional(function () {
this.pushQuery(
`ALTER TABLE ${this.tableCompiler.tableName()} ADD CONSTRAINT ${this.formatter.wrap(
constraintName
)} DEFAULT ${formattedValue} FOR ${this.formatter.wrap(
this.getColumnName()
)}`
);
});
return '';
}
if (!constraintName) {
return `DEFAULT ${formattedValue}`;
}
return `CONSTRAINT ${this.formatter.wrap(
constraintName
)} DEFAULT ${formattedValue}`;
}
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.
// (Yes, even if the column has its own collation, the sqlvariant still uses the database collation.)
// I'm not sure we even need to raise a warning, as MSSQL will return an error when the limit is exceeded itself.
if (comment && 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#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 '';
}
checkLength(operator, length, constraintName) {
return this._check(
`LEN(${this.formatter.wrap(this.getColumnName())}) ${operator_(
operator,
this.columnBuilder,
this.bindingsHolder
)} ${toNumber(length)}`,
constraintName
);
}
checkRegex(regex, constraintName) {
return this._check(
`${this.formatter.wrap(
this.getColumnName()
)} LIKE ${this.client._escapeBinding('%' + regex + '%')}`,
constraintName
);
}
increments(options = { primaryKey: true }) {
return (
'int identity(1,1) not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}
bigincrements(options = { primaryKey: true }) {
return (
'bigint identity(1,1) not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}
}
ColumnCompiler_MSSQL.prototype.bigint = 'bigint';
ColumnCompiler_MSSQL.prototype.mediumint = 'int';
ColumnCompiler_MSSQL.prototype.smallint = 'smallint';
ColumnCompiler_MSSQL.prototype.text = 'nvarchar(max)';
ColumnCompiler_MSSQL.prototype.mediumtext = 'nvarchar(max)';
ColumnCompiler_MSSQL.prototype.longtext = 'nvarchar(max)';
ColumnCompiler_MSSQL.prototype.json = ColumnCompiler_MSSQL.prototype.jsonb =
'nvarchar(max)';
// TODO: mssql supports check constraints as of SQL Server 2008
// so make enu here more like postgres
ColumnCompiler_MSSQL.prototype.enu = 'nvarchar(100)';
ColumnCompiler_MSSQL.prototype.uuid = ({ useBinaryUuid = false } = {}) =>
useBinaryUuid ? 'binary(16)' : 'uniqueidentifier';
ColumnCompiler_MSSQL.prototype.datetime = 'datetime2';
ColumnCompiler_MSSQL.prototype.bool = 'bit';
module.exports = ColumnCompiler_MSSQL;

View File

@ -0,0 +1,91 @@
// MySQL Schema Compiler
// -------
const SchemaCompiler = require('../../../schema/compiler');
class SchemaCompiler_MSSQL extends SchemaCompiler {
constructor(client, builder) {
super(client, builder);
}
dropTableIfExists(tableName) {
const name = this.formatter.wrap(prefixedTableName(this.schema, tableName));
this.pushQuery(
`if object_id('${name}', 'U') is not null DROP TABLE ${name}`
);
}
dropViewIfExists(viewName) {
const name = this.formatter.wrap(prefixedTableName(this.schema, viewName));
this.pushQuery(
`if object_id('${name}', 'V') is not null DROP VIEW ${name}`
);
}
// Rename a table on the schema.
renameTable(tableName, to) {
this.pushQuery(
`exec sp_rename ${this.client.parameter(
prefixedTableName(this.schema, tableName),
this.builder,
this.bindingsHolder
)}, ${this.client.parameter(to, this.builder, this.bindingsHolder)}`
);
}
renameView(viewTable, to) {
this.pushQuery(
`exec sp_rename ${this.client.parameter(
prefixedTableName(this.schema, viewTable),
this.builder,
this.bindingsHolder
)}, ${this.client.parameter(to, this.builder, this.bindingsHolder)}`
);
}
// Check whether a table exists on the query.
hasTable(tableName) {
const formattedTable = this.client.parameter(
prefixedTableName(this.schema, tableName),
this.builder,
this.bindingsHolder
);
const bindings = [tableName];
let sql =
`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ` +
`WHERE TABLE_NAME = ${formattedTable}`;
if (this.schema) {
sql += ' AND TABLE_SCHEMA = ?';
bindings.push(this.schema);
}
this.pushQuery({ sql, bindings, output: (resp) => resp.length > 0 });
}
// Check whether a column exists on the schema.
hasColumn(tableName, column) {
const formattedColumn = this.client.parameter(
column,
this.builder,
this.bindingsHolder
);
const formattedTable = this.client.parameter(
this.formatter.wrap(prefixedTableName(this.schema, tableName)),
this.builder,
this.bindingsHolder
);
const sql =
`select object_id from sys.columns ` +
`where name = ${formattedColumn} ` +
`and object_id = object_id(${formattedTable})`;
this.pushQuery({ sql, output: (resp) => resp.length > 0 });
}
}
SchemaCompiler_MSSQL.prototype.dropTablePrefix = 'DROP TABLE ';
function prefixedTableName(prefix, table) {
return prefix ? `${prefix}.${table}` : table;
}
module.exports = SchemaCompiler_MSSQL;

View File

@ -0,0 +1,378 @@
/* eslint max-len:0 */
// MSSQL Table Builder & Compiler
// -------
const TableCompiler = require('../../../schema/tablecompiler');
const helpers = require('../../../util/helpers');
const { isObject } = require('../../../util/is');
// Table Compiler
// ------
class TableCompiler_MSSQL extends TableCompiler {
constructor(client, tableBuilder) {
super(client, tableBuilder);
}
createQuery(columns, ifNot, like) {
let createStatement = ifNot
? `if object_id('${this.tableName()}', 'U') is null `
: '';
if (like) {
// This query copy only columns and not all indexes and keys like other databases.
createStatement += `SELECT * INTO ${this.tableName()} FROM ${this.tableNameLike()} WHERE 0=1`;
} else {
createStatement +=
'CREATE TABLE ' +
this.tableName() +
(this._formatting ? ' (\n ' : ' (') +
columns.sql.join(this._formatting ? ',\n ' : ', ') +
this._addChecks() +
')';
}
this.pushQuery(createStatement);
if (this.single.comment) {
this.comment(this.single.comment);
}
if (like) {
this.addColumns(columns, this.addColumnsPrefix);
}
}
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}`
);
}
// Compiles column add. Multiple columns need only one ADD clause (not one ADD per column) so core addColumns doesn't work. #1348
addColumns(columns, prefix) {
prefix = prefix || this.addColumnsPrefix;
if (columns.sql.length > 0) {
this.pushQuery({
sql:
(this.lowerCase ? 'alter table ' : 'ALTER TABLE ') +
this.tableName() +
' ' +
prefix +
columns.sql.join(', '),
bindings: columns.bindings,
});
}
}
alterColumns(columns, colBuilder) {
for (let i = 0, l = colBuilder.length; i < l; i++) {
const builder = colBuilder[i];
if (builder.modified.defaultTo) {
const schema = this.schemaNameRaw || 'dbo';
const baseQuery = `
DECLARE @constraint varchar(100) = (SELECT default_constraints.name
FROM sys.all_columns
INNER JOIN sys.tables
ON all_columns.object_id = tables.object_id
INNER JOIN sys.schemas
ON tables.schema_id = schemas.schema_id
INNER JOIN sys.default_constraints
ON all_columns.default_object_id = default_constraints.object_id
WHERE schemas.name = '${schema}'
AND tables.name = '${
this.tableNameRaw
}'
AND all_columns.name = '${builder.getColumnName()}')
IF @constraint IS NOT NULL EXEC('ALTER TABLE ${
this.tableNameRaw
} DROP CONSTRAINT ' + @constraint)`;
this.pushQuery(baseQuery);
}
}
// in SQL server only one column can be altered at a time
columns.sql.forEach((sql) => {
this.pushQuery({
sql:
(this.lowerCase ? 'alter table ' : 'ALTER TABLE ') +
this.tableName() +
' ' +
(this.lowerCase
? this.alterColumnPrefix.toLowerCase()
: this.alterColumnPrefix) +
sql,
bindings: columns.bindings,
});
});
}
// Compiles column drop. Multiple columns need only one DROP clause (not one DROP per column) so core dropColumn doesn't work. #1348
dropColumn() {
const _this2 = this;
const columns = helpers.normalizeArr.apply(null, arguments);
const columnsArray = Array.isArray(columns) ? columns : [columns];
const drops = columnsArray.map((column) => _this2.formatter.wrap(column));
const schema = this.schemaNameRaw || 'dbo';
for (const column of columns) {
const baseQuery = `
DECLARE @constraint varchar(100) = (SELECT default_constraints.name
FROM sys.all_columns
INNER JOIN sys.tables
ON all_columns.object_id = tables.object_id
INNER JOIN sys.schemas
ON tables.schema_id = schemas.schema_id
INNER JOIN sys.default_constraints
ON all_columns.default_object_id = default_constraints.object_id
WHERE schemas.name = '${schema}'
AND tables.name = '${this.tableNameRaw}'
AND all_columns.name = '${column}')
IF @constraint IS NOT NULL EXEC('ALTER TABLE ${this.tableNameRaw} DROP CONSTRAINT ' + @constraint)`;
this.pushQuery(baseQuery);
}
this.pushQuery(
(this.lowerCase ? 'alter table ' : 'ALTER TABLE ') +
this.tableName() +
' ' +
this.dropColumnPrefix +
drops.join(', ')
);
}
changeType() {}
// Renames a column on the table.
renameColumn(from, to) {
this.pushQuery(
`exec sp_rename ${this.client.parameter(
this.tableName() + '.' + from,
this.tableBuilder,
this.bindingsHolder
)}, ${this.client.parameter(
to,
this.tableBuilder,
this.bindingsHolder
)}, 'COLUMN'`
);
}
dropFKRefs(runner, refs) {
const formatter = this.client.formatter(this.tableBuilder);
return Promise.all(
refs.map(function (ref) {
const constraintName = formatter.wrap(ref.CONSTRAINT_NAME);
const tableName = formatter.wrap(ref.TABLE_NAME);
return runner.query({
sql: `ALTER TABLE ${tableName} DROP CONSTRAINT ${constraintName}`,
});
})
);
}
createFKRefs(runner, refs) {
const formatter = this.client.formatter(this.tableBuilder);
return Promise.all(
refs.map(function (ref) {
const tableName = formatter.wrap(ref.TABLE_NAME);
const keyName = formatter.wrap(ref.CONSTRAINT_NAME);
const column = formatter.columnize(ref.COLUMN_NAME);
const references = formatter.columnize(ref.REFERENCED_COLUMN_NAME);
const inTable = formatter.wrap(ref.REFERENCED_TABLE_NAME);
const onUpdate = ` ON UPDATE ${ref.UPDATE_RULE}`;
const onDelete = ` ON DELETE ${ref.DELETE_RULE}`;
return runner.query({
sql:
`ALTER TABLE ${tableName} ADD CONSTRAINT ${keyName}` +
' FOREIGN KEY (' +
column +
') REFERENCES ' +
inTable +
' (' +
references +
')' +
onUpdate +
onDelete,
});
})
);
}
index(columns, indexName, options) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('index', this.tableNameRaw, columns);
let predicate;
if (isObject(options)) {
({ predicate } = options);
}
const predicateQuery = predicate
? ' ' + this.client.queryCompiler(predicate).where()
: '';
this.pushQuery(
`CREATE INDEX ${indexName} ON ${this.tableName()} (${this.formatter.columnize(
columns
)})${predicateQuery}`
);
}
/**
* Create a primary key.
*
* @param {undefined | string | string[]} columns
* @param {string | {constraintName: string, deferrable?: 'not deferrable'|'deferred'|'immediate' }} constraintName
*/
primary(columns, constraintName) {
let deferrable;
if (isObject(constraintName)) {
({ constraintName, deferrable } = constraintName);
}
if (deferrable && deferrable !== 'not deferrable') {
this.client.logger.warn(
`mssql: primary key constraint [${constraintName}] will not be deferrable ${deferrable} because mssql does not support deferred constraints.`
);
}
constraintName = constraintName
? this.formatter.wrap(constraintName)
: this.formatter.wrap(`${this.tableNameRaw}_pkey`);
if (!this.forCreate) {
this.pushQuery(
`ALTER TABLE ${this.tableName()} ADD CONSTRAINT ${constraintName} PRIMARY KEY (${this.formatter.columnize(
columns
)})`
);
} else {
this.pushQuery(
`CONSTRAINT ${constraintName} PRIMARY KEY (${this.formatter.columnize(
columns
)})`
);
}
}
/**
* Create a unique index.
*
* @param {string | string[]} columns
* @param {string | {indexName: undefined | string, deferrable?: 'not deferrable'|'deferred'|'immediate', useConstraint?: true|false, predicate?: QueryBuilder }} indexName
*/
unique(columns, indexName) {
/** @type {string | undefined} */
let deferrable;
let useConstraint = false;
let predicate;
if (isObject(indexName)) {
({ indexName, deferrable, useConstraint, predicate } = indexName);
}
if (deferrable && deferrable !== 'not deferrable') {
this.client.logger.warn(
`mssql: unique index [${indexName}] will not be deferrable ${deferrable} because mssql does not support deferred constraints.`
);
}
if (useConstraint && predicate) {
throw new Error('mssql cannot create constraint with predicate');
}
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('unique', this.tableNameRaw, columns);
if (!Array.isArray(columns)) {
columns = [columns];
}
if (useConstraint) {
// mssql supports unique indexes and unique constraints.
// unique indexes cannot be used with foreign key relationships hence unique constraints are used instead.
this.pushQuery(
`ALTER TABLE ${this.tableName()} ADD CONSTRAINT ${indexName} UNIQUE (${this.formatter.columnize(
columns
)})`
);
} else {
// default to making unique index that allows null https://stackoverflow.com/a/767702/360060
// to be more or less compatible with other DBs (if any of the columns is NULL then "duplicates" are allowed)
const predicateQuery = predicate
? ' ' + this.client.queryCompiler(predicate).where()
: ' WHERE ' +
columns
.map((column) => this.formatter.columnize(column) + ' IS NOT NULL')
.join(' AND ');
this.pushQuery(
`CREATE UNIQUE INDEX ${indexName} ON ${this.tableName()} (${this.formatter.columnize(
columns
)})${predicateQuery}`
);
}
}
// Compile a drop index command.
dropIndex(columns, indexName) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('index', this.tableNameRaw, columns);
this.pushQuery(`DROP INDEX ${indexName} ON ${this.tableName()}`);
}
// Compile a drop foreign key command.
dropForeign(columns, indexName) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('foreign', this.tableNameRaw, columns);
this.pushQuery(
`ALTER TABLE ${this.tableName()} DROP CONSTRAINT ${indexName}`
);
}
// Compile a drop primary key command.
dropPrimary(constraintName) {
constraintName = constraintName
? this.formatter.wrap(constraintName)
: this.formatter.wrap(`${this.tableNameRaw}_pkey`);
this.pushQuery(
`ALTER TABLE ${this.tableName()} DROP CONSTRAINT ${constraintName}`
);
}
// Compile a drop unique key command.
dropUnique(column, indexName) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('unique', this.tableNameRaw, column);
this.pushQuery(`DROP INDEX ${indexName} ON ${this.tableName()}`);
}
}
TableCompiler_MSSQL.prototype.createAlterTableMethods = ['foreign', 'primary'];
TableCompiler_MSSQL.prototype.lowerCase = false;
TableCompiler_MSSQL.prototype.addColumnsPrefix = 'ADD ';
TableCompiler_MSSQL.prototype.dropColumnPrefix = 'DROP COLUMN ';
TableCompiler_MSSQL.prototype.alterColumnPrefix = 'ALTER COLUMN ';
module.exports = TableCompiler_MSSQL;

View File

@ -0,0 +1,55 @@
/* eslint max-len: 0 */
const ViewCompiler = require('../../../schema/viewcompiler.js');
const {
columnize: columnize_,
} = require('../../../formatter/wrappingFormatter');
class ViewCompiler_MSSQL extends ViewCompiler {
constructor(client, viewCompiler) {
super(client, viewCompiler);
}
createQuery(columns, selectQuery, materialized, replace) {
const createStatement = 'CREATE ' + (replace ? 'OR ALTER ' : '') + 'VIEW ';
let sql = createStatement + this.viewName();
const columnList = columns
? ' (' +
columnize_(
columns,
this.viewBuilder,
this.client,
this.bindingsHolder
) +
')'
: '';
sql += columnList;
sql += ' AS ';
sql += selectQuery.toString();
this.pushQuery({
sql,
});
}
renameColumn(from, to) {
this.pushQuery(
`exec sp_rename ${this.client.parameter(
this.viewName() + '.' + from,
this.viewBuilder,
this.bindingsHolder
)}, ${this.client.parameter(
to,
this.viewBuilder,
this.bindingsHolder
)}, 'COLUMN'`
);
}
createOrReplace() {
this.createQuery(this.columns, this.selectQuery, false, true);
}
}
module.exports = ViewCompiler_MSSQL;

View File

@ -0,0 +1,176 @@
const Transaction = require('../../execution/transaction');
const debug = require('debug')('knex:tx');
class Transaction_MSSQL extends Transaction {
begin(/** @type {import('tedious').Connection} */ conn) {
debug('transaction::begin id=%s', this.txid);
return new Promise((resolve, reject) => {
conn.beginTransaction(
(err) => {
if (err) {
debug(
'transaction::begin error id=%s message=%s',
this.txid,
err.message
);
return reject(err);
}
resolve();
},
this.outerTx ? this.txid : undefined,
nameToIsolationLevelEnum(this.isolationLevel)
);
}).then(this._resolver, this._rejecter);
}
savepoint(conn) {
debug('transaction::savepoint id=%s', this.txid);
return new Promise((resolve, reject) => {
conn.saveTransaction(
(err) => {
if (err) {
debug(
'transaction::savepoint id=%s message=%s',
this.txid,
err.message
);
return reject(err);
}
this.trxClient.emit('query', {
__knexUid: this.trxClient.__knexUid,
__knexTxId: this.trxClient.__knexTxId,
autogenerated: true,
sql: this.outerTx
? `SAVE TRANSACTION [${this.txid}]`
: `SAVE TRANSACTION`,
});
resolve();
},
this.outerTx ? this.txid : undefined
);
});
}
commit(conn, value) {
debug('transaction::commit id=%s', this.txid);
return new Promise((resolve, reject) => {
conn.commitTransaction(
(err) => {
if (err) {
debug(
'transaction::commit error id=%s message=%s',
this.txid,
err.message
);
return reject(err);
}
this._completed = true;
resolve(value);
},
this.outerTx ? this.txid : undefined
);
}).then(() => this._resolver(value), this._rejecter);
}
release(conn, value) {
return this._resolver(value);
}
rollback(conn, error) {
this._completed = true;
debug('transaction::rollback id=%s', this.txid);
return new Promise((_resolve, reject) => {
if (!conn.inTransaction) {
return reject(
error || new Error('Transaction rejected with non-error: undefined')
);
}
if (conn.state.name !== 'LoggedIn') {
return reject(
new Error(
"Can't rollback transaction. There is a request in progress"
)
);
}
conn.rollbackTransaction(
(err) => {
if (err) {
debug(
'transaction::rollback error id=%s message=%s',
this.txid,
err.message
);
}
reject(
err ||
error ||
new Error('Transaction rejected with non-error: undefined')
);
},
this.outerTx ? this.txid : undefined
);
}).catch((err) => {
if (!error && this.doNotRejectOnRollback) {
this._resolver();
return;
}
if (error) {
try {
err.originalError = error;
} catch (_err) {
// This is to handle https://github.com/knex/knex/issues/4128
}
}
this._rejecter(err);
});
}
rollbackTo(conn, error) {
return this.rollback(conn, error).then(
() =>
void this.trxClient.emit('query', {
__knexUid: this.trxClient.__knexUid,
__knexTxId: this.trxClient.__knexTxId,
autogenerated: true,
sql: `ROLLBACK TRANSACTION`,
})
);
}
}
module.exports = Transaction_MSSQL;
function nameToIsolationLevelEnum(level) {
if (!level) return;
level = level.toUpperCase().replace(' ', '_');
const knownEnum = isolationEnum[level];
if (!knownEnum) {
throw new Error(
`Unknown Isolation level, was expecting one of: ${JSON.stringify(
humanReadableKeys
)}`
);
}
return knownEnum;
}
// Based on: https://github.com/tediousjs/node-mssql/blob/master/lib/isolationlevel.js
const isolationEnum = {
READ_UNCOMMITTED: 0x01,
READ_COMMITTED: 0x02,
REPEATABLE_READ: 0x03,
SERIALIZABLE: 0x04,
SNAPSHOT: 0x05,
};
const humanReadableKeys = Object.keys(isolationEnum).map((key) =>
key.toLowerCase().replace('_', ' ')
);