UNPKG

@cloki/clickhouse-sql

Version:
799 lines (691 loc) 18.5 kB
const EQUALS = "="; const EQ = EQUALS; const GREATER = ">"; const GT = GREATER; const GREATER_OR_EQUALS = ">="; const GTE = GREATER_OR_EQUALS; const LESS = "<"; const LT = LESS; const LESS_OR_EQUALS = "<="; const LTE = LESS_OR_EQUALS; const NOT_EQUALS = "!="; const NE = NOT_EQUALS; const BETWEEN = "between"; const AND = 'and' const Consts = { EQUALS, GREATER, GREATER_OR_EQUALS, LESS, LESS_OR_EQUALS, NOT_EQUALS, EQ, GT, GTE, LT, LTE, NE, }; class SQLObject { } class Conditions extends SQLObject { constructor(...args) { super(); this.args = args; } push(arg) { this.args.push(arg); } get length() { return this.args.length; } _argToStr(arg) { return (arg.toString) ? arg.toString() : arg; } } class Disjunction extends Conditions { constructor(...args) { super(...args); } toString() { const self = this; return this.args.length ? this.args.map(arg => `(${self._argToStr(arg)})`).join(" or ") : ""; } } class Conjunction extends Conditions { constructor(...args) { super(...args); } toString() { if (!this.args.length) return ""; const self = this; return this.args.map((arg) => `(${self._argToStr(arg)})`).join(" and "); } } class Condition extends SQLObject { constructor(column, operator, value) { super(); this.column = quoteTerm(column); this.operator = operator; this.value = (value instanceof SQLObject) ? value : quoteVal(value); } toString() { if (this.operator) { return [this.column, this.operator, this.value].join(' '); } else { return this.column; } } } class Negation extends Condition { constructor(column, operator, value) { super(column, operator, value); } toString() { return "not(" + super.toString() + ")"; } } class InclusionOperator extends Condition { constructor(inclusionType, column, operator, value) { super(column, operator, value); this.operator = inclusionType; } toString() { return [ quoteTerm(this.column), " ", this.operator, " (", Array.isArray(this.value) ? this.value.map(val => quoteVal(val)).join(',') : quoteVal(this.value), ")" ].join(''); } } class In extends InclusionOperator { constructor(column, operator, values) { super("in", column, operator, values); } } class NotIn extends InclusionOperator { constructor(column, operator, values) { super("not in", column, operator, values); } } class GlobalNotIn extends InclusionOperator { constructor(column, operator, values) { super("global not in", column, operator, values); } } class GlobalIn extends InclusionOperator { constructor(column, operator, values) { super("global in", column, operator, values); } } class Between extends SQLObject { constructor(column, leftBoundary, rightBoundary) { super(); this.column = column; this.leftBoundary = leftBoundary; this.rightBoundary = rightBoundary; } quoteBoundary(b) { return b instanceof SQLObject ? b.toString() : quoteVal(b); } toString() { return [ quoteTerm(this.column), " ", BETWEEN, // because super class adding parents around value and I didn't find an elegant // way how to avoid this. So I'm overloading toString() and copy-pasting implementation this.quoteBoundary(this.leftBoundary), AND, this.quoteBoundary(this.rightBoundary) ].join(' '); } } const Operators = { Conjunction, Disjunction, Negation, In, NotIn, GlobalIn, GlobalNotIn }; function createCondition(...args) { switch (args.length) { case 1: return args[0] instanceof Condition ? args[0] : new Condition(args[0]); case 2: return new Condition(args[0], EQUALS, args[1]); case 3: return new Condition(...args); default: throw new Error("Invalid condition args: ", args); } } let commonReplacer = [ /[\0\n\r\b\t\\\x1a]/g, (s) => { switch (s) { case "\0": return "\\0"; case "\n": return "\\n"; case "\b": return "\\b"; case "\r": return "\\r"; case "\t": return "\\t"; case "\\": return "\\\\"; case "\x1a" : return "\\Z"; default: console.error("uncovered case in replacer:", s); return ''; //logic error } } ]; class Value extends SQLObject { constructor(value) { super(); this.value = value; } toString() { if (typeof this.value === "string") { return "'" + this.value.replace(...commonReplacer).replace(/'/g, "''") + "'"; } if (typeof this.value === 'undefined') { return "''"; } if (Array.isArray(this.value)) { return this.value.map(val => new Value(val)).join(); } return this.value + ''; } } class Term extends SQLObject { constructor(term) { super(); this.term = term; } toString() { const parts = this.term.split('.'); if (parts.length > 1) { return [new Term(parts[0]).toString(), new Term(parts[1]).toString()].join('.'); } return '`' + this.term.replace(...commonReplacer).replace(/`/g, '\\`') + '`'; } } function quoteVal(value) { return value instanceof SQLObject ? value : new Value(value); } function quoteTerm(term) { return term instanceof SQLObject || Number.isFinite(term) ? term : new Term(term); } class SQLFunction extends SQLObject { constructor(name, ...args) { super(); this.name = name; this.args = args; } toString() { return this.name + "(" + this.args.map((arg) => arg instanceof SQLObject || Number.isFinite(arg) ? arg : quoteTerm(arg)).join() + ")"; } } const _curry_f = (name) => { return (...args) => new SQLFunction(name, ...args) }; const AggregateFunctions = { count: _curry_f('count'), any: _curry_f('any'), anyLast: _curry_f('anyLast'), min: _curry_f('min'), max: _curry_f('max'), sum: _curry_f('sum'), avg: _curry_f('avg'), uniq: _curry_f('uniq'), uniqCombined: _curry_f('uniqCombined'), uniqHLL12: _curry_f('uniqHLL12'), uniqExact: _curry_f('uniqExact'), groupArray: _curry_f('groupArray'), groupUniqArray: _curry_f('groupUniqArray') }; const ArithmeticFunctions = { plus: _curry_f('plus'), minus: _curry_f('minus'), multiply: _curry_f('multiply'), divide: _curry_f('divide'), intDiv: _curry_f('intDiv'), intDivOrZero: _curry_f('intDivOrZero'), modulo: _curry_f('modulo'), negate: _curry_f('negate'), abs: _curry_f('abs') }; const TimeFunctions = { toYear: _curry_f('toYear'), toMonth: _curry_f('toMonth'), toDayOfMonth: _curry_f('toDayOfMonth'), toDayOfWeek: _curry_f('toDayOfWeek'), toHour: _curry_f('toHour'), toMinute: _curry_f('toMinute'), toTime: _curry_f('toTime'), toDate: _curry_f('toDate'), toDateTime: _curry_f('toDateTime'), toDateTime64: _curry_f('toDateTime64'), toStartOfDay: _curry_f('toStartOfDay'), toStartOfMonth: _curry_f('toStartOfMonth'), toStartOfQuarter: _curry_f('toStartOfQuarter'), toStartOfYear: _curry_f('toStartOfYear'), toStartOfMinute: _curry_f('toStartOfMinute'), toStartOfFiveMinute: _curry_f('toStartOfFiveMinute'), toStartOfHour: _curry_f('toStartOfHour'), now: _curry_f('now'), today: _curry_f('today'), yesterday: _curry_f('yesterday') }; const IPAddrFunctions = { toIPv4: _curry_f('toIPv4'), toIPv6: _curry_f('toIPv6'), IPv4NumToString: _curry_f('IPv4NumToString'), IPv4StringToNum: _curry_f('IPv4StringToNum'), IPv4NumToStringClassC: _curry_f('IPv4NumToStringClassC'), IPv6NumToString: _curry_f('IPv6NumToString'), IPv6StringToNum: _curry_f('IPv6StringToNum') }; class Raw extends SQLObject { constructor(string) { super(); this.raw = string; } toString() { return this.raw; } } class Query extends SQLObject { } class Parameter extends SQLObject { constructor(name) { super() this.name = name; this.value = null; } set(value) { this.value = value; } get() { return this.value; } toString() { if (typeof this.value === 'undefined') { throw new Error('Unspecified parameter ' + this.name); } return this.value.toString(); } } class With extends Query { constructor(alias, query, inline) { super(); this.alias = alias; if (!query instanceof Select) { throw new Error('query should be a select query'); } this.query = query; this.inline = inline; } toString() { if (this.inline) { return ''; } this.fmt = undefined; return `${this.alias} AS (${this.query.toString()})` } } class WithReference extends SQLObject { constructor(ref) { super(); if (!ref instanceof With) { throw new Error('reference should be a With object'); } this.ref = ref; } toString() { if (this.ref.inline) { return `(${this.ref.query.toString()})`; } return this.ref.alias; } } class Subquery extends SQLObject { /** * * @param query {Select} */ constructor(query) { super(); this.query = query; } toString() { return `(${this.query})`; } } class UnionAll extends Query { constructor(...queries) { super(); this.queries = queries; } toString() { return this.queries.join(' UNION ALL '); } } class Select extends Query { constructor() { super(); this._init() } _init(q) { q = q || {}; this.ctx = q.ctx || {}; this.dist = false; this.params = q.params || {}; this.withs = q.withs || {}; this.tables = q.tables || []; this.joins = q.joins || []; this.conditions = q.conditions || new Conjunction(); this.having_conditions = q.having_conditions || new Conjunction(); this.preconditions = q.preconditions || new Conjunction(); this.aggregations = q.aggregations || []; this.select_list = q.select_list || []; this.order_expressions = q.order_expressions || []; this.request_totals = q.request_totals || undefined; this.sampling = q.sampling || undefined; this.limits = q.limits || undefined; this.limitbycolumns = q.limitbycolumns || undefined; this.fmt = q.fmt || undefined; } distinct(dist) { this.dist = dist; return this; } clone() { const s = new Select(); s._init(this); return s; } select(...columns) { if (columns.length === 0) { return this.select_list; } columns.forEach((col) => this.select_list.push(col)); return this; } /** * * @param queries {With} */ with(...queries) { if (!queries.length) { return this.withs; } let withs = []; for (const q of queries) { withs.push.apply(withs, Object.values(q.query.withs ? q.query.withs : {})); this.ctx = {...this.ctx, ...q.query.ctx}; this.params = {...this.params, ...q.query.params}; q.query.withs = {}; } withs = [...withs, ...queries] for (const q of withs) { this.withs[q.alias] = q; this.fmt = q.query.fmt || this.fmt; q.query.fmt = undefined; } return this; } /** * * @param param {Parameter} */ addParam(param) { this.params[param.name] = param return this; } /** * * @param name {string} */ getParam(name) { return this.params[name]; } /** * @usage * q = selectBuilder->from('table0', ['table1', 'alias1'], { 'table2' : 'alias2'})->toString() * assertEquals(q, "select * from table0, table1 as alias1, table2 as alias2"); * * @param tables * @return {Select|Array} */ from(...tables) { if (!tables.length) { return this.tables; } tables = tables.map(table => { if (typeof table === "string") return [quoteTerm(table)]; if (Array.isArray(table)) { if (table[0] instanceof Query) table[0] = new Subquery(table[0]); else if (table[0] instanceof SQLObject) table[0] = table[0]; else table[0] = quoteTerm(table[0]); table[1] = quoteTerm(table[1]); return table; } if (table instanceof Query) return [new Subquery(table)]; if (table instanceof SQLObject) return [table]; let alias = Object.values(table)[0]; if (alias instanceof Select) alias = '(' + alias.toString() + ')'; else alias = quoteTerm(alias); return [alias, quoteTerm(Object.keys(table)[0])]; }); this.tables = tables; return this; } join(table, type, ...conditions) { if (typeof table === "string") table = quoteTerm(table); if (!Array.isArray(conditions)) { conditions = [conditions]; } if (conditions.length > 1) { conditions = Conjunction(conditions); } this.joins.push({ table, type, conditions }); return this; } prewhere(...args) { this.preconditions.push(createCondition(...args)); return this; } orPrewhere(...args) { let condition = createCondition(...args); if (this.preconditions.length) { this.preconditions = new Disjunction(this.preconditions, condition) } else { this.preconditions.push(condition) } return this; } where(...args) { if (args.length === 1 && args[0] instanceof Condition) { this.conditions.push(args[0]); } else { this.conditions.push(createCondition(...args)); } return this; } orWhere(...args) { let condition = createCondition(...args); if (this.conditions.length) { this.conditions = new Disjunction(this.conditions, condition) } else { this.conditions.push(condition); } return this; } groupBy(...aggregateExpressions) { aggregateExpressions.forEach(a => this.aggregations.push(a)); return this; } withTotals(request_totals = true) { this.request_totals = request_totals; return this; } limit(number, offset) { if (!number && !offset) { this.limits = undefined return this } this.limits = { number, offset }; return this; } limitBy(limit, ...columns) { this.limitbycolumns = { limit, columns }; return this; } orderBy(...expressions) { if (!expressions.length) { return this.order_expressions; } this.order_expressions = expressions; return this; } format(fmt) { this.fmt = fmt; return this; } having(...args) { if (!args.length) { return this.having_conditions; } if (args.length === 1 && args[0] instanceof Condition) { this.having_conditions.push(args[0]); } else { this.having_conditions.push(createCondition(...args)); } return this; } toString() { let wth = Object.values(this.with()).filter(w => !w.inline); wth = wth.length ? "WITH " + wth.join(', ') : ''; let select_list; if (this.select_list.length === 0) { select_list = "*"; } else { select_list = this.select_list.map( c => Array.isArray(c) ? quoteTerm(c[0]) + ' as ' + quoteTerm(c[1]) : quoteTerm(c) ).join(); } const dist = this.dist ? 'DISTINCT' : ''; let from = this.from().map( (table) => table.length === 1 ? table[0] : table[0] + ' as ' + table[1] ); from = from.length ? "from " + from.join() : ""; let join = this.joins.map((join) => { const table = Array.isArray(join.table) && join.table.length > 1 ? [quoteTerm(join.table[0]), join.table[1]].join(' as ') : join.table; return ((join.type) ? join.type + ' ' : '') + 'join ' + table + (join.type === 'array' ? ' ' : ' on ' + join.conditions); }).join(' '); let prewhere = this.preconditions.length ? "prewhere " + this.preconditions : ""; let where = this.conditions.length ? "where " + this.conditions : ""; let groupby = this.aggregations.length ? "group by " + this.aggregations.map(c => quoteTerm(c)).join() : ""; let having = this.having_conditions.length ? "having " + this.having_conditions : ""; let order_by = this.order_expressions.length ? "order by " + this.order_expressions.map(e => Array.isArray(e) ? quoteTerm(e[0]) + " " + e[1] : quoteTerm(e)).join() : ""; let with_totals = this.request_totals ? "with totals" : ""; let sample = this.sampling ? "sample " + this.sampling : ""; let limitby = this.limitbycolumns && this.limitbycolumns.columns.length ? "limit " + this.limitbycolumns.limit + " by " + this.limitbycolumns.columns.map(c => quoteTerm(c)).join() : ''; let limit_number = this.limits && typeof this.limits.number !== "undefined" ? this.limits.number.toString() : undefined; let limit_offset = this.limits && typeof this.limits.offset !== "undefined" ? this.limits.offset.toString() : undefined; let limit = limit_number || limit_offset ? "limit " + ((typeof limit_offset === "undefined") ? limit_number : limit_offset + ", " + limit_number) : ''; let format = this.fmt ? " format " + this.fmt.toUpperCase() : ""; const parts = [ wth, "select", dist, select_list, from, join, sample, prewhere, where, groupby, with_totals, having, order_by, limitby, limit, format, ].filter((v) => v && v != ''); return parts.join(' '); } } const Queries = { Select, With, WithReference, Raw, UnionAll }; const Utility = { quoteVal, val: quoteVal, quoteTerm, term: quoteTerm, raw: (s) => new Raw(s), Condition: (...args) => new Condition(...args), Parameter }; const Shortcuts = { And: (...args) => new Conjunction(...args), Or: (...args) => new Disjunction(...args), Eq: (col, val) => new Condition(col, Consts.EQ, val), Ne: (col, val) => new Condition(col, Consts.NE, val), Gte: (col, val) => new Condition(col, Consts.GTE, val), Lte: (col, val) => new Condition(col, Consts.LTE, val), Lt: (col, val) => new Condition(col, Consts.LT, val), Gt: (col, val) => new Condition(col, Consts.GT, val), in: (col, values) => new In(col, null, values), notIn: (col, values) => new NotIn(col, null, values), cast: (thing, t) => new SQLFunction('cast', thing, quoteVal(t)), between: (col, left, right) => new Between(col, left, right) }; const Dialect = { ...Operators, ...AggregateFunctions, ...ArithmeticFunctions, ...TimeFunctions, ...IPAddrFunctions, ...Consts, ...Queries, ...Utility, ...Shortcuts, }; export default Dialect;