Skip to main content
Module

x/pg_mem/parser/expression-builder.ts

An in memory postgres DB instance for your unit tests
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
import { _ISelection, IValue, _IType, _ISchema, _IAlias } from '../interfaces-private.ts';import { buildLikeMatcher, nullIsh, hasNullish, intervalToSec, parseTime, asSingleQName, colToStr } from '../utils.ts';import { DataType, CastError, QueryError, NotSupported, nil, ColumnNotFound } from '../interfaces.ts';import hash from 'https://deno.land/x/object_hash@2.0.3.1/mod.ts';import { Value, Evaluator } from '../evaluator.ts';import { Types, isNumeric, reconciliateTypes, ArrayType, RecordCol } from '../datatypes/index.ts';import { Expr, ExprBinary, UnaryOperator, ExprCase, ExprWhen, ExprMember, ExprArrayIndex, ExprTernary, BinaryOperator, SelectStatement, ExprValueKeyword, ExprExtract, Interval, ExprOverlay, ExprSubstring, ExprCall } from 'https://deno.land/x/pgsql_ast_parser@10.5.2/mod.ts';import lru from 'https://deno.land/x/lru_cache@6.0.0-deno.4/mod.ts';import { aggregationFunctions, getAggregator } from '../transforms/aggregation.ts';import moment from 'https://deno.land/x/momentjs@2.29.1-deno/mod.ts';import { IS_PARTIAL_INDEXING } from '../execution/clean-results.ts';import { buildCtx } from './context.ts';import { buildSelect } from '../execution/select.ts';

const builtLru = new lru<_ISelection | null, lru<Expr, IValue>>({ max: 30,});export function buildValue(val: Expr): IValue { const ret = _buildValue(val); checkNotUntypedArray(ret); return ret;}

function checkNotUntypedArray(value: IValue) { // A bit ugly: check that this is not a non typed array (empty array) // see https://github.com/oguimbal/pg-mem/issues/64 // + corresponding UTs const type = value.type; if (type instanceof ArrayType && type.of == Types.null) { throw new QueryError(`cannot determine type of empty array`); }}
export function uncache(data: _ISelection) { builtLru.del(data);}
function _buildValue(val: Expr): IValue { // cache expressions build (they almost are always rebuilt several times in a row) const data = buildCtx().selection; let selLru = builtLru.get(data ?? null); let got: IValue | nil; if (selLru) { got = selLru.get(val); if (got) { return got; } } got = _buildValueReal(val); if (data.isAggregation()) { got = data.checkIfIsKey(got); } if (!selLru) { builtLru.set(data ?? null, selLru = new lru({ max: 50, })); } selLru.set(val, got); return got;}
function _buildValueReal(val: Expr): IValue { const { schema, getParameter, selection } = buildCtx(); switch (val.type) { case 'binary': if (val.op === 'IN' || val.op === 'NOT IN') { return buildIn(val.left, val.right, val.op === 'IN'); } return buildBinary(val); case 'unary': return buildUnary(val.op, val.operand); case 'ref': // try to get a column reference // todo refactor getColumn() to NameResolvers const found = selection.getColumn(val, true); if (found) { return found; } // try to get a parameter reference const arg = !val.table && getParameter(val.name); if (arg) { return arg; } // try to select an aliased record (= a table) const alias = !val.table && selection.selectAlias(val.name); if (alias) { return buildRecord(alias); } throw new ColumnNotFound(colToStr(val)); case 'string': return Value.text(val.value); case 'null': return Value.null(); case 'list': case 'array': const vals = val.expressions.map(x => _buildValue(x)); return val.type === 'list' ? Value.list(vals) : Value.array(vals); case 'numeric': return Value.number(val.value); case 'integer': return Value.number(val.value, Types.integer); case 'call': return _buildCall(val); case 'cast': return _buildValue(val.operand) .cast(schema.getType(val.to)) case 'case': return buildCase(val); case 'member': return buildMember(val); case 'arrayIndex': return buildArrayIndex(val); case 'boolean': return Value.bool(val.value); case 'ternary': return buildTernary(val); case 'select': case 'union': case 'union all': case 'with': case 'with recursive': case 'values': return buildSelectAsArray(val); case 'array select': return buildSelectAsArray(val.select); case 'constant': return Value.constant(val.dataType as any, val.value); case 'keyword': return buildKeyword(val, []); case 'parameter': const [_, n] = /^\$(\d+)$/.exec(val.name) ?? []; if (!n) { throw new Error('Unexpected parameter ref shape: ' + val.name); } return getParameter(parseInt(n) - 1)!; case 'extract': return buildExtract(val); case 'overlay': return buildOverlay(val); case 'substring': return buildSubstring(val); case 'default': throw new QueryError(`DEFAULT is not allowed in this context`, '42601'); default: throw NotSupported.never(val); }}
function buildRecord(alias: _IAlias): IValue { const cols = [...alias.listColumns()]; return new Evaluator( Types.record(cols .map<RecordCol>(x => ({ name: x.id!, type: x.type, }))) , null , Math.random().toString() // must not be indexable => always different hash , [] , (raw, t) => raw, { forceNotConstant: true });}
function _buildCall(val: ExprCall): IValue { // if (typeof val.function !== 'string') { // return buildKeyword( val.function, val.args); // } if (val.over) { throw new NotSupported('"OVER" clause is not implemented in pg-mem yet'); } const nm = asSingleQName(val.function, 'pg_catalog'); if (nm && aggregationFunctions.has(nm)) { const agg = getAggregator(); if (!agg) { throw new QueryError(`aggregate functions are not allowed in WHERE`); } return agg.getAggregation(nm, val); } const args = val.args.map(x => _buildValue(x)); return Value.function(val.function, args);}
function buildKeyword(kw: ExprValueKeyword, args: Expr[]): IValue { if (args.length) { throw new NotSupported(`usage of "${kw.keyword}" keyword with arguments, please file an issue in https://github.com/oguimbal/pg-mem if you need it !`); } if (kw.type !== 'keyword') { throw new Error('Invalid AST'); } switch (kw.keyword) { case 'current_catalog': case 'current_role': case 'current_user': case 'session_user': case 'user': return Value.constant(Types.text(), 'pg_mem'); case 'current_schema': return Value.constant(Types.text(), 'public'); case 'current_date': return Value.constant(Types.date, new Date()); case 'current_timestamp': case 'localtimestamp': return Value.constant(Types.timestamp(), new Date()); case 'localtime': case 'current_time': throw new NotSupported('"date" data type, please file an issue in https://github.com/oguimbal/pg-mem if you need it !'); case 'distinct': throw new NotSupported(kw.keyword); default: throw NotSupported.never(kw.keyword); }}
function buildUnary(op: UnaryOperator, operand: Expr) { const expr = _buildValue(operand);
switch (op) { case 'IS NULL': case 'IS NOT NULL': return Value.isNull(expr, op === 'IS NULL'); case 'IS TRUE': case 'IS NOT TRUE': return Value.isTrue(expr, op === 'IS TRUE'); case 'IS FALSE': case 'IS NOT FALSE': return Value.isFalse(expr, op === 'IS FALSE'); case '+': if (!isNumeric(expr.type)) { throw new CastError(expr.type.primary, DataType.float); } return expr; case 'NOT': case '-': return Value.negate(expr); default: throw NotSupported.never(op, 'Unary operator not supported'); }}
function buildIn(left: Expr, array: Expr, inclusive: boolean): IValue { let leftValue = _buildValue(left); let rightValue = _buildValue(array); return Value.in(leftValue, rightValue, inclusive);}

function buildBinary(val: ExprBinary): IValue { let leftValue = _buildValue(val.left); let rightValue = _buildValue(val.right); return buildBinaryValue(leftValue, val.op, rightValue);}
export function buildBinaryValue(leftValue: IValue, op: BinaryOperator, rightValue: IValue): IValue { function expectSame() { const ll = leftValue.type.primary === DataType.list; const rl = rightValue.type.primary === DataType.list; if (ll !== rl) { function doMap(v: IValue): IValue { return v.map(x => { if (!x) { return x; } if (!Array.isArray(x)) { // not supposed to happen throw new Error(`Was expecting an array. Got instead ${x}`); } if (x.length > 1) { throw new QueryError('more than one row returned by a subquery used as an expression', '21000'); } return x[0]; }, (v.type as ArrayType).of); } if (ll) { leftValue = doMap(leftValue); } else { rightValue = doMap(rightValue); } } const type: _IType = reconciliateTypes([leftValue, rightValue]); leftValue = leftValue.cast(type); rightValue = rightValue.cast(type); return type; } function expectBoth(t: _IType) { leftValue = leftValue.cast(t); rightValue = rightValue.cast(t); }
let getter: (a: any, b: any) => any; let returnType: _IType = Types.bool; let commutative = true; let forcehash: any = null; let rejectNils = true; let impure = false; switch (op) { case '=': { const type = expectSame(); getter = (a, b) => type.equals(a, b); break; } case '!=': { const type = expectSame(); getter = (a, b) => { const ret = type.equals(a, b); return nullIsh(ret) ? null : !ret; }; break; } case '>': { const type = expectSame(); getter = (a, b) => type.gt(a, b); forcehash = { op: '>', left: leftValue.hash, right: rightValue.hash }; break; } case '<': { const type = expectSame(); getter = (a, b) => type.lt(a, b); forcehash = { op: '>', left: rightValue.hash, right: leftValue.hash }; break; } case '>=': { const type = expectSame(); getter = (a, b) => type.ge(a, b); forcehash = { op: '>=', left: leftValue.hash, right: rightValue.hash }; break; } case '<=': { const type = expectSame(); getter = (a, b) => type.le(a, b); forcehash = { op: '>=', left: rightValue.hash, right: leftValue.hash }; break; } case 'AND': case 'OR': expectBoth(Types.bool); rejectNils = false; if (op === 'AND') { getter = (a, b) => a && b; } else { getter = (a, b) => a || b; } break; case '&&': if (leftValue.type.primary !== DataType.array || !rightValue.canCast(leftValue.type)) { throw new QueryError(`Operator does not exist: ${leftValue.type.name} && ${rightValue.type.name}`, '42883'); } rightValue = rightValue.cast(leftValue.type); getter = (a, b) => a.some((element: any) => b.includes(element)); break; case 'LIKE': case 'ILIKE': case 'NOT LIKE': case 'NOT ILIKE': expectBoth(Types.text()); const caseSenit = op === 'LIKE' || op === 'NOT LIKE'; const not = op === 'NOT ILIKE' || op === 'NOT LIKE'; if (rightValue.isConstant) { const pattern = rightValue.get(); if (nullIsh(pattern)) { return Value.null(Types.bool); } let matcher: (str: string | number) => boolean | nil; if (rightValue.isAny) { // handle LIKE ANY() if (!Array.isArray(pattern)) { throw new QueryError('Unsupported use of ANY()'); } const patterns = pattern.map(x => buildLikeMatcher(x, caseSenit)); matcher = v => patterns.some(x => x(v)); } else { matcher = buildLikeMatcher(pattern, caseSenit); } getter = !not ? a => nullIsh(a) ? null : matcher(a) : a => { if (nullIsh(a)) { return null; } const val = matcher(a); return nullIsh(val) ? null : !val; }; } else { getter = !not ? (a, b) => hasNullish(a, b) ? null : buildLikeMatcher(b, caseSenit)(a) : (a, b) => { if (hasNullish(a, b)) { return null; } const val = buildLikeMatcher(b, caseSenit)(a); return nullIsh(val) ? null : !val; }; } break; default: { const { schema } = buildCtx(); const resolved = schema.resolveOperator(op, leftValue, rightValue); if (!resolved) { throw new QueryError(`operator does not exist: ${leftValue.type.name} ${op} ${rightValue.type.name}`, '42883'); } leftValue = leftValue.cast(resolved.left); rightValue = rightValue.cast(resolved.right); commutative = resolved.commutative; returnType = resolved.returns; getter = resolved.implementation; rejectNils = !resolved.allowNullArguments; impure = !!resolved.impure; break; } }
const hashed = hash(forcehash ?? (commutative ? { op, vals: [leftValue.hash, rightValue.hash].sort() } : { left: leftValue.hash, op, right: rightValue.hash }));
// handle cases like: blah = ANY(stuff) if (leftValue.isAny || rightValue.isAny) { return buildBinaryAny(leftValue, op, rightValue, returnType, getter, hashed); }
return new Evaluator( returnType , null , hashed , [leftValue, rightValue] , (raw, t) => { const leftRaw = leftValue.get(raw, t); const rightRaw = rightValue.get(raw, t); if (rejectNils && (nullIsh(leftRaw) || nullIsh(rightRaw))) { return null; } return getter(leftRaw, rightRaw); }, impure ? { unpure: impure } : undefined);
}
function buildBinaryAny(leftValue: IValue, op: BinaryOperator, rightValue: IValue, returnType: _IType, getter: (a: any, b: any) => boolean, hashed: string) { if (leftValue.isAny && rightValue.isAny) { throw new QueryError('ANY() cannot be compared to ANY()'); } if (returnType !== Types.bool) { throw new QueryError('Invalid ANY() usage'); } return new Evaluator( returnType , null , hashed , [leftValue, rightValue] , leftValue.isAny ? (raw, t) => { const leftRaw = leftValue.get(raw, t); if (nullIsh(leftRaw)) { return null; } if (!Array.isArray(leftRaw)) { throw new QueryError('Invalid ANY() usage: was expacting an array'); } for (const lr of leftRaw) { const rightRaw = rightValue.get(raw, t); if (getter(lr, rightRaw)) { return true; } } return false; } : (raw, t) => { const rightRaw = rightValue.get(raw, t); if (nullIsh(rightRaw)) { return null; } if (!Array.isArray(rightRaw)) { throw new QueryError('Invalid ANY() usage: was expacting an array'); } for (const rr of rightRaw) { const leftRaw = leftValue.get(raw, t); if (getter(leftRaw, rr)) { return true; } } return false; });}

function buildCase(op: ExprCase): IValue { const whens = !op.value ? op.whens : op.whens.map<ExprWhen>(v => ({ when: { type: 'binary', op: '=', left: op.value!, right: v.when, }, value: v.value, })); if (op.else) { whens.push({ when: { type: 'boolean', value: true }, value: op.else, }); }
const whenExprs = whens.map(x => ({ when: buildValue(x.when).cast(Types.bool), then: buildValue(x.value) }));
const valueType = reconciliateTypes(whenExprs.map(x => x.then)); for (const v of whenExprs) { v.then = v.then.cast(valueType); }
return new Evaluator( valueType , null , hash({ when: whenExprs.map(x => ({ when: x.when.hash, then: x.then.hash })) }) , [ ...whenExprs.map(x => x.when), ...whenExprs.map(x => x.then) ] , (raw, t) => { for (const w of whenExprs) { const cond = w.when.get(raw, t); if (cond) { return w.then.get(raw, t); } } return null; });}
function buildMember(op: ExprMember): IValue { const oop = op.op; if (oop !== '->>' && oop !== '->') { throw NotSupported.never(oop); } const onExpr = buildValue(op.operand); if (onExpr.type !== Types.json && onExpr.type !== Types.jsonb) { throw new QueryError(`Cannot use member expression ${op.op} on type ${onExpr.type.primary}`); }
const conv = op.op === '->' ? ((x: any) => x) : ((x: any) => { if (nullIsh(x)) { return null; } if (typeof x === 'string') { return x; } return JSON.stringify(x); });
return new Evaluator( op.op === '->' ? onExpr.type : Types.text() , null , hash([onExpr.hash, op.op, op.member]) , onExpr , typeof op.member === 'string' ? (raw, t) => { const value = onExpr.get(raw, t); if (!value || typeof value !== 'object') { return null; } return conv(value[op.member]); } : (raw, t) => { const value = onExpr.get(raw, t); if (!Array.isArray(value)) { return null; } const i = op.member < 0 ? value.length + (op.member as number) : op.member as number; return conv(value[i]); });}

function buildArrayIndex(op: ExprArrayIndex): IValue { const onExpr = _buildValue(op.array); if (onExpr.type.primary !== DataType.array) { throw new QueryError(`Cannot use [] expression on type ${onExpr.type.primary}`); } const index = _buildValue(op.index).cast(Types.integer); return new Evaluator( (onExpr.type as ArrayType).of , null , hash({ array: onExpr.hash, index: index.hash }) , [onExpr, index] , (raw, t) => { const value = onExpr.get(raw, t); if (!Array.isArray(value)) { return null; } const i = index.get(raw, t); if (typeof i !== 'number' || i <= 0 || i > value.length) { return null; } const ret = value[i - 1]; // 1-base !
if (Array.isArray(ret)) { // ugly hack.. see clean-results.ts (ret as any)[IS_PARTIAL_INDEXING] = true; } return ret; });}

function buildTernary(op: ExprTernary): IValue { const oop = op.op; if (oop !== 'NOT BETWEEN' && oop !== 'BETWEEN') { throw NotSupported.never(oop); } let value = _buildValue(op.value); let hi = _buildValue(op.hi); let lo = _buildValue(op.lo); const type = reconciliateTypes([value, hi, lo]); value = value.cast(type); hi = hi.cast(type); lo = lo.cast(type); const conv = oop === 'NOT BETWEEN' ? (x: boolean) => !x : (x: boolean) => x;
return new Evaluator( Types.bool , null , hash({ value: value.hash, lo: lo.hash, hi: hi.hash }) , [value, hi, lo] , (raw, t) => { const v = value.get(raw, t); if (nullIsh(v)) { return null; } const lov = lo.get(raw, t); if (!nullIsh(lov) && type.lt(v, lov)) { return conv(false); } const hiv = hi.get(raw, t); if (!nullIsh(hiv) && type.gt(v, hiv)) { return conv(false); } if (nullIsh(lov) || nullIsh(hiv)) { return null; } return conv(true); } );}

function buildSelectAsArray(op: SelectStatement): IValue { // todo: handle refs to 'data' in op statement. // ... and refactor this. This is way too hacky to be maintainable // (this wont allow the subrequest to access outer context, for instance) const onData = buildSelect(op); if (onData.columns.length !== 1) { throw new QueryError('subquery must return only one column', '42601'); } return new Evaluator( onData.columns[0].type.asList() , null , Math.random().toString() // must not be indexable => always different hash , null // , onData.columns[0] , (raw, t) => { const ret = []; for (const v of onData.enumerate(t!)) { ret.push(onData.columns[0].get(v, t)); } return ret; }, { forceNotConstant: true });}

function buildExtract(op: ExprExtract): IValue { const from = _buildValue(op.from); function extract(as: _IType, fn: (v: any) => any, result = Types.integer) { const conv = from.cast(as); return new Evaluator( result , null , hash({ extract: from.hash, field: op.field }) , [conv] , (raw, t) => { const got = conv.get(raw, t); if (nullIsh(got)) { return null; } return fn(got); } ) } switch (op.field.name) { case 'millennium': return extract(Types.date, x => Math.ceil(moment.utc(x).year() / 1000)); case 'century': return extract(Types.date, x => Math.ceil(moment.utc(x).year() / 100)); case 'decade': return extract(Types.date, x => Math.floor(moment.utc(x).year() / 10)); case 'day': if (from.canCast(Types.date)) { return extract(Types.date, x => moment.utc(x).date()); } return extract(Types.interval, (x: Interval) => x.days ?? 0); case 'second': if (from.canCast(Types.time)) { return extract(Types.time, x => { const t = parseTime(x); return t.second() + t.milliseconds() / 1000; }, Types.float); } return extract(Types.interval, (x: Interval) => (x.seconds ?? 0) + (x.milliseconds ?? 0) / 1000, Types.float); case 'minute': if (from.canCast(Types.time)) { return extract(Types.time, x => parseTime(x).minute()); } return extract(Types.interval, (x: Interval) => x.minutes ?? 0); case 'milliseconds': if (from.canCast(Types.time)) { return extract(Types.time, x => { const t = parseTime(x); return t.seconds() * 1000 + t.milliseconds(); }); } return extract(Types.interval, (x: Interval) => (x.seconds ?? 0) * 1000 + (x.milliseconds ?? 0), Types.float); case 'month': if (from.canCast(Types.date)) { return extract(Types.date, x => moment.utc(x).month() + 1); } return extract(Types.interval, (x: Interval) => x.months ?? 0); case 'year': if (from.canCast(Types.date)) { return extract(Types.date, x => moment.utc(x).year()); } return extract(Types.interval, (x: Interval) => x.years ?? 0); case 'dow': return extract(Types.date, x => moment.utc(x).day()); case 'isodow': return extract(Types.date, x => { const dow = moment.utc(x).day(); return dow ? dow : 7; }); case 'doy': return extract(Types.date, x => moment.utc(x).dayOfYear()); case 'epoch': if (from.canCast(Types.timestamp())) { return extract(Types.timestamp(), x => moment.utc(x).unix(), Types.float); } return extract(Types.interval, (x: Interval) => intervalToSec(x)); case 'hour': if (from.canCast(Types.timestamp())) { return extract(Types.timestamp(), x => moment.utc(x).hour()); } return extract(Types.interval, (x: Interval) => x.hours ?? 0); case 'isoyear': return extract(Types.date, x => { const d = moment.utc(x); return d.dayOfYear() <= 1 ? d.year() - 1 : d.year(); }); case 'quarter': return extract(Types.date, x => moment.utc(x).quarter()); case 'week': return extract(Types.date, x => moment.utc(x).week()); case 'microseconds': if (from.canCast(Types.time)) { return extract(Types.time, x => { const t = parseTime(x); return t.seconds() * 1000000 + t.milliseconds() * 1000; }); } return extract(Types.interval, (x: Interval) => (x.seconds ?? 0) * 1000000 + (x.milliseconds ?? 0) * 1000); default: throw new NotSupported('Extract type "' + op.field + '"'); }}

function buildOverlay(op: ExprOverlay): IValue { const value = _buildValue(op.value).cast(Types.text()); const placing = _buildValue(op.placing).cast(Types.text()); const from = _buildValue(op.from).cast(Types.integer); const forr = op.for && _buildValue(op.for).cast(Types.integer);
return new Evaluator( Types.text() , null , hash({ overlay: value.hash, placing: placing.hash, from: from.hash, for: forr?.hash }) , forr ? [value, placing, from, forr] : [value, placing, from] , (raw, t) => { const _value = value.get(raw, t) as string; if (nullIsh(_value)) { return null; } const _placing = placing.get(raw, t) as string; if (nullIsh(_placing)) { return null; } const _from = from.get(raw, t) as number; if (nullIsh(_from)) { return null; } const before = sqlSubstring(_value, 0, _from - 1); let after: string | nil; if (forr) { const _for = forr.get(raw, t) as number; if (nullIsh(_for)) { return null; } after = sqlSubstring(_value, _from + _for); } else { after = sqlSubstring(_value, _placing.length + _from); } if (nullIsh(after)) { return null; } return before + _placing + after; });}
function buildSubstring(op: ExprSubstring): IValue { const value = _buildValue(op.value).cast(Types.text()); const vals = [value]; const from = op.from && _buildValue(op.from).cast(Types.integer); const forr = op.for && _buildValue(op.for).cast(Types.integer); if (forr) { vals.push(forr); } if (from) { vals.push(from); }
return new Evaluator( Types.text() , null , hash({ substr: value.hash, from: from?.hash, for: forr?.hash }) , vals , (raw, t) => { const _value = value.get(raw, t) as string; if (nullIsh(_value)) { return null; } let start = 0; let len: number | nil; if (from) { start = from.get(raw, t) as number; if (nullIsh(start)) { return null; } } if (forr) { len = forr.get(raw, t) as number; if (nullIsh(len)) { return null; } } return sqlSubstring(_value, start, len); });}
export function sqlSubstring(value: string, from = 0, len?: number | nil): string | null { if (nullIsh(from) || nullIsh(value)) { return null; } // sql substring is base-1 from--; if (from < 0) { from = 0; } if (!nullIsh(len)) { if (len! < 0) { throw new QueryError('negative substring length not allowed'); } return value.substr(from, len!); } return value.substr(from);}