Browse Source

Add updateBatch method to query builder

keep-around/4064a99419461bccaf062953d2fcfb38503fbc65
Timothy Warren 1 year ago
parent
commit
4064a99419
29 changed files with 471 additions and 373 deletions
  1. +36
    -32
      .eslintrc
  2. +81
    -15
      lib/Driver.js
  3. +7
    -8
      lib/Helpers.js
  4. +5
    -6
      lib/NodeQuery.js
  5. +24
    -10
      lib/QueryBuilder.js
  6. +76
    -76
      lib/QueryBuilderBase.js
  7. +18
    -18
      lib/QueryParser.js
  8. +0
    -2
      lib/adapters/MSSQLServer/index.js
  9. +1
    -1
      lib/adapters/Mysql/mysql2.js
  10. +32
    -19
      lib/adapters/Pg/Pg.js
  11. +0
    -25
      lib/adapters/Pg/PgNative.js
  12. +1
    -7
      lib/adapters/Pg/index.js
  13. +2
    -2
      lib/adapters/Sqlite/dblite.js
  14. +2
    -2
      lib/adapters/Sqlite/sqlite3.js
  15. +0
    -3
      lib/drivers/MSSQLDriver.js
  16. +1
    -3
      lib/drivers/Pg.js
  17. +7
    -7
      lib/drivers/Sqlite.js
  18. +12
    -18
      package.json
  19. +11
    -0
      test/adapters/__snapshots__/dblite_test.js.snap
  20. +11
    -0
      test/adapters/__snapshots__/mysql2_test.js.snap
  21. +11
    -0
      test/adapters/__snapshots__/pg_test.js.snap
  22. +11
    -0
      test/adapters/__snapshots__/sqlite3_test.js.snap
  23. +1
    -21
      test/adapters/dblite_test.js
  24. +3
    -24
      test/adapters/mysql2_test.js
  25. +4
    -24
      test/adapters/pg_test.js
  26. +1
    -21
      test/adapters/sqlite3_test.js
  27. +0
    -5
      test/base.js
  28. +101
    -15
      test/base/adapterPromiseTestRunner.js
  29. +12
    -9
      test/config-ci.json

+ 36
- 32
.eslintrc View File

@@ -1,39 +1,43 @@
{
"env": {
"node": true,
"es6": true
"commonjs": true,
"es6": true,
"jest": true,
"node": true
},
"extends": ["eslint:recommended", "happiness"],
"rules": {
"arrow-parens": [2, "as-needed"],
"no-console": [1],
"no-constant-condition": [1],
"no-extra-semi": [1],
"no-func-assign": [1],
"no-obj-calls": [2],
"no-unexpected-multiline" : [2],
"no-unneeded-ternary": [2],
"radix": [2],
"no-with": [2],
"no-eval": [2],
"no-unreachable": [1],
"no-irregular-whitespace": [1],
"no-new-wrappers": [2],
"no-new-func": [2],
"curly" : [2, "multi-line"],
"no-implied-eval": [2],
"no-invalid-this": [2],
"constructor-super": [2],
"no-dupe-args": [2],
"no-dupe-keys": [2],
"no-dupe-class-members": [2],
"no-this-before-super": [2],
"prefer-arrow-callback": [1],
"no-var": [2],
"valid-jsdoc": [1],
"strict": [2, "global"],
"callback-return": [1],
"object-shorthand": [1, "methods"],
"prefer-template": [1]
"arrow-parens": ["error", "as-needed"],
"callback-return": ["warn"],
"constructor-super": ["error"],
"curly" : ["error", "multi-line"],
"no-case-declarations": "off",
"no-console": ["warn"],
"no-constant-condition": ["warn"],
"no-dupe-args": ["error"],
"no-dupe-class-members": ["error"],
"no-dupe-keys": ["error"],
"no-eval": ["error"],
"no-extra-semi": ["warn"],
"no-func-assign": ["warn"],
"no-implied-eval": ["error"],
"no-invalid-this": ["error"],
"no-irregular-whitespace": ["warn"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],
"no-obj-calls": ["error"],
"no-this-before-super": ["error"],
"no-unexpected-multiline" : ["error"],
"no-unneeded-ternary": ["error"],
"no-unreachable": ["warn"],
"no-var": ["error"],
"no-with": ["error"],
"object-shorthand": ["warn", "methods"],
"prefer-arrow-callback": ["warn"],
"prefer-template": ["warn"],
"radix": ["error"],
"strict": ["error", "global"],
"valid-jsdoc": ["warn"]
},
"parser": "babel-eslint"
}

+ 81
- 15
lib/Driver.js View File

@@ -1,3 +1,4 @@
const array = require('locutus/php/array');
const Helpers = require('./Helpers');

/**
@@ -64,36 +65,38 @@ const Driver = {
* @return {String|Array} - Quoted identifier(s)
*/
quoteIdentifiers (str) {
let hiers, raw;
let pattern = new RegExp(
const pattern = new RegExp(
`${Driver.identifierStartChar}(` +
'([a-zA-Z0-9_]+)' + '(((.*?)))' +
`)${Driver.identifierEndChar}`, 'ig');

// Recurse for arrays of identifiiers
// Recurse for arrays of identifiers
if (Array.isArray(str)) {
return str.map(Driver.quoteIdentifiers);
}

// cast to string so that you don't have undefined method errors with junk data
str = String(str);

// Handle commas
if (str.includes(',')) {
let parts = str.split(',').map(Helpers.stringTrim);
const parts = str.split(',').map(Helpers.stringTrim);
str = parts.map(Driver.quoteIdentifiers).join(',');
}

// Split identifiers by period
hiers = str.split('.').map(Driver._quote);
raw = hiers.join('.');
const hierarchies = str.split('.').map(Driver._quote);
let raw = hierarchies.join('.');

// Fix functions
if (raw.includes('(') && raw.includes(')')) {
let funcs = pattern.exec(raw);
const functionCalls = pattern.exec(raw);

// Unquote the function
raw = raw.replace(funcs[0], funcs[1]);
raw = raw.replace(functionCalls[0], functionCalls[1]);

// Quote the identifiers inside of the parens
let inParens = funcs[3].substring(1, funcs[3].length - 1);
const inParens = functionCalls[3].substring(1, functionCalls[3].length - 1);
raw = raw.replace(inParens, Driver.quoteIdentifiers(inParens));
}

@@ -126,7 +129,7 @@ const Driver = {
* @return {String} - Query and data to insert
*/
insertBatch (table, data) {
const vals = [];
const values = [];
const fields = Object.keys(data[0]);
let sql = '';

@@ -134,7 +137,7 @@ const Driver = {
// be parameterized
data.forEach(obj => {
Object.keys(obj).forEach(key => {
vals.push(obj[key]);
values.push(obj[key]);
});
});

@@ -145,16 +148,79 @@ const Driver = {
sql += `INSERT INTO ${table} (${Driver.quoteIdentifiers(fields).join(',')}) VALUES `;

// Create placeholder groups
let params = Array(fields.length).fill('?');
let paramString = `(${params.join(',')})`;
let paramList = Array(data.length).fill(paramString);
const params = Array(fields.length).fill('?');
const paramString = `(${params.join(',')})`;
const paramList = Array(data.length).fill(paramString);

sql += paramList.join(',');

return {
sql: sql,
values: vals
values: values
};
},

/**
* Creates a batch update sql statement
*
* @private
* @param {String} table
* @param {Array<Object>} data
* @param {String} updateKey
* @return {Array<String,Object,Number>} array
*/
updateBatch (table, data, updateKey) {
let affectedRows = 0;
let insertData = [];
const fieldLines = [];

let sql = `UPDATE ${Driver.quoteTable(table)} SET `;

// get the keys of the current set of data, except the one used to
// set the update condition
const fields = data.reduce((previous, current) => {
affectedRows++;
const keys = Object.keys(current).filter(key => {
return key !== updateKey && !previous.includes(key);
});
return previous.concat(keys);
}, []);

// Create the CASE blocks for each data set
fields.forEach(field => {
let line = `${Driver.quoteIdentifiers(field)} = CASE\n`;
const cases = [];
data.forEach(currentCase => {
insertData.push(currentCase[updateKey]);
insertData.push(currentCase[field]);

const newCase = `WHEN ${Driver.quoteIdentifiers(updateKey)} =? THEN ? `;
cases.push(newCase);
});

line += `${cases.join('\n')}\n`;
line += `ELSE ${Driver.quoteIdentifiers(field)} END`;

fieldLines.push(line);
});

sql += `${fieldLines.join(',\n')}\n`;

const whereValues = [];
data.forEach(entry => {
const insertValue = entry[updateKey];
whereValues.push(insertValue);
insertData.push(insertValue);
});

// Create the placeholders for the WHERE IN clause
const placeholders = Array(whereValues.length);
placeholders.fill('?');

sql += `WHERE ${Driver.quoteIdentifiers(updateKey)} IN `;
sql += `( ${placeholders.join(',')} )`;

return [sql, insertData, affectedRows];
}
};


+ 7
- 8
lib/Helpers.js View File

@@ -42,7 +42,7 @@ class Helpers {
* @return {String} - Type of the object
*/
static type (o) {
let type = Object.prototype.toString.call(o).slice(8, -1).toLowerCase();
const type = Object.prototype.toString.call(o).slice(8, -1).toLowerCase();

// handle NaN and Infinity
if (type === 'number') {
@@ -62,11 +62,10 @@ class Helpers {
* Determine whether an object is scalar
*
* @param {mixed} obj - Object to test
* @return {bool} - Is object scalar
* @return {boolean} - Is object scalar
*/
static isScalar (obj) {
let scalar = ['string', 'number', 'boolean'];
return scalar.indexOf(Helpers.type(obj)) !== -1;
return ['string', 'number', 'boolean'].includes(Helpers.type(obj));
}

/**
@@ -77,7 +76,7 @@ class Helpers {
* @return {Array} - The new array of plucked values
*/
static arrayPluck (arr, key) {
let output = [];
const output = [];

// Empty case
if (arr.length === 0) {
@@ -119,20 +118,20 @@ class Helpers {
}

/**
* Make the first letter of the string uppercase
* Make the first constter of the string uppercase
*
* @param {String} str - The string to modify
* @return {String} - The modified string
*/
static upperCaseFirst (str) {
str += '';
let first = str.charAt(0).toUpperCase();
const first = str.charAt(0).toUpperCase();
return first + str.substr(1);
}
}

// Define an 'is' method for each type
let types = [
const types = [
'Null',
'Undefined',
'Object',

+ 5
- 6
lib/NodeQuery.js View File

@@ -50,17 +50,16 @@ class NodeQuery {
this.instance = null;

if (config !== undefined) {
let drivername = dbDriverMap.get(config.driver);
const driverName = dbDriverMap.get(config.driver);

if (!drivername) {
if (!driverName) {
throw new Error(`Selected driver (${config.driver}) does not exist!`);
}

const driver = require(`./drivers/${drivername}`);
const Adapter = require(`./adapters/${drivername}`);
const driver = require(`./drivers/${driverName}`);
const Adapter = require(`./adapters/${driverName}`);

let adapter = Adapter(config);
this.instance = new QueryBuilder(driver, adapter);
this.instance = new QueryBuilder(driver, Adapter(config));
}
}


+ 24
- 10
lib/QueryBuilder.js View File

@@ -107,7 +107,7 @@ class QueryBuilder extends QueryBuilderBase {
}
});

let safeArray = this.driver.quoteIdentifiers(fields);
const safeArray = this.driver.quoteIdentifiers(fields);

// Join the strings back together
safeArray.forEach((field, index) => {
@@ -378,8 +378,8 @@ class QueryBuilder extends QueryBuilderBase {
table = table.join(' ');

// Parse out the join condition
let parsedCondition = this.parser.compileJoin(cond);
let condition = `${table} ON ${parsedCondition}`;
const parsedCondition = this.parser.compileJoin(cond);
const condition = `${table} ON ${parsedCondition}`;

// Append the join condition to the query map
this._appendMap(`\n${type.toUpperCase()} JOIN `, condition, 'join');
@@ -395,7 +395,7 @@ class QueryBuilder extends QueryBuilderBase {
*/
groupBy (field) {
if (!Helpers.isScalar(field)) {
let newGroupArray = field.map(this.driver.quoteIdentifiers);
const newGroupArray = field.map(this.driver.quoteIdentifiers);
this.state.groupArray = this.state.groupArray.concat(newGroupArray);
} else {
this.state.groupArray.push(this.driver.quoteIdentifiers(field));
@@ -421,7 +421,7 @@ class QueryBuilder extends QueryBuilderBase {

this.state.orderArray[field] = type;

let orderClauses = [];
const orderClauses = [];

// Flatten key/val pairs into an array of space-separated pairs
Object.keys(this.state.orderArray).forEach(key => {
@@ -454,7 +454,7 @@ class QueryBuilder extends QueryBuilderBase {
* @return {QueryBuilder} - The Query Builder object, for chaining
*/
groupStart () {
let conj = (this.state.queryMap.length < 1) ? ' WHERE ' : ' AND ';
const conj = (this.state.queryMap.length < 1) ? ' WHERE ' : ' AND ';
this._appendMap(conj, '(', 'groupStart');

return this;
@@ -549,7 +549,7 @@ class QueryBuilder extends QueryBuilderBase {
* @return {Promise<Result>} - Promise containing the result of the query
*/
insertBatch (table, data) {
let batch = this.driver.insertBatch(table, data);
const batch = this.driver.insertBatch(table, data);

// Run the query
return this.query(batch.sql, batch.values);
@@ -571,6 +571,20 @@ class QueryBuilder extends QueryBuilderBase {
return this._run('update', this.driver.quoteTable(table));
}

/**
* Creates a batch update sql statement
*
* @param {String} table - The table to update
* @param {Object} data - Batch insert data
* @param {String} updateKey - The field in the table to compare against for updating
* @return {Number} Number of rows updated
*/
updateBatch (table, data, updateKey) {
const [sql, insertData, affectedRows] = this.driver.updateBatch(table, data, updateKey);
this._run('', table, sql, insertData);
return affectedRows;
}

/**
* Run the generated delete query
*
@@ -613,7 +627,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledInsert (table, reset) {
getCompiledInsert (table, reset = true) {
return this._getCompile('insert', this.driver.quoteTable(table), reset);
}

@@ -624,7 +638,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledUpdate (table, reset) {
getCompiledUpdate (table, reset = true) {
return this._getCompile('update', this.driver.quoteTable(table), reset);
}

@@ -635,7 +649,7 @@ class QueryBuilder extends QueryBuilderBase {
* @param {Boolean} [reset=true] - Whether to reset the query builder so another query can be built
* @return {String} - The compiled sql statement
*/
getCompiledDelete (table, reset) {
getCompiledDelete (table, reset = true) {
return this._getCompile('delete', this.driver.quoteTable(table), reset);
}
}

+ 76
- 76
lib/QueryBuilderBase.js View File

@@ -16,74 +16,6 @@ class QueryBuilderBase {
this.state = new State();
}

/**
* Complete the sql building based on the type provided
*
* @private
* @param {String} type - Type of SQL query
* @param {String} table - The table to run the query on
* @return {String} - The compiled sql
*/
_compile (type, table) {
// Put together the basic query
let sql = this._compileType(type, table);

// Set each subClause
['queryMap', 'groupString', 'orderString', 'havingMap'].forEach(clause => {
let param = this.state[clause];

if (!Helpers.isScalar(param)) {
Object.keys(param).forEach(part => {
sql += param[part].conjunction + param[part].string;
});
} else {
sql += param;
}
});

// Append the limit, if it exists
if (Helpers.isNumber(this.state.limit)) {
sql = this.driver.limit(sql, this.state.limit, this.state.offset);
}

return sql;
}

_compileType (type, table) {
let sql = '';

switch (type) {
case 'insert':
let params = Array(this.state.setArrayKeys.length).fill('?');

sql = `INSERT INTO ${table} (`;
sql += this.state.setArrayKeys.join(',');
sql += `) VALUES (${params.join(',')})`;
break;

case 'update':
sql = `UPDATE ${table} SET ${this.state.setString}`;
break;

case 'delete':
sql = `DELETE FROM ${table}`;
break;

default:
sql = `SELECT * FROM ${this.state.fromString}`;

// Set the select string
if (this.state.selectString.length > 0) {
// Replace the star with the selected fields
sql = sql.replace('*', this.state.selectString);
}

break;
}

return sql;
}

_like (field, val, pos, like, conj) {
field = this.driver.quoteIdentifiers(field);

@@ -147,7 +79,7 @@ class QueryBuilderBase {
Object.keys(obj).forEach(k => {
// If a single value for the return
if (['key', 'value'].indexOf(valType) !== -1) {
let pushVal = (valType === 'key') ? k : obj[k];
const pushVal = (valType === 'key') ? k : obj[k];
this.state[letName].push(pushVal);
} else {
this.state[letName][k] = obj[k];
@@ -166,8 +98,8 @@ class QueryBuilderBase {
}

_fixConjunction (conj) {
let lastItem = this.state.queryMap[this.state.queryMap.length - 1];
let conjunctionList = Helpers.arrayPluck(this.state.queryMap, 'conjunction');
const lastItem = this.state.queryMap[this.state.queryMap.length - 1];
const conjunctionList = Helpers.arrayPluck(this.state.queryMap, 'conjunction');

if (this.state.queryMap.length === 0 || (!Helpers.regexInArray(conjunctionList, /^ ?WHERE/i))) {
conj = ' WHERE ';
@@ -189,7 +121,7 @@ class QueryBuilderBase {
this.state = this.parser.parseWhere(this.driver, this.state);

this.state.whereMap.forEach(clause => {
let conj = this._fixConjunction(defaultConj);
const conj = this._fixConjunction(defaultConj);
this._appendMap(conj, clause, 'where');
});

@@ -198,7 +130,7 @@ class QueryBuilderBase {

_whereNull (field, stmt, conj) {
field = this.driver.quoteIdentifiers(field);
let item = `${field} ${stmt}`;
const item = `${field} ${stmt}`;

this._appendMap(this._fixConjunction(conj), item, 'whereNull');
}
@@ -225,7 +157,7 @@ class QueryBuilderBase {

_whereIn (key, val, inClause, conj) {
key = this.driver.quoteIdentifiers(key);
let params = Array(val.length);
const params = Array(val.length);
params.fill('?');

val.forEach(value => {
@@ -233,7 +165,7 @@ class QueryBuilderBase {
});

conj = (this.state.queryMap.length > 0) ? ` ${conj} ` : ' WHERE ';
let str = `${key} ${inClause} (${params.join(',')}) `;
const str = `${key} ${inClause} (${params.join(',')}) `;

this._appendMap(conj, str, 'whereIn');
}
@@ -257,7 +189,7 @@ class QueryBuilderBase {
_getCompile (type, table, reset) {
reset = reset || false;

let sql = this._compile(type, table);
const sql = this._compile(type, table);

if (reset) {
this._resetState();
@@ -266,6 +198,74 @@ class QueryBuilderBase {
return sql;
}

/**
* Complete the sql building based on the type provided
*
* @private
* @param {String} type - Type of SQL query
* @param {String} table - The table to run the query on
* @return {String} - The compiled sql
*/
_compile (type, table) {
// Put together the basic query
let sql = this._compileType(type, table);

// Set each subClause
['queryMap', 'groupString', 'orderString', 'havingMap'].forEach(clause => {
const param = this.state[clause];

if (!Helpers.isScalar(param)) {
Object.keys(param).forEach(part => {
sql += param[part].conjunction + param[part].string;
});
} else {
sql += param;
}
});

// Append the limit, if it exists
if (Helpers.isNumber(this.state.limit)) {
sql = this.driver.limit(sql, this.state.limit, this.state.offset);
}

return sql;
}

_compileType (type, table) {
let sql = '';

switch (type) {
case 'insert':
const params = Array(this.state.setArrayKeys.length).fill('?');

sql = `INSERT INTO ${table} (`;
sql += this.state.setArrayKeys.join(',');
sql += `) VALUES (${params.join(',')})`;
break;

case 'update':
sql = `UPDATE ${table} SET ${this.state.setString}`;
break;

case 'delete':
sql = `DELETE FROM ${table}`;
break;

default:
sql = `SELECT * FROM ${this.state.fromString}`;

// Set the select string
if (this.state.selectString.length > 0) {
// Replace the star with the selected fields
sql = sql.replace('*', this.state.selectString);
}

break;
}

return sql;
}

_resetState () {
this.state = new State();
}

+ 18
- 18
lib/QueryParser.js View File

@@ -54,7 +54,7 @@ class QueryParser {
* @return {Array|null} - Filtered set of possible matches
*/
filterMatches (array) {
let output = [];
const output = [];

// Return non-array matches
if (Helpers.isNull(array)) {
@@ -85,8 +85,8 @@ class QueryParser {
* @return {Object} - Join condition components
*/
parseJoin (sql) {
let matches = {};
let output = {
const matches = {};
const output = {
functions: [],
identifiers: [],
operators: [],
@@ -117,7 +117,7 @@ class QueryParser {
* @return {String} - The parsed/escaped join condition
*/
compileJoin (condition) {
let parts = this.parseJoin(condition);
const parts = this.parseJoin(condition);

// Quote the identifiers
parts.combined.forEach((part, i) => {
@@ -137,11 +137,11 @@ class QueryParser {
* @return {String} - The parsed/escaped where condition
*/
parseWhere (driver, state) {
let whereMap = state.whereMap;
const whereMap = state.whereMap;
let whereValues = state.rawWhereValues;

let outputMap = [];
let outputValues = [];
const outputMap = [];
const outputValues = [];

Object.keys(whereMap).forEach(key => {
// Combine fields, operators, functions and values into a full clause
@@ -158,16 +158,16 @@ class QueryParser {
}

// Separate the clause into separate pieces
let parts = this.parseJoin(fullClause);
const parts = this.parseJoin(fullClause);

// Filter explicit literals from lists of matches
if (whereValues.indexOf(whereMap[key]) !== -1) {
let value = whereMap[key];
let identIndex = parts.identifiers.indexOf(value);
let litIndex = (Helpers.isArray(parts.literals)) ? parts.literals.indexOf(value) : -1;
let combIndex = parts.combined.indexOf(value);
let funcIndex = (Helpers.isArray(parts.functions)) ? parts.functions.indexOf(value) : -1;
let inOutputArray = outputValues.indexOf(value) !== -1;
const value = whereMap[key];
const identIndex = parts.identifiers.indexOf(value);
const litIndex = (Helpers.isArray(parts.literals)) ? parts.literals.indexOf(value) : -1;
const combIndex = parts.combined.indexOf(value);
const funcIndex = (Helpers.isArray(parts.functions)) ? parts.functions.indexOf(value) : -1;
let inOutputArray = outputValues.includes(value);

// Remove the identifier in question,
// and add to the output values array
@@ -201,8 +201,8 @@ class QueryParser {

// Filter false positive identifiers
parts.identifiers = parts.identifiers.filter(item => {
let isInCombinedMatches = parts.combined.indexOf(item) !== -1;
let isNotInBlackList = this.identifierBlacklist.indexOf(item.toLowerCase()) === -1;
const isInCombinedMatches = parts.combined.indexOf(item) !== -1;
const isNotInBlackList = this.identifierBlacklist.indexOf(item.toLowerCase()) === -1;

return isInCombinedMatches && isNotInBlackList;
}, this);
@@ -210,7 +210,7 @@ class QueryParser {
// Quote identifiers
if (Helpers.isArray(parts.identifiers)) {
parts.identifiers.forEach(ident => {
let index = parts.combined.indexOf(ident);
const index = parts.combined.indexOf(ident);
if (index !== -1) {
parts.combined[index] = driver.quoteIdentifiers(ident);
}
@@ -224,7 +224,7 @@ class QueryParser {
// a where condition,
if (Helpers.isArray(parts.literals)) {
parts.literals.forEach(lit => {
let litIndex = parts.combined.indexOf(lit);
const litIndex = parts.combined.indexOf(lit);

if (litIndex !== -1) {
parts.combined[litIndex] = '?';

+ 0
- 2
lib/adapters/MSSQLServer/index.js View File

@@ -1,5 +1,3 @@
'use strict';

module.exports = config => {

};

+ 1
- 1
lib/adapters/Mysql/mysql2.js View File

@@ -36,7 +36,7 @@ class Mysql extends Adapter {
* Run the sql query as a prepared statement
*
* @param {String} sql - The sql with placeholders
* @param {Array|undefined} params - The values to insert into the query
* @param {Array} params - The values to insert into the query
* @return {Promise} Result of query
*/
execute (sql, params) {

+ 32
- 19
lib/adapters/Pg/Pg.js View File

@@ -7,14 +7,38 @@ const url = require('url');
class Pg extends Adapter {
constructor (config) {
let instance = null;
let connectionString = Pg.formatConnectionString(config);

if (connectionString !== '') {
const conn = new pg.Client(connectionString);
conn.connect(err => {
if (err) {
throw new Error(err);
}
});

instance = Promise.resolve(conn);
}

super(instance);
}

/**
* Convert the connection object to a connection string
*
* @param {Object} config - the configuration object
* @return {String} - the connection string
*/
static formatConnectionString (config) {
let connectionString = '';

if (Helpers.isObject(config)) {
let host = config.host || 'localhost';
let user = config.user || 'postgres';
let password = `:${config.password}` || '';
let port = config.port || 5432;
const host = config.host || 'localhost';
const user = config.user || 'postgres';
const password = `:${config.password}` || '';
const port = config.port || 5432;

let conn = {
const conn = {
protocol: 'postgres',
slashes: true,
host: `${host}:${port}`,
@@ -27,18 +51,7 @@ class Pg extends Adapter {
connectionString = config;
}

if (connectionString !== '') {
let conn = new pg.Client(connectionString);
conn.connect(err => {
if (err) {
throw new Error(err);
}
});

instance = Promise.resolve(conn);
}

super(instance);
return connectionString;
}

/**
@@ -52,9 +65,9 @@ class Pg extends Adapter {
return new Result();
}

let cols = [];
const cols = [];
result.fields.forEach(field => {
cols = field.name;
cols.push(field.name);
});

return new Result(result.rows, cols);

+ 0
- 25
lib/adapters/Pg/PgNative.js View File

@@ -1,25 +0,0 @@
const Pg = require('./Pg');
const pg = require('pg').native;

class PgNative extends Pg {
constructor (config) {
super(config);
let instance = null;
let connectionString = Pg._formatConnectionString(config);

if (connectionString !== '') {
let conn = new pg.Client(connectionString);
conn.connect(err => {
if (err) {
throw new Error(err);
}
});

instance = Promise.resolve(conn);
}

super.instance = instance;
}
}

module.exports = PgNative;

+ 1
- 7
lib/adapters/Pg/index.js View File

@@ -1,8 +1,2 @@
const Pg = require('./Pg');
const PgNative = require('./PgNative');

module.exports = config => {
return (config.native)
? new PgNative(config.connection)
: new Pg(config.connection);
};
module.exports = config => new Pg(config.connection);

+ 2
- 2
lib/adapters/Sqlite/dblite.js View File

@@ -5,10 +5,10 @@ const dbliteAdapter = require('dblite');

class SqliteDblite extends Adapter {
constructor (config) {
let file = (Helpers.isString(config)) ? config : config.file;
const file = (Helpers.isString(config)) ? config : config.file;

const instance = new Promise((resolve, reject) => {
let conn = dbliteAdapter(file);
const conn = dbliteAdapter(file);

// Stop the stupid 'bye bye' message being output
conn.on('close', () => {});

+ 2
- 2
lib/adapters/Sqlite/sqlite3.js View File

@@ -5,10 +5,10 @@ const sqlite3 = require('sqlite3').verbose();

class SqliteSqlite3 extends Adapter {
constructor (config) {
let file = (Helpers.isString(config)) ? config : config.file;
const file = (Helpers.isString(config)) ? config : config.file;

const instance = new Promise((resolve, reject) => {
let conn = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, err => {
const conn = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, err => {
if (err) {
reject(err);
}

+ 0
- 3
lib/drivers/MSSQLDriver.js View File

@@ -1,5 +1,3 @@
'use strict';

/**
* Driver for Microsoft SQL Server databases
*
@@ -8,7 +6,6 @@
module.exports = (() => {
delete require.cache[require.resolve('../Driver')];
const driver = require('../Driver');
const Helpers = require('../Helpers');

driver.identifierStartChar = '[';
driver.identifierEndChar = ']';

+ 1
- 3
lib/drivers/Pg.js View File

@@ -5,7 +5,5 @@
*/
module.exports = (() => {
delete require.cache[require.resolve('../Driver')];
let driver = require('../Driver');

return driver;
return require('../Driver');
})();

+ 7
- 7
lib/drivers/Sqlite.js View File

@@ -5,7 +5,7 @@
*/
module.exports = (() => {
delete require.cache[require.resolve('../Driver')];
let driver = require('../Driver');
const driver = require('../Driver');

// Sqlite doesn't have a truncate command
driver.hasTruncate = false;
@@ -22,11 +22,11 @@ module.exports = (() => {
// Get the data values to insert, so they can
// be parameterized
let sql = '';
let first = data.shift();
const first = data.shift();

let vals = [];
const vals = [];
data.forEach(obj => {
let row = [];
const row = [];
Object.keys(obj).forEach(key => {
row.push(obj[key]);
});
@@ -37,8 +37,8 @@ module.exports = (() => {

// Get the field names from the keys of the first
// object to be inserted
let fields = Object.keys(first);
let cols = [];
const fields = Object.keys(first);
const cols = [];
fields.forEach(key => {
cols.push(`'${driver._quote(first[key])}' AS ${driver.quoteIdentifiers(key)}`);
});
@@ -46,7 +46,7 @@ module.exports = (() => {
sql += `SELECT ${cols.join(', ')}\n`;

vals.forEach(rowValues => {
let quoted = rowValues.map(value => String(value).replace('\'', '\'\''));
const quoted = rowValues.map(value => String(value).replace('\'', '\'\''));
sql += `UNION ALL SELECT '${quoted.join('\', \'')}'\n`;
});


+ 12
- 18
package.json View File

@@ -1,6 +1,6 @@
{
"name": "ci-node-query",
"version": "6.0.0",
"version": "5.0.0",
"description": "A query builder for node based on the one in CodeIgniter",
"author": "Timothy J Warren <tim@timshomepage.net>",
"engines": {
@@ -42,27 +42,31 @@
"glob": "^7.0.3",
"mysql2": "^1.2.0",
"pg": "^7.4",
"require-reload": "~0.2.2",
"sqlite3": "^3.1.8",
"tedious": "^2.0.0",
"xregexp": "^4.0.0"
},
"devDependencies": {
"babel-eslint": "^8.2.1",
"chai": "^4.1",
"chai-as-promised": "^7.1",
"documentation": "latest",
"eslint": "^4.16.0",
"eslint-config-happiness": "^10.2.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"globstar": "^1.0.0",
"happiness": "^10.0",
"jest": "^22.0.0",
"jsdoc": "^3.4.3",
"npm-run-all": "^4.0.2",
"nsp": "^3.1",
"pg-native": "^2.2"
"require-reload": "~0.2.2"
},
"license": "MIT",
"jest": {
"collectCoverageFrom": [
"lib/**/*.js"
],
"coverageDirectory": "coverage",
"coverageReporters": [
"html",
@@ -82,21 +86,11 @@
"default": "npm-run-all --parallel audit lint:src lint:tests && npm run test",
"predocs": "globstar -- documentation build -f md -o API.md \"lib/*.js\"",
"docs": "globstar -- documentation build -f html -o docs \"lib/*.js\"",
"fix": "happiness --fix \"lib/**/*.js\" \"test/**/*.js\"",
"fix": "eslint --fix ./lib ./test",
"postdocs": "jsdoc lib -r -d documentation",
"happy": "happiness \"lib/**/*.js\" \"test/**/*.js\"",
"happy:src": "happiness \"lib/**/*.js\"",
"happy:tests": "happiness \"test/**/*.js\"",
"lint": "npm-run-all lint:tests lint:src happy",
"lint": "npm-run-all lint:src lint:tests",
"lint:src": "eslint ./lib",
"lint:tests": "eslint ./test",
"test": "jest"
},
"happiness": {
"env": {
"es6": true,
"jest": true
},
"parser": "babel-eslint"
}
}

+ 11
- 0
test/adapters/__snapshots__/dblite_test.js.snap View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Dblite adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \\"create_test\\" WHERE \\"id\\" = ?"`;

exports[`Dblite adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \\"create_test\\" (\\"id\\",\\"key\\",\\"val\\") VALUES (?,?,?)"`;

exports[`Dblite adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;

exports[`Dblite adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;

exports[`Dblite adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \\"create_test\\" SET \\"id\\"=?,\\"key\\"=?,\\"val\\"=? WHERE \\"id\\" = ?"`;

+ 11
- 0
test/adapters/__snapshots__/mysql2_test.js.snap View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Mysql2 adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \`create_test\` WHERE \`id\` = ?"`;

exports[`Mysql2 adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \`create_test\` (\`id\`,\`key\`,\`val\`) VALUES (?,?,?)"`;

exports[`Mysql2 adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \`id\`, \`key\` AS \`k\`, \`val\` FROM \`create_test\` WHERE (\`id\` > ? AND \`id\` < ?) LIMIT 1,2"`;

exports[`Mysql2 adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \`id\`, \`key\` AS \`k\`, \`val\` FROM \`create_test\` WHERE (\`id\` > ? AND \`id\` < ?) LIMIT 1,2"`;

exports[`Mysql2 adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \`create_test\` SET \`id\`=?,\`key\`=?,\`val\`=? WHERE \`id\` = ?"`;

+ 11
- 0
test/adapters/__snapshots__/pg_test.js.snap View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Pg adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \\"create_test\\" WHERE \\"id\\" = ?"`;

exports[`Pg adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \\"create_test\\" (\\"id\\",\\"key\\",\\"val\\") VALUES (?,?,?)"`;

exports[`Pg adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;

exports[`Pg adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;

exports[`Pg adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \\"create_test\\" SET \\"id\\"=?,\\"key\\"=?,\\"val\\"=? WHERE \\"id\\" = ?"`;

+ 11
- 0
test/adapters/__snapshots__/sqlite3_test.js.snap View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Sqlite3 adapter tests - Get compiled - getCompiledDelete 1`] = `"DELETE FROM \\"create_test\\" WHERE \\"id\\" = ?"`;

exports[`Sqlite3 adapter tests - Get compiled - getCompiledInsert 1`] = `"INSERT INTO \\"create_test\\" (\\"id\\",\\"key\\",\\"val\\") VALUES (?,?,?)"`;

exports[`Sqlite3 adapter tests - Get compiled - getCompiledSelect 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;

exports[`Sqlite3 adapter tests - Get compiled - getCompiledSelect 2 1`] = `"SELECT \\"id\\", \\"key\\" AS \\"k\\", \\"val\\" FROM \\"create_test\\" WHERE (\\"id\\" > ? AND \\"id\\" < ?) LIMIT 2 OFFSET 1"`;

exports[`Sqlite3 adapter tests - Get compiled - getCompiledUpdate 1`] = `"UPDATE \\"create_test\\" SET \\"id\\"=?,\\"key\\"=?,\\"val\\"=? WHERE \\"id\\" = ?"`;

+ 1
- 21
test/adapters/dblite_test.js View File

@@ -19,7 +19,7 @@ describe('Dblite adapter tests -', () => {
});

testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
it('Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
.from('create_test')
.where('id', 'ABS(-88)')
@@ -27,26 +27,6 @@ describe('Dblite adapter tests -', () => {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Batch', async () => {
let data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];

let promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});
afterAll(() => {
qb.end();
});

+ 3
- 24
test/adapters/mysql2_test.js View File

@@ -24,7 +24,7 @@ describe('Mysql2 adapter tests -', () => {
});

testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
it('Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
.from('create_test')
.where('id', 'CEILING(SQRT(88))')
@@ -36,28 +36,7 @@ describe('Mysql2 adapter tests -', () => {
let promise = await qb.truncate('create_test');
expect(promise).toEqual(expect.anything());
});
it('Test Insert Batch', async () => {
let data = [
{
id: 5442,
key: 4,
val: Buffer.from('7')
}, {
id: 892,
key: 35,
val: Buffer.from('10 o\'clock')
}, {
id: 482,
key: 404,
val: 97
}
];

const promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});

/* describeTeardown(() => {
afterAll(() => {
qb.end();
}); */
});
});

+ 4
- 24
test/adapters/pg_test.js View File

@@ -45,36 +45,16 @@ describe('Pg adapter tests -', () => {
});

testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
it('Select with function and argument in WHERE clause', async () => {
const promise = await qb.select('id')
.from('create_test')
.where('id', 'CEILING(SQRT(88))')
.get();

expect(promise).toEqual(expect.anything());
});
it('Promise - Test Truncate', async () => {
let promise = await qb.truncate('create_test');
expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Batch', async () => {
let data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];

let promise = await qb.insertBatch('create_test', data);
it('Test Truncate', async () => {
const promise = await qb.truncate('create_test');
expect(promise).toEqual(expect.anything());
});
afterAll(() => {

+ 1
- 21
test/adapters/sqlite3_test.js View File

@@ -20,7 +20,7 @@ describe('Sqlite3 adapter tests -', () => {
});

testRunner(qb);
it('Promise - Select with function and argument in WHERE clause', async () => {
it('Select with function and argument in WHERE clause', async () => {
let promise = await qb.select('id')
.from('create_test')
.where('id', 'ABS(-88)')
@@ -28,26 +28,6 @@ describe('Sqlite3 adapter tests -', () => {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Batch', async () => {
let data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];

let promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});
afterAll(() => {
qb.end();
});

+ 0
- 5
test/base.js View File

@@ -1,13 +1,8 @@
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);

// Load the test config file
const configFile = './config.json';

module.exports = {
config: require(configFile),
expect: chai.expect,
tests: require('./base/tests'),
promiseTestRunner: require('./base/adapterPromiseTestRunner')
};

+ 101
- 15
test/base/adapterPromiseTestRunner.js View File

@@ -37,13 +37,14 @@ module.exports = function promiseTestRunner (qb) {
});
});
});
describe('DB update tests -', async () => {

describe('DB update tests -', () => {
beforeAll(done => {
let sql = qb.driver.truncate('create_test');
qb.query(sql).then(res => done())
qb.query(sql).then(() => done())
.catch(err => done(err));
});
it('Promise - Test Insert', async () => {
it('Test Insert', async () => {
const promise = await qb.set('id', 98)
.set('key', '84')
.set('val', Buffer.from('120'))
@@ -51,7 +52,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test Insert Object', async () => {
it('Test Insert Object', async () => {
const promise = await qb.insert('create_test', {
id: 587,
key: 1,
@@ -60,7 +61,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test Update', async () => {
it('Test Update', async () => {
const promise = await qb.where('id', 7)
.update('create_test', {
id: 7,
@@ -70,7 +71,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test set Array Update', async () => {
it('Test set Array Update', async () => {
let object = {
id: 22,
key: 'gogle',
@@ -83,7 +84,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test where set update', async () => {
it('Test where set update', async () => {
const promise = await qb.where('id', 36)
.set('id', 36)
.set('key', 'gogle')
@@ -92,17 +93,17 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Test delete', async () => {
it('Test delete', async () => {
const promise = await qb.delete('create_test', {id: 5});
expect(promise).toEqual(expect.anything());
});
it('Promise - Delete with where', async () => {
it('Delete with where', async () => {
const promise = await qb.where('id', 5)
.delete('create_test');

expect(promise).toEqual(expect.anything());
});
it('Promise - Delete multiple where values', async () => {
it('Delete multiple where values', async () => {
const promise = await qb.delete('create_test', {
id: 5,
key: 'gogle'
@@ -111,8 +112,46 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
});
describe('Grouping tests -', async () => {
it('Promise - Using grouping method', async () => {

describe('Batch tests -', () => {
it('Test Insert Batch', async () => {
const data = [
{
id: 544,
key: 3,
val: Buffer.from('7')
}, {
id: 89,
key: 34,
val: Buffer.from('10 o\'clock')
}, {
id: 48,
key: 403,
val: Buffer.from('97')
}
];

const promise = await qb.insertBatch('create_test', data);
expect(promise).toEqual(expect.anything());
});
it('Test Update Batch', async () => {
const data = [{
id: 480,
key: 49,
val: '7x7'
}, {
id: 890,
key: 100,
val: '10x10'
}];

const affectedRows = qb.updateBatch('create_test', data, 'id');
expect(affectedRows).toBe(2);
});
});

describe('Grouping tests -', () => {
it('Using grouping method', async () => {
const promise = await qb.select('id, key as k, val')
.from('create_test')
.groupStart()
@@ -124,7 +163,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Using where first grouping', async () => {
it('Using where first grouping', async () => {
const promise = await qb.select('id, key as k, val')
.from('create_test')
.where('id !=', 5)
@@ -137,7 +176,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Using or grouping method', async () => {
it('Using or grouping method', async () => {
const promise = await qb.select('id, key as k, val')
.from('create_test')
.groupStart()
@@ -152,7 +191,7 @@ module.exports = function promiseTestRunner (qb) {

expect(promise).toEqual(expect.anything());
});
it('Promise - Using or not grouping method', async () => {
it('Using or not grouping method', async () => {
const promise = await qb.select('id, key as k, val')
.from('create_test')
.groupStart()
@@ -168,4 +207,51 @@ module.exports = function promiseTestRunner (qb) {
expect(promise).toEqual(expect.anything());
});
});
describe('Get compiled - ', () => {
it('getCompiledSelect', () => {
const sql = qb.select('id, key as k, val')
.from('create_test')
.groupStart()
.where('id >', 1)
.where('id <', 900)
.groupEnd()
.limit(2, 1)
.getCompiledSelect();

expect(sql).toMatchSnapshot();
});
it('getCompiledSelect 2', () => {
const sql = qb.select('id, key as k, val')
.groupStart()
.where('id >', 1)
.where('id <', 900)
.groupEnd()
.limit(2, 1)
.getCompiledSelect('create_test');

expect(sql).toMatchSnapshot();
});
it('getCompiledInsert', () => {
const sql = qb.set({
id: 587,
key: 1,
val: Buffer.from('2')
}).getCompiledInsert('create_test');

expect(sql).toMatchSnapshot();
});
it('getCompiledUpdate', () => {
const sql = qb.where('id', 36)
.set('id', 36)
.set('key', 'gogle')
.set('val', Buffer.from('non-word'))
.getCompiledUpdate('create_test');

expect(sql).toMatchSnapshot();
});
it('getCompiledDelete', () => {
const sql = qb.where({id: 5}).getCompiledDelete('create_test');
expect(sql).toMatchSnapshot();
});
});
};

+ 12
- 9
test/config-ci.json View File

@@ -18,6 +18,18 @@
"database": "test"
}
},
"pg-native": {
"driver": "pg",
"native": true,
"connection": "postgres://test:test@posgres/test"
},
"pg-object-native": {
"driver": "pg",
"native": true,
"connection": {
"database": "test"
}
},
"dblite": {
"adapter": "dblite",
"driver": "sqlite",
@@ -26,14 +38,5 @@
"sqlite3": {
"driver": "sqlite",
"connection": ":memory:"
},
"node-firebird": {
"driver": "firebird",
"connection": {
"host": "127.0.0.1",
"database": "/../FB_TEST_DB.FDB",
"user": "SYSDBA",
"password": "masterkey"
}
}
}

Loading…
Cancel
Save