UNPKG

knex

Version:

A batteries-included SQL query & schema builder for PostgresSQL, MySQL, CockroachDB, MSSQL and SQLite3

293 lines (272 loc) • 7.29 kB
const transform = require('lodash/transform'); const QueryBuilder = require('../query/querybuilder'); const { compileCallback, wrapAsIdentifier } = require('./formatterUtils'); // Valid values for the `order by` clause generation. const orderBys = ['asc', 'desc']; // Turn this into a lookup map const operators = transform( [ '=', '<', '>', '<=', '<=>', '>=', '<>', '!=', 'like', 'not like', 'between', 'not between', 'ilike', 'not ilike', 'exists', 'not exist', 'rlike', 'not rlike', 'regexp', 'not regexp', 'match', 'similar to', 'not similar to', '&', '|', '^', '<<', '>>', '~', '~=', '~*', '!~', '!~*', '#', '&&', '@>', '<@', '||', '&<', '&>', '-|-', '@@', '!!', ['?', '\\?'], ['?|', '\\?|'], ['?&', '\\?&'], ], (result, key) => { if (Array.isArray(key)) { result[key[0]] = key[1]; } else { result[key] = key; } }, {} ); // Accepts a string or array of columns to wrap as appropriate. Column can be raw function columnize(target, builder, client, bindingHolder) { if (!Array.isArray(target)) { return wrap(target, undefined, builder, client, bindingHolder); } const columns = target; let str = '', i = -1; while (++i < columns.length) { if (i > 0) str += ', '; str += wrap(columns[i], undefined, builder, client, bindingHolder); } return str; } // Puts the appropriate wrapper around a value depending on the database // engine, unless it's a knex.raw value, in which case it's left alone. function wrap(value, isParameter, builder, client, bindingHolder) { const raw = unwrapRaw(value, isParameter, builder, client, bindingHolder); if (raw) return raw; switch (typeof value) { case 'function': return outputQuery( compileCallback(value, undefined, client, bindingHolder), true, builder, client ); case 'object': return parseObject(value, builder, client, bindingHolder); case 'number': return value; default: return wrapString(value + '', builder, client); } } function unwrapRaw(value, isParameter, builder, client, bindingsHolder) { let query; if (value instanceof QueryBuilder) { query = client.queryCompiler(value).toSQL(); if (query.bindings) { bindingsHolder.bindings.push(...query.bindings); } return outputQuery(query, isParameter, builder, client); } if (value && value.isRawInstance) { value.client = client; if (builder._queryContext) { value.queryContext = () => { return builder._queryContext; }; } query = value.toSQL(); if (query.bindings) { bindingsHolder.bindings.push(...query.bindings); } return query.sql; } if (isParameter) { bindingsHolder.bindings.push(value); } } function operator(value, builder, client, bindingsHolder) { const raw = unwrapRaw(value, undefined, builder, client, bindingsHolder); if (raw) return raw; const operator = operators[(value || '').toLowerCase()]; if (!operator) { throw new TypeError(`The operator "${value}" is not permitted`); } return operator; } // Find a case-insensitive " as " without allocating a lowercased string. // ASCII uppercase letters differ from lowercase by bit 0x20, so `| 32` // folds A/S to a/s before comparing to 97 ("a") and 115 ("s"). function getAliasSeparatorIndex(value) { let i = -1; while ((i = value.indexOf(' ', i + 1)) !== -1) { if ( i + 3 < value.length && (value.charCodeAt(i + 1) | 32) === 97 && (value.charCodeAt(i + 2) | 32) === 115 && value.charCodeAt(i + 3) === 32 ) { return i; } } return -1; } // Coerce to string to prevent strange errors when it's not a string. function wrapString(value, builder, client) { const asIndex = getAliasSeparatorIndex(value); if (asIndex !== -1) { const first = value.slice(0, asIndex); const second = value.slice(asIndex + 4); return client.alias( wrapString(first, builder, client), wrapAsIdentifier(second, builder, client) ); } let segmentStart = 0; let dotIndex = value.indexOf('.'); if (dotIndex === -1) { return wrapAsIdentifier(value, builder, client); } let wrapped = ''; let i = 0; while (dotIndex !== -1) { const segment = value.slice(segmentStart, dotIndex); if (i > 0) wrapped += '.'; wrapped += i === 0 ? wrapString((segment || '').trim(), builder, client) : wrapAsIdentifier(segment, builder, client); segmentStart = dotIndex + 1; dotIndex = value.indexOf('.', segmentStart); i++; } return `${wrapped}.${wrapAsIdentifier( value.slice(segmentStart), builder, client )}`; } // Key-value notation for alias function parseObject(obj, builder, client, formatter) { const ret = []; for (const alias in obj) { const queryOrIdentifier = obj[alias]; // Avoids double aliasing for subqueries if (typeof queryOrIdentifier === 'function') { const compiled = compileCallback( queryOrIdentifier, undefined, client, formatter ); compiled.as = alias; // enforces the object's alias ret.push(outputQuery(compiled, true, builder, client)); } else if (queryOrIdentifier instanceof QueryBuilder) { ret.push( client.alias( `(${wrap(queryOrIdentifier, undefined, builder, client, formatter)})`, wrapAsIdentifier(alias, builder, client) ) ); } else { ret.push( client.alias( wrap(queryOrIdentifier, undefined, builder, client, formatter), wrapAsIdentifier(alias, builder, client) ) ); } } return ret.join(', '); } // Ensures the query is aliased if necessary. function outputQuery(compiled, isParameter, builder, client) { let sql = compiled.sql || ''; if (sql) { if ( (compiled.method === 'select' || compiled.method === 'first') && (isParameter || compiled.as) ) { sql = `(${sql})`; if (compiled.as) return client.alias(sql, wrapString(compiled.as, builder, client)); } } return sql; } /** * Creates SQL for a parameter, which might be passed to where() or .with() or * pretty much anywhere in API. * * @param value * @param method Optional at least 'select' or 'update' are valid * @param builder * @param client * @param bindingHolder */ function rawOrFn(value, method, builder, client, bindingHolder) { if (typeof value === 'function') { return outputQuery( compileCallback(value, method, client, bindingHolder), undefined, builder, client ); } return unwrapRaw(value, undefined, builder, client, bindingHolder) || ''; } // Specify the direction of the ordering. function direction(value, builder, client, bindingsHolder) { const raw = unwrapRaw(value, undefined, builder, client, bindingsHolder); if (raw) return raw; return orderBys.indexOf((value || '').toLowerCase()) !== -1 ? value : 'asc'; } module.exports = { columnize, direction, operator, outputQuery, getAliasSeparatorIndex, rawOrFn, unwrapRaw, wrap, wrapString, };