start adding where clauses
This commit is contained in:
parent
1ea6b9c713
commit
0411e34f10
5 changed files with 203 additions and 13 deletions
|
@ -1,7 +1,6 @@
|
|||
import { Result } from './result.js'
|
||||
import { Iterator, Stream } from './stream.js'
|
||||
import { curry, head, join, of } from './fn.js'
|
||||
import { pipe } from 'bitecs'
|
||||
import { pipe, curry, head, join, of } from './fn.js'
|
||||
|
||||
class ParseError extends Error {
|
||||
constructor(message, state, source) {
|
||||
|
@ -159,6 +158,8 @@ export const map = curry((fn, parser, state) => {
|
|||
return result
|
||||
})
|
||||
|
||||
export const map1 = curry((fn, parser, state) => map(([x]) => fn(x), parser, state))
|
||||
export const always = curry((value, parser, state) => map(() => value, parser, state))
|
||||
|
||||
export const anyChar = state => {
|
||||
const ch = next(state)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { join } from '../fn.js'
|
|||
import { alpha, alphanumeric, any, char, digit, list, many, map, maybe, noCaseString, separated, seq, skip, string, until } from '../parser.js'
|
||||
import { Alias, Identifier, Literal, ObjectPath, Property } from './types.js'
|
||||
|
||||
export const Symbol = Object.freeze({
|
||||
export const Symbols = Object.freeze({
|
||||
Bracket: Object.freeze({
|
||||
Angle: Object.freeze({
|
||||
Left: char('<'),
|
||||
|
@ -23,6 +23,7 @@ export const Symbol = Object.freeze({
|
|||
}),
|
||||
Colon: char(':'),
|
||||
Comma: char(','),
|
||||
Equal: char('='),
|
||||
Hyphen: char('-'),
|
||||
Newline: char('\n'),
|
||||
Tab: char('\t'),
|
||||
|
@ -34,20 +35,20 @@ export const Symbol = Object.freeze({
|
|||
|
||||
export const word = value => seq(skip(ws), value, skip(ws))
|
||||
export const collect = parser => map(join(''), parser)
|
||||
export const quoted = collect(seq(skip(Symbol.Quote), until(Symbol.Quote), skip(Symbol.Quote)))
|
||||
export const quoted = collect(seq(skip(Symbols.Quote), until(Symbols.Quote), skip(Symbols.Quote)))
|
||||
export const number = seq(digit, many(digit))
|
||||
export const signed = seq(maybe(any(Symbol.Plus, Symbol.Hyphen)), number)
|
||||
export const ws = skip(many(any(Symbol.Newline, Symbol.Space, Symbol.Tab)))
|
||||
export const signed = seq(maybe(any(Symbols.Plus, Symbols.Hyphen)), number)
|
||||
export const ws = skip(many(any(Symbols.Newline, Symbols.Space, Symbols.Tab)))
|
||||
export const identifier = map(([x]) => new Identifier(x), collect(seq(alpha, many(alphanumeric))))
|
||||
|
||||
const float = map(([n]) => parseFloat(n, 10), collect(seq(signed, Symbol.Period, number)))
|
||||
const float = map(([n]) => parseFloat(n, 10), collect(seq(signed, Symbols.Period, number)))
|
||||
const integer = map(([n]) => parseInt(n, 10), collect(signed))
|
||||
const str = quoted
|
||||
|
||||
const toBoolean = v => v === 'false' ? false : true
|
||||
const boolean = map(([v]) => toBoolean(v), any(string('true'), string('false')))
|
||||
|
||||
const property = map(([x]) => new Property(x), seq(skip(Symbol.Period), identifier))
|
||||
const property = map(([x]) => new Property(x), seq(skip(Symbols.Period), identifier))
|
||||
const accessor = map(x => new ObjectPath(...x), seq(identifier, property, many(property)))
|
||||
|
||||
export const literal = map(([x]) => new Literal(x), any(float, integer, str, boolean))
|
||||
|
|
|
@ -11,8 +11,8 @@ class Value {
|
|||
return this.#value.toString()
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#value?.value ?? this.#value
|
||||
valueOf() {
|
||||
return this.#value?.valueOf() ?? this.#value
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
|
@ -86,6 +86,69 @@ export class Literal {
|
|||
}
|
||||
}
|
||||
|
||||
export const None = new Literal(null)
|
||||
|
||||
export class Operator {
|
||||
#fn
|
||||
|
||||
constructor(fn, length = fn.length) {
|
||||
this.#fn = fn
|
||||
this.length = length
|
||||
}
|
||||
|
||||
#pop(n, stack) {
|
||||
let result = []
|
||||
|
||||
for(let i = 0; i < n; i++) {
|
||||
result.push(stack.pop())
|
||||
}
|
||||
|
||||
result.reverse()
|
||||
return result
|
||||
}
|
||||
|
||||
apply(stack) {
|
||||
const args = this.#pop(this.length, stack)
|
||||
return this.#fn.apply(undefined, args)
|
||||
}
|
||||
}
|
||||
|
||||
export class UnaryExpression {
|
||||
constructor(op, value) {
|
||||
this.op = op
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
export class BinaryExpression {
|
||||
constructor(left, op, right) {
|
||||
this.left = left
|
||||
this.op = op
|
||||
this.right = right
|
||||
}
|
||||
}
|
||||
|
||||
export const Is = new Operator((x, y) => typeof x === y)
|
||||
export const Not = new Operator(x => !x)
|
||||
export const And = new Operator((x, y) => x && y)
|
||||
export const Or = new Operator((x, y) => x || y)
|
||||
export const Xor = new Operator((x, y) => x != y)
|
||||
export const GreaterThan = new Operator((x, y) => x > y)
|
||||
export const GreaterThanEqual = new Operator((x, y) => x >= y)
|
||||
export const LesserThan = new Operator((x, y) => x < y)
|
||||
export const LesserThanEqual = new Operator((x, y) => x <= y)
|
||||
export const Equal = new Operator((x, y) => x === y)
|
||||
|
||||
export class Filter {
|
||||
constructor(...args) {
|
||||
this.values = this.#shufflingYard(args)
|
||||
}
|
||||
|
||||
#shufflingYard(args) {
|
||||
// do the thing
|
||||
return args
|
||||
}
|
||||
}
|
||||
|
||||
export class Alias extends Identifier {
|
||||
constructor(value, alias) {
|
||||
|
@ -101,7 +164,7 @@ export class ObjectPath extends Identifier {
|
|||
}
|
||||
|
||||
toString() {
|
||||
return `${this.value}.${this.path.join('.')}`
|
||||
return [this.valueOf()].concat(path).join('.')
|
||||
}
|
||||
|
||||
from(component) {
|
||||
|
@ -231,11 +294,10 @@ class ReturnValue {
|
|||
}
|
||||
|
||||
withEntity(entity) {
|
||||
return assocPath(this.#value.toString(), this.#getter(entity), {})
|
||||
return assocPath(this.#value.toString(), this.#getter(entity), {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class KeyValuePair {
|
||||
constructor(key, value) {
|
||||
this.key = key
|
||||
|
|
36
src/query-parser/where.js
Normal file
36
src/query-parser/where.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { construct } from '../fn.js'
|
||||
import { always, any, char, many1, map, map1, noCaseString, seq, skip, string } from '../parser.js'
|
||||
import { Symbols, value, word, ws } from './common.js'
|
||||
import { And, Equal, Filter, GreaterThan, GreaterThanEqual, LesserThan, LesserThanEqual, Literal, None, Not, Or, Xor } from './types.js'
|
||||
|
||||
const { Left, Right } = Symbols.Bracket.Round
|
||||
const lparen = map1(construct(Literal), Left)
|
||||
const rparen = map1(construct(Literal), Right)
|
||||
|
||||
// type and existence
|
||||
export const nil = always(None, noCaseString('null'))
|
||||
export const is = noCaseString('is')
|
||||
|
||||
// binary
|
||||
export const gt = always(GreaterThan, char('>'))
|
||||
export const gte = always(GreaterThanEqual, string('>='))
|
||||
export const lt = always(LesserThan, char('<'))
|
||||
export const lte = always(LesserThanEqual, string('<='))
|
||||
export const eq = always(Equal, char('='))
|
||||
|
||||
export const and = always(And, noCaseString('and'))
|
||||
export const or = always(Or, noCaseString('or'))
|
||||
export const xor = always(Xor, noCaseString('xor'))
|
||||
|
||||
export const compare = any(gte, lte, gt, lt, eq)
|
||||
export const logic = any(and, or, xor)
|
||||
|
||||
// unary
|
||||
export const not = always(Not, noCaseString('not'))
|
||||
export const unary = any(not)
|
||||
|
||||
export const expr = any(lparen, rparen, compare, logic, unary, value)
|
||||
export const params = map(construct(Filter), many1(word(expr)))
|
||||
|
||||
const keyword = noCaseString('where')
|
||||
export const whereClause = seq(skip(keyword), ws, params)
|
90
tests/query-parser/where.test.js
Normal file
90
tests/query-parser/where.test.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { describe, it } from 'node:test'
|
||||
import assert from '../assert.js'
|
||||
import { and, eq, expr, gt, gte, lt, lte, not, or, params, whereClause, xor } from '../../src/query-parser/where.js'
|
||||
import { And, Equal, Filter, GreaterThan, GreaterThanEqual, LesserThan, LesserThanEqual, Literal, Or } from '../../src/query-parser/types.js'
|
||||
|
||||
const l = x => new Literal(x)
|
||||
describe('where operators', () => {
|
||||
it('or', () => {
|
||||
assert.parseOk(or, 'OR', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([true, true]), true)
|
||||
assert.strictEqual(actual.apply([false, true]), true)
|
||||
assert.strictEqual(actual.apply([true, false]), true)
|
||||
assert.strictEqual(actual.apply([false, false]), false)
|
||||
})
|
||||
})
|
||||
|
||||
it('and', () => {
|
||||
assert.parseOk(and, 'AND', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([true, true]), true)
|
||||
assert.strictEqual(actual.apply([false, true]), false)
|
||||
assert.strictEqual(actual.apply([true, false]), false)
|
||||
assert.strictEqual(actual.apply([false, false]), false)
|
||||
})
|
||||
})
|
||||
|
||||
it('xor', () => {
|
||||
assert.parseOk(xor, 'XOR', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([true, true]), false)
|
||||
assert.strictEqual(actual.apply([false, true]), true)
|
||||
assert.strictEqual(actual.apply([true, false]), true)
|
||||
assert.strictEqual(actual.apply([false, false]), false)
|
||||
})
|
||||
})
|
||||
|
||||
it('not', () => {
|
||||
assert.parseOk(not, 'NOT', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([true]), false)
|
||||
assert.strictEqual(actual.apply([false]), true)
|
||||
})
|
||||
})
|
||||
|
||||
it('gt', () => {
|
||||
assert.parseOk(gt, '>', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([1, 2]), false)
|
||||
assert.strictEqual(actual.apply([2, 1]), true)
|
||||
assert.strictEqual(actual.apply([1, 1]), false)
|
||||
})
|
||||
})
|
||||
|
||||
it('gte', () => {
|
||||
assert.parseOk(gte, '>=', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([1, 2]), false)
|
||||
assert.strictEqual(actual.apply([2, 1]), true)
|
||||
assert.strictEqual(actual.apply([1, 1]), true)
|
||||
})
|
||||
})
|
||||
|
||||
it('lt', () => {
|
||||
assert.parseOk(lt, '<', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([1, 2]), true)
|
||||
assert.strictEqual(actual.apply([2, 1]), false)
|
||||
assert.strictEqual(actual.apply([1, 1]), false)
|
||||
})
|
||||
})
|
||||
|
||||
it('lte', () => {
|
||||
assert.parseOk(lte, '<=', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([1, 2]), true)
|
||||
assert.strictEqual(actual.apply([2, 1]), false)
|
||||
assert.strictEqual(actual.apply([1, 1]), true)
|
||||
})
|
||||
})
|
||||
|
||||
it('eq', () => {
|
||||
assert.parseOk(eq, '=', ([actual]) => {
|
||||
assert.strictEqual(actual.apply([1, 2]), false)
|
||||
assert.strictEqual(actual.apply([2, 1]), false)
|
||||
assert.strictEqual(actual.apply([1, 1]), true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('WHERE keyword', () => {
|
||||
it('handles complex filters', () => {
|
||||
assert.parseOk(whereClause, 'WHERE 1.1 > 5 AND (1 = 500 OR 2 > 3) OR 5 <= 2', ([actual]) => {
|
||||
const expected = new Filter(l(1.1), GreaterThan, l(5), And, l('('), l(1), Equal, l(500), Or, l(2), GreaterThan, l(3), l(')'), Or, l(5), LesserThanEqual, l(2))
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue