futoin-database
Version:
Neutral database interface with powerful Query and revolution Transaction builders
1,323 lines (1,118 loc) • 36.4 kB
JavaScript
;
/**
* @file
*
* Copyright 2017 FutoIn Project (https://futoin.org)
* Copyright 2017 Andrey Galkin <andrey@futoin.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const _driverImpl = new Map();
const _cloneDeep = require( 'lodash/cloneDeep' );
const FIELD_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*|\.\*)?$/;
const COND = '$COND$';
const COND_RE = /^(.+)\s(=|<>|!=|>|>=|<|<=|IN|NOT IN|BETWEEN|NOT BETWEEN|LIKE|NOT LIKE)\s*$/;
const IS_DEBUG = process && process.env.NODE_ENV !== 'production';
/**
* Wrapper for raw expression to prevent escaping
*/
class Expression {
constructor( expr ) {
this._expr = expr;
}
toQuery() {
return this._expr;
}
/**
* Allows easy joining with raw query
* @returns {string} as is
*/
toString() {
return this._expr;
}
}
/**
* Interface for prepared statement execution
*/
class Prepared {
/**
* @func
* @name Prepared#execute
* @param {AsyncSteps} as - step interface
* @param {object} [params=null] - parameters to subsitute
*/
/**
* @func
* @name Prepared#executeAsync
* @param {AsyncSteps} as - step interface
* @param {object} [params=null] - parameters to subsitute
*/
}
/**
* Additional helpers interface
*/
class Helpers {
entity( entity ) {
void entity;
throw new Error( 'Not implemented' );
}
escape( value, op=undefined ) {
void value;
void op;
throw new Error( 'Not implemented' );
}
identifier( name ) {
void name;
throw new Error( 'Not implemented' );
}
expr( expr ) {
void expr;
throw new Error( 'Not implemented' );
}
/**
* Get DB-specific current timestamp expression
* @func
* @name now
* @returns {Expression} - current timestamp
*/
/**
* Convert native timestamp to DB format
* @func
* @name date
* @param {moment|string} value - native timestamp
* @returns {Expression} - timestamp in DB format
*/
/**
* Convert DB timestamp to native format
* @func
* @name nativeDate
* @param {string} value - timestamp in DB format
* @returns {moment} - timestamp in moment.js
*/
/**
* Create expression representing date modification of
* input expression by specified number of seconds.
* @func
* @name dateModify
* @param {Expression|string} expr - source expression (e.g field name)
* @param {seconds} seconds - number of seconds to add/subtract(negative)
* @returns {Expression} - DB expression
*/
/**
* Concat arguments. Useful for in-query operation with unknown values.
* @func
* @name concat
* @param {string|Expression} value... - String to escape or Expresion object
* @returns {Expression} - concatenated argument expression
*/
/**
* Cast expression to type
* @func
* @name cast
* @param {string|Expression} value - String to escape or Expresion object
* @param {type} type - target type
* @returns {Expression} - cast expression
* @note implementation by implicitly substitute not supported types with acceptable
* equiavelnt like JSON->TEXT/
*/
/**
* Add arguments in query.
* @func
* @name add
* @param {string|Expression} a... - arguments
* @returns {Expression} - addition expression
*/
/**
* Subtract arguments in query.
* @func
* @name sub
* @param {string|Expression} a - first arg
* @param {string|Expression} b - second arg
* @returns {Expression} - subtraction expression
*/
/**
* Multiply arguments in query.
* @func
* @name mul
* @param {string|Expression} a... - arguments
* @returns {Expression} - multiplication expression
*/
/**
* Divide arguments in query.
* @func
* @name div
* @param {string|Expression} a - first arg
* @param {string|Expression} b - second arg
* @returns {Expression} - division expression
*/
/**
* Reminder of division in query.
* @func
* @name rem
* @param {string|Expression} a - first arg
* @param {string|Expression} b - second arg
* @returns {Expression} - reminder expression
*/
/**
* Get minimal value of arguments.
* @func
* @name least
* @param {string|Expression} a... - arguments
* @returns {Expression} - addition expression
*/
/**
* Get maximal value of arguments.
* @func
* @name greatest
* @param {string|Expression} a... - arguments
* @returns {Expression} - addition expression
*/
}
/**
* Basic interface for DB flavour support
* @private
*/
class IDriver {
get helpers() {
return this._helpers;
}
constructor( helpers ) {
this.COND = COND;
this._helpers = helpers || new Helpers();
}
backref( query_id, field, multi ) {
void query_id;
void field;
void multi;
throw new Error( 'Not implemented' );
}
checkField( field ) {
if ( typeof field !== 'string' || !field.match( FIELD_RE ) ) {
throw new Error( `Invalid field name: ${field}` );
}
}
build( _state ) {
throw new Error( 'Not implemented' );
}
_ensureUsed( state, used ) {
for ( let f in state ) {
let v = state[f];
if ( v instanceof Map ) {
if ( v.size && !used[f] ) {
throw new Error( `Unused map "${f}"` );
}
} else if ( v instanceof Array ) {
if ( v.length && !used[f] ) {
throw new Error( `Unused array "${f}"` );
}
} else if ( v && !used[f] ) {
throw new Error( `Unused generic "${f}"` );
}
}
}
}
/**
* Basic logic for SQL-based helpers
*/
class SQLHelpers extends Helpers {
entity( entity ) {
if ( typeof entity === 'string' ) {
return entity;
} else if ( entity instanceof Array ) {
if ( entity.length !== 2 ) {
throw new Error(
`Entity as array format is [name, alias]: ${entity}`
);
}
let q = entity[0];
const alias = entity[1];
if ( q instanceof QueryBuilder ) {
if ( q._state.type !== 'SELECT' ) {
throw new Error( 'Not a SELECT sub-query' );
}
q = q._toQuery();
q = `(${q})`;
}
return `${q} AS ${alias}`;
} else if ( entity === null ) {
return null;
} else if ( entity instanceof QueryBuilder ) {
throw new Error(
`Entity as sub-query format is [QB, alias]: ${entity}`
);
} else {
throw new Error( `Unknown entity type: ${entity}` );
}
}
escape( value, op=undefined ) {
if ( value instanceof QueryBuilder ) {
const raw_query = value._toQuery();
return `(${raw_query})`;
}
if ( op === 'BETWEEN' || op === 'NOT BETWEEN' ) {
if ( value instanceof Array && value.length === 2 ) {
const a = this.escape( value[0] );
const b = this.escape( value[1] );
return `${a} AND ${b}`;
} else {
throw new Error( `BETWEEN requires array with two elements` );
}
}
if ( value instanceof Array ) {
const raw_query = value.map( v => this.escape( v ) ).join( ',' );
return `(${raw_query})`;
}
return this._escapeSimple( value );
}
_escapeSimple( _value ) {
throw new Error( 'Not implemented' );
/*
switch (typeof value) {
case 'boolean':
return value ? 'TRUE' : 'FALSE';
case 'string':
return implementation_defined.escape(value);
case 'number':
return `${value}`;
default:
if (value === null) {
return 'NULL';
}
if (value instanceof QueryBuilder.Expression)
{
return value.toQuery();
}
throw new Error(`Unknown type: ${typeof value}`);
}
*/
}
expr( expr ) {
return new Expression( this._expr( expr ) );
}
_expr( expr ) {
if ( expr instanceof QueryBuilder ) {
const raw_query = expr._toQuery();
return `(${raw_query})`;
}
if ( expr instanceof Expression ) {
return expr.toString();
}
if ( typeof expr !== 'string' ) {
throw new Error( 'Expression must be QueryBuilder, Expression or string' );
}
return this._escapeExpr( expr );
}
_escapeExpr( expr ) {
return expr;
}
concat( ...args ) {
const escaped = args.map( ( v ) => this.escape( v ) );
return new Expression(
this.expr( `(${escaped.join( '||' )})` )
);
}
cast( a, type ) {
const expr = this.escape( a );
return new Expression( `CAST(${expr} AS ${type})` );
}
add( ...args ) {
const escaped = args.map( ( v ) => this.escape( v ) );
return new Expression( `(${escaped.join( '+' )})` );
}
sub( a, b ) {
a = this.escape( a );
b = this.escape( b );
return new Expression( `(${a}-${b})` );
}
mul( ...args ) {
const escaped = args.map( ( v ) => this.escape( v ) );
return new Expression( `(${escaped.join( '*' )})` );
}
div( a, b ) {
a = this.escape( a );
b = this.escape( b );
return new Expression( `(${a}/${b})` );
}
mod( a, b ) {
a = this.escape( a );
b = this.escape( b );
return new Expression( `(${a}%${b})` );
}
least( ...args ) {
const escaped = args.map( ( v ) => this.escape( v ) );
return new Expression( `LEAST(${escaped.join( ',' )})` );
}
greatest( ...args ) {
const escaped = args.map( ( v ) => this.escape( v ) );
return new Expression( `GREATEST(${escaped.join( ',' )})` );
}
}
/**
* Basic logic for SQL-based databases
* @private
*/
class SQLDriver extends IDriver {
constructor( helpers ) {
super( helpers || new SQLHelpers() );
}
backref( query_id, field, multi ) {
if ( field !== '$id' ) this.checkField( field );
multi = multi === true ? 'm' : 's';
return new Expression( `$'${query_id}:${field}:${multi}'$` );
}
build( state ) {
const type = state.type;
const entity = state.entity;
const q = [];
const use = {
type: true,
entity: true,
select: false,
toset: false,
multiset: false,
where: false,
having: false,
group: false,
order: false,
limit: false,
joins: false,
params: false,
};
const build_cond = ( c ) => {
if ( c instanceof Array ) {
if ( c[0] === COND ) {
if ( c.length == 2 ) {
return c[1];
} else if ( c.length === 4 ) {
return `${c[1]} ${c[2]} ${c[3]}`;
}
} else {
const op = c[0];
const res = [];
const iter = c[Symbol.iterator]();
iter.next();
for ( let iv = iter.next(); !iv.done; iv = iter.next() ) {
let v = iv.value;
let r = build_cond( v );
if ( ( v[0] === 'OR' && v.length > 2 ) ||
( op === 'OR' && v[0] !== COND ) ) {
r = `(${r})`;
}
res.push( r );
}
return res.join( ` ${op} ` );
}
}
throw new Error( `Must not get here: ${c}` );
};
const add_cond = ( kw, cond ) => {
if ( cond.length ) {
q.push( ` ${kw} ` );
q.push( build_cond( cond ) );
}
};
const add_where = () => {
use.where = true;
add_cond( 'WHERE', state.where );
};
const require_entity = () => {
if ( !entity ) {
throw new Error( 'Entity is not set' );
}
};
const require_toset = () => {
if ( !state.toset.size ) {
throw new Error( 'Nothing to set' );
}
use.toset = true;
};
switch ( type ) {
case 'DELETE':
require_entity();
q.push( `DELETE FROM ${entity}` );
add_where();
break;
case 'INSERT': {
const toset = state.toset;
require_entity();
q.push( `INSERT INTO ${entity} ` );
if ( toset instanceof Array ) {
use.toset = true;
const fields = toset[0];
if ( fields.length ) {
q.push( `(${fields.join( ',' )}) ` );
}
q.push( toset[1] );
} else if ( state.multiset ) {
const multiset = state.multiset;
use.multiset = true;
use.toset = true;
// NOTE: side effect
if ( state.toset.size ) {
multiset.push( state.toset );
state.toset = new Map();
}
const first = multiset[0];
const first_keys = Array.from( first.keys() );
q.push( '(' );
q.push( first_keys.join( ',' ) );
q.push( ') VALUES (' );
q.push( Array.from( first.values() ).join( ',' ) );
q.push( ')' );
const row_count = multiset.length;
for ( let i = 1; i < row_count; ++i ) {
const row = multiset[i];
if ( row.size != first.size ) {
throw new Error( 'Multi-row field count mismatch' );
}
const vals = first_keys.map( f => {
const v = row.get( f );
if ( v === undefined ) {
throw new Error( `Multi-row missing field: ${f}` );
}
return v;
} );
q.push( ',(' + vals.join( ',' ) + ')' );
}
} else {
require_toset();
q.push( '(' );
q.push( Array.from( toset.keys() ).join( ',' ) );
q.push( ') VALUES (' );
q.push( Array.from( toset.values() ).join( ',' ) );
q.push( ')' );
}
break;
}
case 'SELECT': {
q.push( 'SELECT ' );
if ( state.select.size ) {
q.push( this._build_select_part( state.select ) );
use.select = true;
} else {
q.push( '*' );
}
if ( !entity ) break;
//---
q.push( ` FROM ${entity}` );
//---
for ( let j of state.joins ) {
q.push( ` ${j.type} JOIN ${j.entity}` );
add_cond( 'ON', j.cond );
}
use.joins = true;
//---
add_where();
//---
const group = state.group;
if ( group.length ) {
q.push( ` GROUP BY ${group.join( ',' )}` );
use.group = true;
}
//---
use.having = true;
add_cond( 'HAVING', state.having );
//---
const order = state.order;
if ( order.length ) {
const order_parts = order.map( ( v ) => `${v[0]} ${v[1]}` );
q.push( ` ORDER BY ${order_parts.join( ',' )}` );
use.order = true;
}
//---
const limit = state.limit;
if ( limit && this._isLimitOffsetSupport() ) {
use.limit = true;
q.push( ` LIMIT ${limit[0]}` );
const offset = limit[1];
if ( offset !== undefined ) {
q.push( ` OFFSET ${offset}` );
}
}
break;
}
case 'UPDATE': {
require_entity();
require_toset();
q.push( `UPDATE ${entity} SET ` );
const fields = [];
for ( let [ f, v ] of state.toset.entries() ) {
fields.push( `${f}=${v}` );
}
q.push( fields.join( ',' ) );
use.toset = true;
add_where();
break;
}
case 'CALL':
q.push( this._build_call( entity, state.params ) );
use.params = true;
break;
case 'GENERIC':
throw new Error( 'GENERIC query cannot be built' );
default:
throw new Error( `Unsupported query type ${type}` );
}
if ( IS_DEBUG ) {
this._ensureUsed( state, use );
}
return q.join( '' );
}
_build_select_part( select ) {
const fields = [];
for ( let [ f, v ] of select.entries() ) {
if ( v === f ) {
fields.push( `${f}` );
} else {
fields.push( `${v} AS ${f}` );
}
}
return fields.join( ',' );
}
_build_call( entity, params ) {
return `CALL ${entity}(${params.join( ',' )})`;
}
_isLimitOffsetSupport() {
return true;
}
}
/**
* Neutral query builder
*/
class QueryBuilder {
/**
* Base for QB Driver implementation
*/
static get IDriver() {
return IDriver;
}
/**
* Base for SQL-based QB Driver implementation
*/
static get SQLDriver() {
return SQLDriver;
}
/**
* Wrapper for raw expressions
*/
static get Expression() {
return Expression;
}
/**
* Interface of Prepared statement
*/
static get Prepared() {
return Prepared;
}
/**
* Base for Helpers
*/
static get Helpers() {
return Helpers;
}
/**
* Base for SQLHelpers
*/
static get SQLHelpers() {
return SQLHelpers;
}
/**
* @internal
* @param {QueryBuilder|L1Face} qb_or_lface - ref
* @param {string} db_type - type of driver
* @param {string} type - type of driver
* @param {string|null} entity - primary target to operate on
*/
constructor( qb_or_lface, db_type=null, type=null, entity=null ) {
if ( qb_or_lface instanceof QueryBuilder ) {
this._lface = qb_or_lface._lface;
this._db_type = qb_or_lface._db_type;
this._state = _cloneDeep( qb_or_lface._state );
} else {
this._lface = qb_or_lface;
this._db_type = db_type;
this._state = {
type: type ? type.toUpperCase() : 'GENERIC',
entity: this.helpers().entity( entity ),
select: new Map(),
toset: new Map(),
multiset: null,
where: [],
having: [],
group: [],
order: [],
limit: null,
joins: [],
params: [],
};
}
}
/**
* Register query builder driver implementation
* @param {string} type - type of driver
* @param {IDriver|function|string|object} module - implementation
*/
static addDriver( type, module ) {
_driverImpl.set( type, module );
}
/**
* Get implementation of previously registered driver
* @param {string} type - type of driver
* @returns {IDriver} actual implementation of query builder driver
*/
static getDriver( type ) {
let impl = _driverImpl.get( type );
if ( typeof impl === 'undefined' ) {
throw new Error( `Unknown DB type: ${type}` );
} else if ( impl instanceof IDriver ) {
return impl;
} else if ( typeof impl === 'string' && typeof module !== 'undefined' ) {
impl = module.require( impl );
impl = new impl( type );
} else if ( typeof impl === 'function' ) {
if ( typeof impl.prototype === 'object' &&
impl.prototype instanceof IDriver ) {
impl = new impl;
} else {
impl = impl();
}
} else {
throw new Error( 'Not supported driver definition' );
}
_driverImpl[type] = impl;
return impl;
}
/**
* Get related QB driver
* @returns {IDriver} actual implementation of query builder driver
*/
getDriver() {
return this.constructor.getDriver( this._db_type );
}
/**
* Get a copy of Query Builder
* @returns {QueryBuilder} copy which can be processed independently
*/
clone() {
return new QueryBuilder( this );
}
/**
* Escape value for embedding into raw query
* @param {*} value - value, array or sub-query to escape
* @returns {string} driver-specific escape
*/
escape( value ) {
return this.helpers().escape( value );
}
/**
* Escape identifier for embedding into raw query
* @param {string} name - raw identifier to escape
* @returns {string} driver-specific escape
*/
identifier( name ) {
return this.helpers().identifier( name );
}
/**
* Wrap raw expression to prevent escaping.
* @param {string} expr - expression to wrap
* @return {Expression} wrapped expression
*/
expr( expr ) {
return this.helpers().expr( expr );
}
/**
* Wrap parameter name to prevent escaping.
* @param {string} name - name to wrap
* @return {Expression} wrapped expression
*/
param( name ) {
return this.expr( `:${name}` );
}
/**
* Get additional helpers
* @returns {Helpers} - db-specific helpers object
*/
helpers() {
return this.getDriver().helpers;
}
/**
* Set fields to retrieve.
*
* Can be called multiple times for appending.
* @p fields can be a Map or object:
* - keys are field names as is
* - values - any expression which is not being escaped automatically
* @p fields can be a list of field names (array)
* - values - field names
* @p fields can be a single string
* - optional @p value is expresion
*
* Value can be another QueryBuilder instance.
*
* @param {Map|object|string|array} fields - see concept for details
* @param {*} [value=undefined] - optional value for
* @returns {QueryBuilder} self
*/
get( fields, value=undefined ) {
const select = this._state.select;
const driver = this.getDriver();
const helpers = driver.helpers;
if ( value !== undefined ) {
driver.checkField( fields );
select.set( fields, helpers.expr( value ) );
} else if ( fields instanceof Map ) {
for ( let [ f, v ] of fields.entries() ) {
driver.checkField( f );
select.set( f, helpers.expr( v ) );
}
} else if ( fields instanceof Array ) {
for ( let f of fields ) {
driver.checkField( f );
select.set( f, f );
}
} else if ( typeof fields === 'object' ) {
for ( let f in fields ) {
driver.checkField( f );
select.set( f, helpers.expr( fields[f] ) );
}
} else if ( typeof fields === 'string' ) {
driver.checkField( fields );
select.set( fields, fields );
} else {
throw new Error( `Not supported fields definition: ${fields}` );
}
return this;
}
/**
* Database neutral way to request last insert ID
*
* For databases without RETURNING or OUTPUT clause in INSERT it
* is expected to always return '$id' field on insert.
*
* For others, it would build a valid RETURNING/OUTPUT clause.
*
* @param {string} field - field name with auto-generated value
* @returns {QueryBuilder} self
*/
getInsertID( field ) {
const select = this._state.select;
const driver = this.getDriver();
driver.checkField( field );
select.set( '$id', field );
return this;
}
/**
* Save current set() context and start new INSERT row
* @returns {QueryBuilder} self
*/
newRow() {
const state = this._state;
if ( state.type !== 'INSERT' ) {
throw new Error( 'Not an INSERT query for multi-row' );
}
if ( state.toset instanceof Array ) {
throw new Error( 'INSERT-SELECT can not be mixed with multirow' );
}
if ( !state.multiset ) {
state.multiset = [];
}
if ( state.toset.size ) {
state.multiset.push( state.toset );
state.toset = new Map();
}
return this;
}
/**
* Add fields to set in UPDATE query.
*
* @p fields can be Map or object to setup multiple fields at once.
* - keys - key name as is, no escape
* - value - any value to be escaped or QueryBuilder instance
*
* Single field => value can be used as shortcut for object form.
*
* @param {Map|object|string} field - field(s) to assign
* @param {string|number|null|QueryBuilder} [value=undefined] - value to assign
* @returns {QueryBuilder} self
*/
set( field, value=undefined ) {
const toset = this._state.toset;
const driver = this.getDriver();
const helpers = driver.helpers;
if ( toset instanceof Array ) {
throw new Error( 'INSERT-SELECT can not be mixed with others' );
}
if ( value !== undefined ) {
driver.checkField( field );
toset.set( field, helpers.escape( value ) );
} else if ( field instanceof Map ) {
for ( let [ f, v ] of field.entries() ) {
driver.checkField( f );
toset.set( f, helpers.escape( v ) );
}
} else if ( field instanceof QueryBuilder ) {
if ( toset.size ) {
throw new Error( 'INSERT-SELECT can not be mixed with others' );
}
if ( field._state.type !== 'SELECT' ) {
throw new Error( 'Not a SELECT sub-query' );
}
if ( this._state.type !== 'INSERT' ) {
throw new Error( 'Not an INSERT query for INSERT-SELECT' );
}
this._state.toset = [ Array.from( field._state.select.keys() ), field._toQuery() ];
} else if ( typeof field === 'object' ) {
for ( let f in field ) {
driver.checkField( f );
toset.set( f, helpers.escape( field[f] ) );
}
} else {
throw new Error( `Not supported set definition: ${field}` );
}
return this;
}
/**
* Control "WHERE" part
* @param {*} conditions - constraints to add
* @param {*} [value=undefined] - optional value for single field
* @returns {QueryBuilder} self
*/
where( conditions, value=undefined ) {
if ( value !== undefined ) {
conditions = { [conditions]: value };
}
this._processConditions( this._state.where, conditions );
return this;
}
/**
* Control "HAVING" part
* @param {*} conditions - constraints to add
* @param {*} [value=undefined] - optional value for single field
* @returns {QueryBuilder} self
* @see QueryBuilder.where
*/
having( conditions, value=undefined ) {
if ( value !== undefined ) {
conditions = { [conditions]: value };
}
this._processConditions( this._state.having, conditions );
return this;
}
/**
* Append group by
* @param {string} field_expr - field or expressions
* @returns {QueryBuilder} self
*/
group( field_expr ) {
this._state.group.push( field_expr );
return this;
}
/**
* Append order by
* @param {string} field_expr - field or expressions
* @param {Boolean} [ascending=true] - ascending sorting, if true
* @returns {QueryBuilder} self
*/
order( field_expr, ascending=true ) {
const order = ascending ? 'ASC' : 'DESC';
this._state.order.push( [ field_expr, order ] );
return this;
}
/**
* Limit query output
*
* @param {integer} count - size
* @param {integer} [offset=0] - offset
* @returns {QueryBuilder} self
* @note if @p count is omitted then @p start is used as count!
*/
limit( count, offset = undefined ) {
this._state.limit = [ count, offset ];
return this;
}
/**
* Add "JOIN" part
* @param {string} type - e.g. INNER, LEFT
* @param {string|array} entity - fornat is the same as of QueryBuilder
* @param {*} conditions - constraints to add
* @returns {QueryBuilder} self
* @see QueryBuilder.where
*/
join( type, entity, conditions=undefined ) {
const joins = this._state.joins;
const cond = [];
if ( conditions ) {
this._processConditions( cond, conditions );
}
entity = this.helpers().entity( entity );
joins.push( {
type,
entity,
cond,
} );
return this;
}
/**
* Add "INNER JOIN"
* @param {string|array} entity - fornat is the same as of QueryBuilder
* @param {*} conditions - constraints to add
* @returns {QueryBuilder} self
* @see QueryBuilder.where
*/
innerJoin( entity, conditions=undefined ) {
return this.join( 'INNER', entity, conditions );
}
/**
* Add "LEFT JOIN"
* @param {string|array} entity - fornat is the same as of QueryBuilder
* @param {*} conditions - constraints to add
* @returns {QueryBuilder} self
* @see QueryBuilder.where
*/
leftJoin( entity, conditions=undefined ) {
return this.join( 'LEFT', entity, conditions );
}
/**
* Complete query and execute through associated interface.
* @param {AsyncSteps} as - steps interface
* @param {Boolean} unsafe_dml - raise error, if DML without conditions
* @see L1Face.query
*/
execute( as, unsafe_dml=false ) {
const q = this._toQuery( unsafe_dml );
this._lface.query( as, q );
}
/**
* Complete query and execute through associated interface.
* @param {AsyncSteps} as - steps interface
* @param {Boolean} unsafe_dml - raise error, if DML without conditions
* @see L1Face.query
* @see L1Face.associateResult
*/
executeAssoc( as, unsafe_dml=false ) {
this.execute( as, unsafe_dml );
as.add( ( as, res ) => {
const rows = this._lface.associateResult( res );
as.success( rows, res.affected );
} );
}
/**
* Prepare statement for efficient execution multiple times
* @param {Boolean} unsafe_dml - raise error, if DML without conditions
* @returns {ExecPrepared} closue with prepared statement
*/
prepare( unsafe_dml=false ) {
const q = this._toQuery( unsafe_dml );
const iface = this._lface;
return new class extends Prepared {
execute( as, params=null ) {
if ( params ) {
iface.paramQuery( as, q, params );
} else {
iface.query( as, q );
}
}
executeAssoc( as, params ) {
this.execute( as, params );
as.add( ( as, res ) => {
const rows = iface.associateResult( res );
as.success( rows, res.affected );
} );
}
};
}
_toQuery( unsafe_dml=true ) {
const state = this._state;
if ( !unsafe_dml &&
( state.type !== 'SELECT' ) &&
( state.type !== 'INSERT' ) &&
!state.where.length ) {
throw new Error( 'Unsafe DML' );
}
return this.getDriver().build( state );
}
_processConditions( target, conditions ) {
if ( !target.length ) {
target.push( 'AND' );
}
if ( conditions instanceof Array ) {
let dst = target;
const op = conditions[0];
const iter = conditions[Symbol.iterator]();
if ( op === 'OR' || op === 'AND' ) {
iter.next();
if ( op !== target[0] ) {
dst = [ op ];
target.push( dst );
}
}
for ( let iv = iter.next(); !iv.done; iv = iter.next() ) {
this._processConditions( dst, iv.value );
}
} else if ( typeof conditions === 'string' ) {
target.push( [ COND, conditions ] );
} else {
const helpers = this.helpers();
const confField = ( f, v ) => {
const m = f.match( COND_RE );
let op = '=';
if ( m ) {
f = m[1];
op = m[2];
}
return [
COND,
f,
op,
helpers.escape( v, op ),
];
};
if ( conditions instanceof Map ) {
for ( let [ f, v ] of conditions.entries() ) {
target.push( confField( f, v ) );
}
} else if ( typeof conditions === 'object' ) {
for ( let f in conditions ) {
target.push( confField( f, conditions[f] ) );
}
} else {
throw new Error( `Unknown condition type: ${conditions}` );
}
}
}
_callParams( args ) {
const params = this._state.params;
const helpers = this.helpers();
args.forEach( ( v ) => params.push( helpers.escape( v ) ) );
return this;
}
toString() {
return this._toQuery( true );
}
static _replaceParams( helpers, q, params, used_params=null ) {
for ( let p in params ) {
let v = helpers.escape( params[p] );
let nq = q.replace(
new RegExp( `(:${p})($|\\W)`, 'g' ),
`${v}$2` );
if ( nq === q ) {
if ( !used_params ) throw new Error( `Unused param "${p}"` );
} else if ( used_params ) {
used_params[p] = true;
}
q = nq;
}
return q;
}
}
module.exports = QueryBuilder;