sharp-db
Version:
Classes for running SQL and building select queries for MySQL in Node
961 lines (897 loc) • 55.2 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Select/Select.js - Documentation</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger" class="navicon-button x">
<div class="navicon"></div>
</label>
<label for="nav-trigger" class="overlay"></label>
<nav>
<li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading">Classes</li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="DataBroker.html">DataBroker</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="DataBroker.html#cleanup">cleanup</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="DataBroker.html#created_and_modified">created_and_modified</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="DataBroker.html#createdAndModified">createdAndModified</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="DataBroker.html#delete">delete</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="DataBroker.html#insert">insert</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Db.html">Db</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#.destroyAll">destroyAll</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#.endAll">endAll</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#.factory">factory</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#.withInstance">withInstance</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#beginTransaction">beginTransaction</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#bindArgs">bindArgs</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#commit">commit</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#connect">connect</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#connectOnce">connectOnce</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#delete">delete</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#deleteFrom">deleteFrom</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#destroy">destroy</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#emit">emit</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#emitError">emitError</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#end">end</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#escape">escape</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#escapeLike">escapeLike</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#escapeQuoteless">escapeQuoteless</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#exportAsSql">exportAsSql</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#insert">insert</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#insertExtended">insertExtended</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#insertInto">insertInto</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#insertIntoOnDuplicateKeyUpdate">insertIntoOnDuplicateKeyUpdate</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#multiQuery">multiQuery</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#query">query</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#quote">quote</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#rollback">rollback</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#select">select</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectByKey">selectByKey</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectExists">selectExists</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectFirst">selectFirst</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectFrom">selectFrom</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectGrouped">selectGrouped</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectHash">selectHash</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectId">selectId</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectIndexed">selectIndexed</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectList">selectList</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectOrCreate">selectOrCreate</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectOrCreateId">selectOrCreateId</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectUuid">selectUuid</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#selectValue">selectValue</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#startTransaction">startTransaction</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#tpl">tpl</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#update">update</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Db.html#updateTable">updateTable</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="DbEvent.html">DbEvent</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Parser.html">Parser</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_extractSubqueries">_extractSubqueries</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleConditions">_handleConditions</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleCrossJoin">_handleCrossJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleFrom">_handleFrom</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleFullJoin">_handleFullJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleFullOuterJoin">_handleFullOuterJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleGroupBy">_handleGroupBy</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleHaving">_handleHaving</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleInnerJoin">_handleInnerJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleLeftJoin">_handleLeftJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleLeftOuterJoin">_handleLeftOuterJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleLimit">_handleLimit</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleOffset">_handleOffset</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleOrderBy">_handleOrderBy</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleRightJoin">_handleRightJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleRightOuterJoin">_handleRightOuterJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleSelect">_handleSelect</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_handleWhere">_handleWhere</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_injectSubqueries">_injectSubqueries</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_split">_split</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#_stripComments">_stripComments</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Parser.html#parse">parse</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Select.html">Select</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#._extractBindingName">_extractBindingName</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#.init">init</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#.parse">parse</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#_conditions">_conditions</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#_isEntirelyDigits">_isEntirelyDigits</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#_isEntirelyDigitsNoZeros">_isEntirelyDigitsNoZeros</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#_isPlaceholder">_isPlaceholder</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#_spliceChildData">_spliceChildData</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#_spliceSiblingData">_spliceSiblingData</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#bind">bind</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#column">column</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#columns">columns</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#crossJoin">crossJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#escape">escape</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#escapeLike">escapeLike</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#escapeQuoteless">escapeQuoteless</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetch">fetch</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetchFirst">fetchFirst</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetchGrouped">fetchGrouped</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetchHash">fetchHash</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetchIndexed">fetchIndexed</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetchList">fetchList</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fetchValue">fetchValue</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#foundRows">foundRows</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#from">from</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fullJoin">fullJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#fullOuterJoin">fullOuterJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#getClone">getClone</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#getFoundRowsQuery">getFoundRowsQuery</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#getFoundRowsSql">getFoundRowsSql</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#groupBy">groupBy</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#having">having</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#innerJoin">innerJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#join">join</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#leftJoin">leftJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#leftOuterJoin">leftOuterJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#limit">limit</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#normalized">normalized</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#offset">offset</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#option">option</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#orderBy">orderBy</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#orHaving">orHaving</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#orWhere">orWhere</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#page">page</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#parse">parse</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#reset">reset</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#rightJoin">rightJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#rightOuterJoin">rightOuterJoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#sortField">sortField</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#table">table</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#tables">tables</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#toBoundSql">toBoundSql</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#toString">toString</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#unbind">unbind</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#unjoin">unjoin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#where">where</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#whereBetween">whereBetween</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#withChildData">withChildData</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Select.html#withSiblingData">withSiblingData</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Ssh.html">Ssh</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Ssh.html#end">end</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Ssh.html#tunnelTo">tunnelTo</a></span></li>
</nav>
<div id="main">
<h1 class="page-title">Select/Select.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>const mysql = require('mysql2');
const Parser = require('../Parser/Parser.js');
const Db = require('../Db/Db.js');
const cloneDeep = require('lodash/cloneDeep');
const escapeRegExp = require('lodash/escapeRegExp');
const forOwn = require('../forOwnDefined/forOwnDefined.js');
const substrCount = require('quickly-count-substrings');
/**
* Build a select query
* Class Select
*/
class Select {
/**
* Load the given SQL into this object
* @param {String} sql The SQL to parse
* @returns {Select}
*/
parse(sql) {
this.reset();
const parser = new Parser(this);
parser.parse(sql);
return this;
}
/**
* Return a new Select object that matches the given SQL
* @param {String} sql The SQL to parse
* @param {Db} [db] The Db instance to use for queries
* @returns {Select}
*/
static parse(sql, db = null) {
return Select.init(db).parse(sql);
}
/**
* Select constructor
* @param {Db} [db] The Db instance to use
*/
constructor(db = null) {
this.db = db || Db.factory();
this.reset();
}
/**
* Shortcut to initialize without the `new` keyword
* @param {Db} [db] The Db instance to use
* @return {Select}
*/
static init(db = null) {
return new Select(db || Db.factory());
}
/**
* Get the SQL as a pretty-printed string
* @return {String}
*/
toString() {
const lines = [
'SELECT',
this._options.length ? ` ${this._options.join('\n ')}` : null,
this._columns.length ? ` ${this._columns.join(',\n ')}` : ' *\n',
`FROM ${this._tables.join(', ')}`,
this._joins.length ? this._joins.join('\n') : null,
this._wheres.length ? `WHERE ${this._wheres.join('\n AND ')}` : null,
this._groupBys.length ? `GROUP BY ${this._groupBys.join(',\n ')}` : null,
this._havings.length ? `HAVING ${this._havings.join('\n AND ')}` : null,
this._orderBys.length ? `ORDER BY ${this._orderBys.join(',\n ')}` : null,
];
if (this._page > 0) {
const offset = (this._page - 1) * this._limit;
lines.push(`LIMIT ${this._limit}`);
lines.push(`OFFSET ${offset}`);
} else {
if (this._limit) {
lines.push(`LIMIT ${this._limit}`);
}
if (this._offset) {
lines.push(`OFFSET ${this._offset}`);
}
}
return lines.filter(Boolean).join('\n').trim();
}
/**
* Get the SQL as a one-line string
* @return {String}
*/
normalized() {
const lines = [
'SELECT',
this._options.length ? this._options.join(' ') : null,
this._columns.length ? this._columns.join(', ') : '*',
`FROM ${this._tables.join(', ')}`,
this._joins.length ? this._joins.join(' ') : null,
this._wheres.length ? `WHERE ${this._wheres.join(' AND ')}` : null,
this._groupBys.length ? `GROUP BY ${this._groupBys.join(', ')}` : null,
this._havings.length ? `HAVING ${this._havings.join(' AND ')}` : null,
this._orderBys.length ? `ORDER BY ${this._orderBys.join(', ')}` : null,
];
if (this._page > 0) {
const offset = (this._page - 1) * this._limit;
lines.push(`LIMIT ${this._limit}`);
lines.push(`OFFSET ${offset}`);
} else {
if (this._limit) {
lines.push(`LIMIT ${this._limit}`);
}
if (this._offset) {
lines.push(`OFFSET ${this._offset}`);
}
}
return lines.filter(Boolean).join(' ').trim();
}
/**
* Get normalized SQL with all parameters bound
* @returns {String}
*/
toBoundSql() {
const sql = this.normalized();
const options = this.db.bindArgs(sql, [this._bound]);
return options.sql;
}
/**
* @param {String|Array} [field] If given, reset the given component(s), otherwise reset all query components
* Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
* @return {Select}
*/
reset(field = null) {
if (Array.isArray(field)) {
field.forEach(name => this.reset(name));
return this;
}
if (field) {
const pluralizable = [
'option',
'column',
'table',
'where',
'having',
'groupBy',
'orderBy',
];
let prop = '_' + field.replace(/s$/, '');
if (pluralizable.indexOf(field) > -1) {
prop += 's';
}
this[prop] = ['limit', 'offset', 'page'].indexOf(field) > -1 ? null : [];
} else {
/**
* The list of sibling relationship definitions
* @property {Object[]}
* @private
*/
this._siblings = [];
/**
* The list of child relationship definitions
* @property {Object[]}
* @private
*/
this._children = [];
/**
* The list of strings to come immediately after "SELECT"
* and before column names
* @property {String[]}
* @private
*/
this._options = [];
/**
* The list of column names to select
* @property {String[]}
* @private
*/
this._columns = [];
/**
* The list of tables in the FROM clause
* @property {String[]}
* @private
*/
this._tables = [];
/**
* The list of JOIN strings to add
* @property {String[]}
* @private
*/
this._joins = [];
/**
* The list of WHERE clauses
* @property {String[]}
* @private
*/
this._wheres = [];
/**
* The list of HAVING clauses
* @property {String[]}
* @private
*/
this._havings = [];
/**
* The list of GROUP BY clauses
* @property {String[]}
* @private
*/
this._groupBys = [];
/**
* The list of ORDER BY clauses
* @property {String[]}
* @private
*/
this._orderBys = [];
/**
* The LIMIT to use
* @property {Number}
* @private
*/
this._limit = null;
/**
* The OFFSET to use
* @property {Number}
* @private
*/
this._offset = null;
/**
* The page used to construct an OFFSET based on the LIMIT
* @property {Number}
* @private
*/
this._page = null;
/**
* Values to bind by name to the query before executing
* @property {Object}
* @private
*/
this._bound = {};
}
return this;
}
/**
* Specify data from a sibling table be spliced in
* Can be used for one-to-one or many-to-one relationships
* @param {String} property The name of the property into which to splice
* @param {Select} siblingQuery The Select query to fetch the sibling data
* @returns {Select}
* @chainable
*/
withSiblingData(property, siblingQuery) {
this._siblings.push({ property, query: siblingQuery });
return this;
}
/**
* Specify data from a child table to be spliced in
* Can be used for one-to-many or many-to-many relationships
* @param {String} property The name of the property into which to splice
* @param {Select} childQuery The Select query to fetch the child data
* @returns {Select}
*/
withChildData(property, childQuery) {
this._children.push({ property, query: childQuery });
return this;
}
/**
* Bind values by name to the query
* @param {Object|String|Array} placeholder The name of the placeholder or an object with placeholder: value pairs
* @param {*} [value=null] The value to bind when placeholder is a string
* @example
* query.bind('postId', 123); // replace :postId with 123
* query.bind({ postId: 123 }); // replace :postId with 123
* @return {Select}
*/
bind(placeholder, value = null) {
if (typeof placeholder === 'object' && arguments.length === 1) {
forOwn(placeholder, (val, field) => {
this._bound[field] = val;
});
return this;
}
this._bound[placeholder] = value;
return this;
}
/**
* Unbind a previously bound property
* @param {String} [placeholder]
* @return {Select}
*/
unbind(placeholder = null) {
if (placeholder) {
this._bound[placeholder] = undefined;
} else {
this._bound = {};
}
return this;
}
/**
* Fetch records and splice in related data
* @param [options] Query options
* @return {Promise<Object>}
*/
async fetch(options = {}) {
options.sql = this.toString();
const {
query: initialSql,
results,
fields,
} = await this.db.select(options, this._bound);
const queries1 = await this._spliceChildData(results);
const queries2 = await this._spliceSiblingData(results);
const queries = [initialSql, ...queries1, ...queries2];
return { queries, results, fields };
}
/**
* Fetch the first matched record
* @return {Object|null}
*/
async fetchFirst(options = {}) {
options.sql = this.toString();
const oldLimit = this._limit;
this.limit(1);
const { queries, results, fields } = await this.fetch(options);
this.limit(oldLimit);
return { queries, results: results[0], fields };
}
/**
* Fetch each record as an object with key-value pairs
* @return {Promise<Object>}
*/
fetchHash(options = {}) {
options.sql = this.toString();
return this.db.selectHash(options, this._bound);
}
/**
* Fetch each record as an array of values
* @return {Promise<Object>}
*/
fetchList(options = {}) {
options.sql = this.toString();
return this.db.selectList(options, this._bound);
}
/**
* Fetch the value of first column of the first record
* @return {Promise}
*/
fetchValue(options = {}) {
options.sql = this.toString();
return this.db.selectValue(options, this._bound);
}
/**
* Fetch values and index by the given field name
* @param {String} byField The field by which to index (e.g. id)
* @return {Promise<Object>}
*/
async fetchIndexed(byField, options = {}) {
options.sql = this.toString();
const { queries, results, fields } = await this.fetch(options);
const indexed = {};
results.forEach(r => (indexed[r[byField]] = r));
return { queries, results: indexed, fields };
}
/**
* Fetch values grouped by the given field name
* @param {String} byField The field by which to group
* @example
* const query = Select.parse('SELECT * FROM comments');
* const byUser = query.fetchGrouped('user_id')
* // a key for each user id with an array of comments for each key
* @return {Array}
*/
async fetchGrouped(byField, options = {}) {
options.sql = this.toString();
const { query, results, fields } = await this.fetch(options);
const grouped = {};
results.forEach(r => {
if (!grouped[r[byField]]) {
grouped[r[byField]] = [];
}
grouped[r[byField]].push(r);
});
return { query, results: grouped, fields };
}
/**
* Clone this object
* @return {Select}
*/
getClone() {
const copy = new Select();
copy._children = cloneDeep(this._children);
copy._siblings = cloneDeep(this._siblings);
copy._options = cloneDeep(this._options);
copy._columns = cloneDeep(this._columns);
copy._tables = cloneDeep(this._tables);
copy._joins = cloneDeep(this._joins);
copy._wheres = cloneDeep(this._wheres);
copy._havings = cloneDeep(this._havings);
copy._groupBys = cloneDeep(this._groupBys);
copy._orderBys = cloneDeep(this._orderBys);
copy._limit = this._limit;
copy._offset = this._offset;
copy._page = this._page;
copy._bound = cloneDeep(this._bound);
return copy;
}
/**
* Build a version of this query that simply returns COUNT(*)
* @param {String} [countExpr="*"] Use to specify `DISTINCT colname` if needed
* @return {Select} The SQL query
*/
getFoundRowsQuery(countExpr = '*') {
if (this._havings.length === 0) {
const clone = this.getClone();
clone._columns = [`COUNT(${countExpr}) AS foundRows`];
clone._options = [];
clone._groupBys = [];
clone._orderBys = [];
clone._limit = null;
clone._offset = null;
clone._page = null;
return clone;
} else {
const subquery = this.getClone();
subquery._limit = null;
subquery._offset = null;
subquery._page = null;
return subquery;
}
}
/**
* Get SQL needed to return the found rows of this query
* @param {String} countExpr The expression to use inside the COUNT()
* @param {Boolean} normalize If true, return a normalized sql
* @returns {String}
*/
getFoundRowsSql(countExpr = '*', normalize = false) {
const query = this.getFoundRowsQuery(countExpr);
if (this._havings.length === 0) {
return normalize ? query.normalized() : query.toString();
} else if (normalize) {
const subquerySql = query.normalized();
return `SELECT COUNT(*) AS foundRows FROM (${subquerySql}) AS subq`;
} else {
const subquerySql = query.toString().replace(/\n/g, '\n\t');
return `SELECT COUNT(*) AS foundRows FROM (\n\t${subquerySql}\n) AS subq`;
}
}
/**
* Run a version of this query that simply returns COUNT(*)
* @param {String} [countExpr="*"] Use to specify `DISTINCT colname` if needed
* @return {Promise<Number>} The number of rows or false on error
*/
foundRows(countExpr = '*', options = {}) {
options.sql = this.getFoundRowsSql(countExpr);
return this.db.selectValue(options, this._bound);
}
/**
* Extract the name of the first bound variable
* E.g. given "SELECT * FROM users WHERE id IN(:id)" it would return "id"
* @param {String} sql
* @returns {*|string}
* @private
*/
static _extractBindingName(sql) {
const match = sql.match(/:([\w_]+)/);
if (!match) {
throw new Error(`Unable to find bound variable in SQL "${sql}"`);
}
return match[1];
}
/**
* Fetch sibling data and splice it into the given result set
* @param {Array} queries The final SQL statements that were executed
*/
async _spliceSiblingData(records) {
if (this._siblings.length === 0 || records.length === 0) {
return [];
}
const sqlQueries = [];
for (const { property, query } of this._siblings) {
const onColumn = Select._extractBindingName(query.toString());
const values = records.map(record => record[onColumn]);
query.bind(onColumn, values);
const { queries, results, fields } = await query.fetch();
const indexed = {};
const firstField = fields[0].name;
results.forEach(result => {
const key = result[firstField];
indexed[key] = result;
});
records.forEach(record => {
record[property] = indexed[record[onColumn]];
});
sqlQueries.push(...queries);
}
return sqlQueries;
}
/**
* Fetch child data and splice it into the given result set
* @param {Array} queries The final SQL statements that were executed
*/
async _spliceChildData(records) {
if (this._children.length === 0 || records.length === 0) {
return [];
}
const sqlQueries = [];
for (const { property, query } of this._children) {
const onColumn = Select._extractBindingName(query.toString());
const values = records.map(record => record[onColumn]);
query.bind(onColumn, values);
const { queries, results, fields } = await query.fetch();
const firstField = fields[0].name;
const grouped = {};
results.forEach(result => {
const key = result[firstField];
if (!grouped[key]) {
grouped[key] = [];
}
grouped[key].push(result);
});
records.forEach(record => {
record[property] = grouped[record[onColumn]] || [];
});
sqlQueries.push(...queries);
}
return sqlQueries;
}
/**
* Add an array of column names to fetch
* @param {String[]} columnNames The names of columns
* @return {Select}
*/
columns(columnNames) {
this._columns = this._columns.concat(columnNames);
return this;
}
/**
* Add a column name to fetch
* @param {String} columnName The name of the column
* @return {Select}
*/
column(columnName) {
this._columns.push(columnName);
return this;
}
/**
* Add an option expression such as "TOP 10" or "SQL_CALC_FOUND_ROWS"
* @param {String} optionExpression Expression to go after "SELECT" and before column list
* @return {Select}
*/
option(optionExpression) {
this._options.push(optionExpression);
return this;
}
/**
* Add a table to the "FROM" clause (same as .from())
* @param {String} tableName The name of the table to query
* @return {Select}
*/
table(tableName) {
this._tables.push(tableName);
return this;
}
/**
* Add multiple table to the "FROM" clause
* @param {Array} tableNames The names of the tables to query
* @return {Select}
*/
tables(tableNames) {
this._tables.push(...tableNames);
return this;
}
/**
* Add a table to the "FROM" clause (same as .table())
* @param {String} tableName The name of the table to query
* @return {Select}
*/
from(tableName) {
this._tables.push(tableName);
return this;
}
/**
* Add an INNER JOIN expression (same as .innerJoin())
* @param {String} expression The expression following the INNER JOIN keyword
* @example query.join('posts p ON p.id = c.post_id');
* @return {Select}
*/
join(expression) {
this._joins.push(`INNER JOIN ${expression}`);
return this;
}
/**
* Add a LEFT JOIN expression
* @param {String} expression The expression following the LEFT JOIN keyword
* @example query.leftJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
leftJoin(expression) {
this._joins.push(`LEFT JOIN ${expression}`);
return this;
}
/**
* Add a FULL JOIN expression
* @param {String} expression The expression following the FULL JOIN keyword
* @example query.fullJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
fullJoin(expression) {
this._joins.push(`FULL JOIN ${expression}`);
return this;
}
/**
* Add a RIGHT JOIN expression
* @param {String} expression The expression following the RIGHT JOIN keyword
* @example query.rightJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
rightJoin(expression) {
this._joins.push(`RIGHT JOIN ${expression}`);
return this;
}
/**
* Add a CROSS JOIN expression
* @param {String} expression The expression following the CROSS JOIN keyword
* @example query.join('posts p ON p.id = c.post_id');
* @return {Select}
*/
crossJoin(expression) {
this._joins.push(`CROSS JOIN ${expression}`);
return this;
}
/**
* Add an INNER JOIN expression (same as ->join())
* @param {String} expression The expression following the INNER JOIN keyword
* @example query.innerJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
innerJoin(expression) {
this._joins.push(`INNER JOIN ${expression}`);
return this;
}
/**
* Add a LEFT OUTER JOIN expression
* @param {String} expression The expression following the LEFT OUTER JOIN keyword
* @example query.leftOuterJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
leftOuterJoin(expression) {
this._joins.push(`LEFT OUTER JOIN ${expression}`);
return this;
}
/**
* Add a FULL OUTER JOIN expression
* @param {String} expression The expression following the FULL OUTER JOIN keyword
* @example query.fullOuterJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
fullOuterJoin(expression) {
this._joins.push(`FULL OUTER JOIN ${expression}`);
return this;
}
/**
* Add a RIGHT OUTER JOIN expression
* @param {String} expression The expression following the RIGHT OUTER JOIN keyword
* @example query.rightOuterJoin('posts p ON p.id = c.post_id');
* @return {Select}
*/
rightOuterJoin(expression) {
this._joins.push(`RIGHT OUTER JOIN ${expression}`);
return this;
}
/**
* Remove a join condition with the specified table
* @param {String|String[]} table The name of the table or tables in the first part of the join statement
* @return {Select}
*/
unjoin(table) {
if (Array.isArray(table)) {
table.forEach(t => this.unjoin(t));
return this;
}
table = escapeRegExp(table);
this._joins = this._joins.filter(join => {
const regex = new RegExp(`^([A-Z]+) JOIN ${table}\\b`);
return !regex.test(join);
});
return this;
}
/**
* Utility function to add conditions for a clause (WHERE, HAVING)
* @param {Array} collection The collection to add the clauses to (e.g. this._wheres or this._havings)
* @param {Array} criteria A list of expressions to stringify
* @property {*} criteria[0] The expression or name of the column on which to match
* @property {*} [criteria[1]] The comparison operator; defaults to "="
* @property {*} [criteria[2]] The value to test against
* @example The following are equivalent
* this._conditions(this._wheres, ['deleted_at IS NULL']);
* this._conditions(this._wheres, ['deleted_at', null]);
* this._conditions(this._wheres, ['deleted_at', '=', null]);
* @example More examples
* this._conditions(this._wheres, ['fname', 'LIKE', 'joe']); // `fname` LIKE 'joe'
* this._conditions(this._wheres, ['fname', 'LIKE ?', 'joe']); // `fname` LIKE 'joe'
* this._conditions(this._wheres, ['fname LIKE %?%', 'joe']); // `fname` LIKE '%joe%'
* this._conditions(this._wheres, ['fname LIKE ?%', 'joe']); // `fname` LIKE 'joe%'
* this._conditions(this._wheres, ['fname', 'LIKE ?%', 'joe']); // `fname` LIKE 'joe%'
* this._conditions(this._wheres, ['price >', 10]); // `price` > 10
* this._conditions(this._wheres, ['price', '>', 10]); // `price` > 10
* this._conditions(this._wheres, ['price =', 10]); // `price` = 10
* this._conditions(this._wheres, ['price !=', 10]); // `price` != 10
* this._conditions(this._wheres, ['price', 10]); // `price` = 10
* this._conditions(this._wheres, ['price', '=', 10]); // `price` = 10
* this._conditions(this._wheres, ['price', '!=', 10]); // `price` != 10
* this._conditions(this._wheres, ['price', 'BETWEEN', [10,20]]); // `price` BETWEEN 10 AND 20
* this._conditions(this._wheres, ['price', 'NOT BETWEEN', [10,20]]); // `price` NOT BETWEEN 10 AND 20
* this._conditions(this._wheres, ['price', [10,20]]); // `price` IN(10,20)
* this._conditions(this._wheres, ['price', '=', [10,20]]); // `price` IN(10,20)
* this._conditions(this._wheres, ['price', 'IN', [10,20]]); // `price` IN(10,20)
* this._conditions(this._wheres, ['price', 'NOT IN', [10,20]]); // `price` NOT IN(10,20)
* @return {Select}
*/
_conditions(collection, criteria) {
if (typeof criteria === 'string') {
collection.push(criteria);
return this;
}
const numArgs = criteria.length;
let [column, operator, value] = criteria;
if (Array.isArray(column)) {
column.forEach(val => {
this._conditions(collection, [val]);
});
return this;
} else if (typeof column === 'object') {
forOwn(column, (val, name) => {
this._conditions(collection, [name, val]);
});
return this;
}
if (/^\w+$/.test(column)) {
column = mysql.escapeId(column);
}
if (numArgs === 1) {
// condition is a stand-alone expression
// e.g. "SUM(price) > 10"
collection.push(column);
return this;
} else if (
numArgs === 2 &&
Array.isArray(operator) &&
operator.length > 0 &&
substrCount(column, '?') === operator.length
) {
// column is a string with question marks and operator is an array of replacements
// e.g. query.where('SUBSTR(prefs, ?, ?) = role', [1, 4]);
const values = operator;
let i = 0;
const sql = column.replace(/(%|)\?(%|)/g, ($0, $1, $2) => {
const escNoQuotes = this.escapeQuoteless(values[i++]);
return `'${$1}${escNoQuotes}${$2}'`;
});
collection.push(sql);
return this;
} else if (numArgs === 2) {
// condition has pairs of "column + operator" => "value"
// e.g. ["price >", 10]
// e.g. ["status LIKE ?%", 10]
value = operator;
const parts = column.split(' ');
column = parts.shift();
operator = parts.join(' ');
}
if (!operator) {
operator = '=';
}
operator = operator.toLocaleUpperCase();
const likeMatch = operator.match(
/^(LIKE|NOT LIKE)(?: (\?|\?%|%\?|%\?%))?$/i
);
if (operator === 'NOT BETWEEN' || operator === 'BETWEEN') {
// expect a two-item array
const from = mysql.escape(value[0]);
const to = mysql.escape(value[1]);
collection.push(`${column} ${operator} ${from} AND ${to}`);
} else if (likeMatch) {
const like = likeMatch[1].toUpperCase(); // Either LIKE or NOT LIKE
const infix = likeMatch[2]; // ONE OF ?% or %?% or %? or ?
if (Array.isArray(value)) {
const ors = [];
for (const v of value) {
const quoted = this.escapeLike(infix, v);
ors.push(`${column} ${like} ${quoted}`);
}
const joined = ors.join(' OR ');
collection.push(`(${joined})`);
} else {
const quoted = this.escapeLike(infix, value);
collection.push(`${column} ${like} ${quoted}`);
}
} else if (value === null) {
collection.push(
operator === '=' ? `${column} IS NULL` : `${column} IS NOT NULL`
);
} else if (Array.isArray(value)) {
// an array of values should be IN or NOT IN
const inVals = value.map(v => mysql.escape(v));
const joined = inVals.join(',');
collection.push(
operator === '=' || operator === 'IN'
? `${column} IN(${joined})`
: `${column} NOT IN(${joined})`
);
} else if (operator === 'IN' || operator === 'NOT IN') {
// in clause that is not array
value = mysql.escape(value);
collection.push(`${column} ${operator}(${value})`);
} else {
value = mysql.escape(value);
collection.push(`${column} ${operator} ${value}`);
}
return this;
}
/**
* Add a group by column or expression
* @param {String} column The name of a column (or expression) to group by
* @return {Select}
*/
groupBy(column) {
this._groupBys.push(column);
return this;
}
/**
* Add WHERE clauses to conditions (See _conditions for usage)
* @param {String} column The expression or name of the column on which to match
* @param {*} [operator] The comparison operator; defaults to "="
* @param {*} [value] The value to test against
* @return {Select}
*/
where(...args) {
this._conditions(this._wheres, args);
return this;
}
/**
* Add a WHERE clause with a BETWEEN condition
* @param {String} column The column name
* @param {Array} twoValueArray The two values to be between
* @return {Select}
*/
whereBetween(column, twoValueArray) {
const isNullish = v =>
v === undefined || v === null || v === false || isNaN(v);
if (!isNullish(twoValueArray[0]) && !isNullish(twoValueArray[1])) {
this.where(column, 'BETWEEN', twoValueArray);
} else if (!isNullish(twoValueArray[0]) && isNullish(twoValueArray[1])) {
this.where(column, '>=', twoValueArray[0]);
} else if (isNullish(twoValueArray[0]) && !isNullish(twoValueArray[1])) {
this.where(column, '<=', twoValueArray[1]);
} else {
// both are nullish!
throw new Error(
'Select.whereBetween(): Array must have at least 1 non nullish value'
);
}
return this;
}
/**
* Add WHERE conditions to place inside an OR block (See _conditions for usage)
* @param {Array} conditions A list where each item is an array with parameters that would be taken by where()
* @return {Select}
*/
orWhere(conditions) {
const criteria = [];
// TODO: something wrong with this loop
conditions.forEach(condition => {
this._conditions(criteria, condition);
});
const joined = criteria.join(' OR ');
if (joined.slice(0, 1) === '(' && joined.slice(-1) === ')') {
this.where(joined);
} else {
this.where(`(${joined