Commit da1d29a8 authored by Dian Fay's avatar Dian Fay
Browse files

fix: ensure common statement options are handled consistently (fixes #694)

parent cf7f8473
Loading
Loading
Loading
Loading
Loading
+7 −3
Changes for lib/readable.js: 7 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -88,7 +88,12 @@ Readable.prototype.count = function (conditions = {}, params = []) {
 * @return {Promise} Number of matching documents.
 */
Readable.prototype.countDoc = function (criteria = {}) {
  const query = new Select(this, criteria, {exprs: {count: 'COUNT(1)'}, order: null, single: true, generator: 'docGenerator'});
  const query = new Select(this, criteria, {
    exprs: {count: 'COUNT(1)'},
    order: null,
    single: true,
    document: true
  });

  return this.db.query(query).then(res => res.count);
};
@@ -113,7 +118,6 @@ Readable.prototype.find = function (criteria = {}, options = {}) {
 */
Readable.prototype.findDoc = function (criteria = {}, options = {}) {
  options.document = true;
  options.generator = 'docGenerator';

  return this.find(criteria, options);
};
@@ -254,7 +258,7 @@ Readable.prototype.searchDoc = function (plan, options = {}) {
    conditions: `${tsv} @@ to_tsquery($1)`,
    params: [plan.term],
    where: plan.where,
    nestedGenerator: options.document ? 'docGenerator' : 'tableGenerator'
    isDocument: options.document
  };

  options.document = true;  // ensure document result handling activates
+5 −28
Changes for lib/statement/delete.js: 5 added lines, 28 removed lines.
Original line number Diff line number Diff line
'use strict';

const _ = require('lodash');
const parseKey = require('../util/parse-key');
const where = require('./where');
const Statement = require('./statement');

/**
 * Represents a DELETE query.
@@ -14,35 +13,13 @@ const where = require('./where');
 * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Delete options}.
 */
const Delete = function (source, criteria = {}, options = {}) {
  if (source.isPkSearch(criteria, options)) {
    options.generator = 'tableGenerator'; // pks are always table columns
  Statement.call(this, source, options);

    if (!_.isPlainObject(criteria)) {
      // primitive unary pk search
      criteria = _.fromPairs([[source.pk[0], criteria]]);

      options.single = source.loader !== 'join';
    }
  }

  const {conditions, params} = where(source, criteria, 0, options.generator);

  this.source = source;

  // options governing query behavior
  this.build = options.build || false;
  this.decompose = options.decompose;
  this.document = options.document || false;
  this.single = options.single || false;
  this.stream = options.stream || false;

  // options governing SQL statement elements, in rough order of appearance:
  this.only = options.only || false;
  this.conditions = conditions;
  this.returning = options.fields ? options.fields.map(f => parseKey(f, source).lhs) : ['*'];
  this.params = params;
  this.setCriteria(criteria);
};

Delete.prototype = Object.create(Statement.prototype);

/**
 * Format this object into a SQL DELETE.
 *
+5 −9
Changes for lib/statement/insert.js: 5 added lines, 9 removed lines.
Original line number Diff line number Diff line
'use strict';

const _ = require('lodash');
const parseKey = require('../util/parse-key');
const prepareParams = require('../util/prepare-params');
const Statement = require('./statement');

/**
 * Represents an INSERT query.
@@ -14,20 +14,15 @@ const prepareParams = require('../util/prepare-params');
 * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Insert options}.
 */
const Insert = function (source, record, options = {}) {
  this.source = source;
  Statement.call(this, source, options);

  this.records = _.castArray(record);

  const fields = this.compileFieldSet(this.records);

  // options governing query behavior
  this.build = options.build || false;
  this.decompose = options.decompose;
  this.document = options.document || false;
  this.single = !_.isArray(record);
  this.stream = options.stream || false;

  // options governing SQL statement elements, in rough order of appearance:
  this.only = options.only || false;
  this.columns = _.intersection(fields, this.source.columnNames);
  this.junctions = _.difference(fields, this.source.columnNames);

@@ -55,9 +50,10 @@ const Insert = function (source, record, options = {}) {
  this.onConflictIgnore = options.onConflictIgnore;
  this.onConflictUpdate = options.onConflictUpdate;
  this.onConflictUpdateExclude = options.onConflictUpdateExclude;
  this.returning = options.fields ? options.fields.map(f => parseKey(f, source).lhs) : ['*'];
};

Insert.prototype = Object.create(Statement.prototype);

/**
 * Build the set of unique column names being targeted across all records.
 *
+9 −28
Changes for lib/statement/select.js: 9 added lines, 28 removed lines.
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@

const _ = require('lodash');
const parseKey = require('../util/parse-key');
const where = require('./where');
const Statement = require('./statement');

/**
 * Represents a SELECT query.
@@ -14,34 +14,13 @@ const where = require('./where');
 * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Select options}.
 */
const Select = function (source, criteria = {}, options = {}) {
  if (!!source.isPkSearch && source.isPkSearch(criteria)) {
    options.generator = 'tableGenerator'; // pks are always table columns
  Statement.call(this, source, options);

    if (!_.isPlainObject(criteria)) {
      // primitive unary pk search
      criteria = _.fromPairs([[source.pk[0], criteria]]);

      options.single = source.loader !== 'join';
    }
  }

  const {conditions, params} = where(source, criteria, 0, options.generator);

  this.source = source;

  // options governing query behavior; some also affect statement generation,
  // such as options.document with buildSelectList.
  this.build = options.build || false;
  this.document = options.document || false;
  this.decompose = options.decompose;
  this.single = options.single || false;
  this.stream = options.stream || false;
  this.setCriteria(criteria);

  // options governing SQL statement elements, in rough order of appearance:
  this.distinct = options.distinct || false;
  this.selectList = this.buildSelectList(options.fields, options.exprs);
  this.only = options.only || false;
  this.conditions = conditions;
  this.order = _.reduce(options.order, (acc, val) => {
    const direction = val.direction && val.direction.toLowerCase() === 'desc' ? ' DESC' : ' ASC';
    const nulls = val.nulls ? ` NULLS ${val.nulls === 'first' ? 'FIRST' : 'LAST'}` : '';
@@ -55,7 +34,6 @@ const Select = function (source, criteria = {}, options = {}) {
  this.pageLength = options.pageLength;
  this.forUpdate = options.forUpdate || false;
  this.forShare = options.forShare || false;
  this.params = params;

  // with pageLength set for keyset pagination, add last values of ordering
  // fields to criteria
@@ -78,6 +56,8 @@ const Select = function (source, criteria = {}, options = {}) {
  }
};

Select.prototype = Object.create(Statement.prototype);

/**
 * Build a list of strings comprising fields (plus aliases, for document
 * tables and joined or compound Readables) and expressions to be retrieved
@@ -118,9 +98,10 @@ Select.prototype.buildSelectList = function (fields, exprs) {

    // we got nothing *explicitly*, error state
    throw new Error('At least one of fields or exprs must be supplied and must define a field or expression to select.');
  } else if (this.document) {
    // if the user *did* specify something, but we're querying a document table
    // and so require the id field in addition to whatever they're after
  } else if (this.document && fields) {
    // if we're querying fields on a document table, the id must always be
    // included. Expressions always need to be fully qualified, so expression-
    // only queries (e.g. countDoc) aren't affected here.
    selectList.unshift('"id"');
  }

+60 −0
Changes for lib/statement/statement.js: 60 added lines, 0 removed lines.
Original line number Diff line number Diff line
'use strict';

const _ = require('lodash');
const parseKey = require('../util/parse-key');
const where = require('./where');

/**
 * An SQL DML statement.
 *
 * @class
 * @param {Table} source - Database object to query.
 * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Update options}.
 */
const Statement = function (source, options = {}) {
  this.source = source;

  // query and result processing options
  this.build = options.build || false;
  this.decompose = options.decompose;
  this.document = options.document || false;
  this.single = options.single || false;
  this.stream = options.stream || false;

  // common SQL statement modifications
  this.only = options.only || false;
  this.returning = options.fields ? options.fields.map(f => parseKey(f, source).lhs) : ['*'];

  return this;
};

/**
 * Set the conditions and parameters for SELECT, UPDATE, and DELETE queries.
 *
 * @param {Object} criteria - A criteria object.
 * @param {Array} [initialParams] - Existing parameters which will be prepended
 * to parameters generated from criteria.
 */
Statement.prototype.setCriteria = function (criteria, initialParams = []) {
  this.isPkSearch = this.source.isPkSearch(criteria, this);

  if (this.isPkSearch && !_.isPlainObject(criteria)) {
    // primitive unary pk search
    this.criteria = _.fromPairs([[this.source.pk[0], criteria]]);
    this.single = this.source.loader !== 'join';
  } else {
    this.criteria = criteria;
  }

  const {conditions, params} = where(
    this.source,
    this.criteria,
    initialParams.length,
    !this.isPkSearch && this.document
  );

  this.conditions = conditions;
  this.params = initialParams.length ? initialParams.concat(params) : params;
};

module.exports = Statement;
Loading