start adding where clauses

This commit is contained in:
Rowan 2024-11-27 03:23:00 -06:00
parent 1ea6b9c713
commit 0411e34f10
5 changed files with 203 additions and 13 deletions

View file

@ -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)

View file

@ -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))

View file

@ -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
View 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)

View 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)
})
})
})