add where clauses and aliasing
This commit is contained in:
parent
0411e34f10
commit
d9ebc1e875
15 changed files with 393 additions and 216 deletions
|
@ -5,7 +5,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "node --test ./tests/**/*.test.js"
|
||||
"test": "node --test ./tests/*.test.js ./tests/**/*.test.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
|
@ -209,8 +209,23 @@ export const maybe = curry((parser, state) => {
|
|||
return result.isOk() ? result : succeed([], state)
|
||||
})
|
||||
|
||||
export const manyUntil = curry((failParser, parser, state) => {
|
||||
let result = Result.Ok(state)
|
||||
|
||||
while (failParser(clone(result.unwrap())).isErr()) {
|
||||
const res = parser(clone(result.unwrap()))
|
||||
if (res.isOk()) {
|
||||
result = res
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export const eof = state => {
|
||||
return state[1].done() ? succeed([], state) : fail('eof did not match', state)
|
||||
return state[1].done() ? succeed([], state) : fail('not at end of file', state)
|
||||
}
|
||||
|
||||
export const parse = curry((parser, input) => pipe(ParserState, parser)(input))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { join, values } from '../fn.js'
|
||||
import { alpha, alphanumeric, any, char, digit, many, map, maybe, noCaseString, seq, skip, string, until } from '../parser.js'
|
||||
import { Alias, Identifier, Literal, ObjectPath, Property } from './types.js'
|
||||
|
||||
export const Symbols = Object.freeze({
|
||||
|
@ -33,7 +33,14 @@ export const Symbols = Object.freeze({
|
|||
Quote: char('"')
|
||||
})
|
||||
|
||||
export const word = value => seq(skip(ws), value, skip(ws))
|
||||
export const Keyword = Object.freeze({
|
||||
Use: noCaseString('use'),
|
||||
Match: noCaseString('match'),
|
||||
Where: noCaseString('where'),
|
||||
Return: noCaseString('return')
|
||||
})
|
||||
|
||||
export const word = value => seq(ws, value, ws)
|
||||
export const collect = parser => map(join(''), parser)
|
||||
export const quoted = collect(seq(skip(Symbols.Quote), until(Symbols.Quote), skip(Symbols.Quote)))
|
||||
export const number = seq(digit, many(digit))
|
||||
|
@ -57,4 +64,5 @@ export const baseValue = any(literal, accessor, identifier)
|
|||
const as = noCaseString('as')
|
||||
export const alias = map(([value, name]) => new Alias(value, name), seq(baseValue, word(skip(as)), identifier))
|
||||
export const value = any(alias, baseValue)
|
||||
export const keyword = any(...values(Keyword))
|
||||
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
import { construct, head, ifElse, is, pipe, prepend } from '../fn.js'
|
||||
import { filter, find, is } from '../fn.js'
|
||||
import { map, maybe, seq } from '../parser.js'
|
||||
import { word } from './common.js'
|
||||
import { matchClause } from './match.js'
|
||||
import { returnClause } from './return.js'
|
||||
import { Query, SelectedGraph } from './types.js'
|
||||
import { useClause } from './use.js'
|
||||
import { matchClause } from './match.js'
|
||||
import { whereClause } from './where.js'
|
||||
import { returnClause } from './return.js'
|
||||
import { Filter, Match, Query, ReturnValues, SelectedGraph } from './types.js'
|
||||
|
||||
const hasUseClause = pipe(
|
||||
head,
|
||||
is(SelectedGraph)
|
||||
)
|
||||
const use = find(is(SelectedGraph))
|
||||
const match = filter(is(Match))
|
||||
const where = filter(is(Filter))
|
||||
const rv = find(is(ReturnValues))
|
||||
|
||||
const constructQuery = construct(Query)
|
||||
const noUseQuery = pipe(prepend(undefined), constructQuery)
|
||||
|
||||
const collect = ifElse(
|
||||
hasUseClause,
|
||||
constructQuery,
|
||||
noUseQuery
|
||||
)
|
||||
const collect = clauses => {
|
||||
return new Query(
|
||||
use(clauses),
|
||||
match(clauses),
|
||||
where(clauses),
|
||||
rv(clauses)
|
||||
)
|
||||
}
|
||||
|
||||
export const query = map(
|
||||
collect,
|
||||
seq(
|
||||
maybe(useClause),
|
||||
maybe(word(useClause)),
|
||||
word(matchClause),
|
||||
maybe(word(whereClause)),
|
||||
word(returnClause)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { identifier, literal, Symbol, ws } from './common.js'
|
||||
import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component } from './types.js'
|
||||
import { construct, curry, is } from '../fn.js'
|
||||
import { identifier, Keyword, literal, Symbols, ws } from './common.js'
|
||||
import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component, Match } from './types.js'
|
||||
import { construct, curry, head, is } from '../fn.js'
|
||||
import { many, maybe, map, seq, skip, between, noCaseString, separated, list, any } from '../parser.js'
|
||||
|
||||
const { Bracket, Colon, Comma, Hyphen } = Symbol
|
||||
const { Bracket, Colon, Comma, Hyphen } = Symbols
|
||||
|
||||
const name = map(
|
||||
construct(Name),
|
||||
|
@ -67,7 +67,7 @@ const makeRelationship = args => {
|
|||
const len = args.length - 1
|
||||
|
||||
if (len <= 0) {
|
||||
return args
|
||||
return new Match(head(args))
|
||||
}
|
||||
|
||||
let right = args[len]
|
||||
|
@ -78,14 +78,13 @@ const makeRelationship = args => {
|
|||
right = new Relationship(left, edge, right)
|
||||
}
|
||||
|
||||
return right
|
||||
return new Match(right)
|
||||
}
|
||||
|
||||
const keyword = noCaseString('match')
|
||||
const params = map(
|
||||
makeRelationship,
|
||||
seq(node, many(relationship))
|
||||
)
|
||||
|
||||
export const matchClause = seq(skip(keyword), ws, params)
|
||||
export const matchClause = seq(skip(Keyword.Match), ws, params)
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { ReturnValues } from './types.js'
|
||||
import { list, map, noCaseString, seq, skip } from '../parser.js'
|
||||
import { value, Symbol, ws } from './common.js'
|
||||
|
||||
const keyword = noCaseString('return')
|
||||
import { list, map, seq, skip } from '../parser.js'
|
||||
import { value, Symbols, ws, Keyword } from './common.js'
|
||||
|
||||
const params = map(
|
||||
x => new ReturnValues(...x),
|
||||
seq(list(seq(ws, Symbol.Comma, ws), value))
|
||||
seq(list(seq(ws, Symbols.Comma, ws), value))
|
||||
)
|
||||
|
||||
export const returnClause = seq(skip(keyword), ws, params)
|
||||
export const returnClause = seq(skip(Keyword.Return), ws, params)
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { assocPath, is } from '../fn.js'
|
||||
import { assocPath, curry, identity, is, last, mergeRightDeep, path } from '../fn.js'
|
||||
import { Stream } from '../stream.js'
|
||||
|
||||
class Value {
|
||||
#value
|
||||
value
|
||||
|
||||
constructor(value) {
|
||||
this.#value = value
|
||||
this.value = value
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.#value.toString()
|
||||
return this.value.toString()
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.#value?.valueOf() ?? this.#value
|
||||
return this.value?.valueOf() ?? this.value
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
|
@ -20,9 +21,9 @@ class Value {
|
|||
return false
|
||||
}
|
||||
|
||||
const value = this.value
|
||||
const value = this.valueOf()
|
||||
return value === other
|
||||
|| value === other.value
|
||||
|| value === other.valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,24 +32,13 @@ export class Identifier extends Value {
|
|||
super(value)
|
||||
}
|
||||
|
||||
from(component) {
|
||||
return component
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectIdentifier extends Identifier {
|
||||
constructor(value) {
|
||||
super(value)
|
||||
from(node) {
|
||||
return node[this.valueOf()]
|
||||
}
|
||||
|
||||
from(component) {
|
||||
return new Proxy(component, {
|
||||
get(target, entity, _receiver) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(target).map(([k, v]) => [k, v[entity]])
|
||||
)
|
||||
}
|
||||
})
|
||||
pluck(node) {
|
||||
const key = this.valueOf()
|
||||
return { [key]: node[key] }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,8 +47,8 @@ export class Property extends Identifier {
|
|||
super(value)
|
||||
}
|
||||
|
||||
from(component) {
|
||||
return component[this.value]
|
||||
pluck(node) {
|
||||
return node[this.valueOf()]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,69 +74,166 @@ export class Literal {
|
|||
constructor(value) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
from(node) {
|
||||
return this.valueOf()
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.value.valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
export const None = new Literal(null)
|
||||
|
||||
export class Operator {
|
||||
export class Function {
|
||||
#fn
|
||||
|
||||
constructor(fn, length = fn.length) {
|
||||
constructor(name, fn, length = fn.length) {
|
||||
this.name = name
|
||||
this.#fn = fn
|
||||
this.length = length
|
||||
}
|
||||
|
||||
#pop(n, stack) {
|
||||
let result = []
|
||||
#pop(stack) {
|
||||
const result = []
|
||||
const len = stack.length - 1
|
||||
const n = Math.min(this.length, len)
|
||||
|
||||
for(let i = 0; i < n; i++) {
|
||||
for(let i = len; i >= len - n; i--) {
|
||||
result.push(stack.pop())
|
||||
}
|
||||
|
||||
result.reverse()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
apply(stack) {
|
||||
const args = this.#pop(this.length, stack)
|
||||
const args = this.#pop(stack)
|
||||
return this.#fn.apply(undefined, args)
|
||||
}
|
||||
}
|
||||
|
||||
export class UnaryExpression {
|
||||
constructor(op, value) {
|
||||
this.op = op
|
||||
this.value = value
|
||||
const Associativity = Object.freeze({
|
||||
Left: 0,
|
||||
Right: 1
|
||||
})
|
||||
|
||||
export class Operator extends Function {
|
||||
constructor(name, fn, priority, associativity = Associativity.Left, length = fn.length) {
|
||||
super(name, fn, length)
|
||||
this.priority = priority
|
||||
this.associativity = associativity
|
||||
}
|
||||
|
||||
compare(other) {
|
||||
if (other.priority > this.priority) {
|
||||
return 1
|
||||
} else if (other.priority === this.priority) {
|
||||
if (this.associativity === Associativity.Left) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 const Is = new Operator('IS', (x, y) => typeof x === y, 2, Associativity.Right)
|
||||
export const Not = new Operator('NOT', x => !x, 2, Associativity.Right)
|
||||
export const And = new Operator('AND', (x, y) => x && y, 11)
|
||||
export const Or = new Operator('OR', (x, y) => x || y, 12)
|
||||
export const Xor = new Operator('XOR', (x, y) => x != y, 14)
|
||||
export const GreaterThan = new Operator('GT', (x, y) => x > y, 6)
|
||||
export const GreaterThanEqual = new Operator('GTE', (x, y) => x >= y, 6)
|
||||
export const LesserThan = new Operator('LT', (x, y) => x < y, 6)
|
||||
export const LesserThanEqual = new Operator('LTE', (x, y) => x <= y, 6)
|
||||
export const Equal = new Operator('EQ', (x, y) => x === y, 7)
|
||||
|
||||
export class Filter {
|
||||
constructor(...args) {
|
||||
this.values = this.#shufflingYard(args)
|
||||
this.values = this.#shuntingYard(args)
|
||||
}
|
||||
|
||||
#shufflingYard(args) {
|
||||
// do the thing
|
||||
return args
|
||||
#takeWhile(fn, stack) {
|
||||
const result = []
|
||||
let val = last(stack)
|
||||
|
||||
while (stack.length > 0 && fn(val)) {
|
||||
result.push(stack.pop())
|
||||
val = last(stack)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
#takeUntil(fn, stack) {
|
||||
return this.#takeWhile(x => !fn(x), stack)
|
||||
}
|
||||
|
||||
#shuntingYard(args) {
|
||||
const stream = new Stream(args)
|
||||
const output = []
|
||||
const operators = []
|
||||
const valueOf = curry((x, y) => y.valueOf() === x)
|
||||
const leftParen = valueOf('(')
|
||||
const comma = valueOf(',')
|
||||
const rightParen = valueOf(')')
|
||||
|
||||
while (!stream.done()) {
|
||||
const next = stream.next()
|
||||
if (is(Function, next)) {
|
||||
if (is(Operator, next)) {
|
||||
const ops = this.#takeUntil(op => leftParen(op) && next.compare(op) <= 0, operators)
|
||||
output.push.apply(output, ops)
|
||||
}
|
||||
operators.push(next)
|
||||
} else if (comma(next)) {
|
||||
output.push.apply(output, this.#takeUntil(leftParen, operators))
|
||||
} else if (leftParen(next)) {
|
||||
operators.push(next)
|
||||
} else if (rightParen(next)) {
|
||||
const ops = this.#takeUntil(leftParen, operators)
|
||||
output.push.apply(output, ops)
|
||||
if (!leftParen(operators.pop())) {
|
||||
throw new SyntaxError('mismatched parenthesis')
|
||||
}
|
||||
if (is(Function, last(operators))) {
|
||||
output.push(operators.pop())
|
||||
}
|
||||
} else {
|
||||
output.push(next)
|
||||
}
|
||||
}
|
||||
|
||||
const rest = this.#takeUntil(leftParen, operators)
|
||||
output.push.apply(output, rest)
|
||||
|
||||
if (operators.length > 0) {
|
||||
throw new SyntaxError('mismatched parenthesis')
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
passes(nodes) {
|
||||
return nodes.every(node => {
|
||||
const values = this.values.map(v => v.from ? v.from(node.values) : v)
|
||||
const stream = new Stream(values)
|
||||
const stack = []
|
||||
while (!stream.done()) {
|
||||
const next = stream.next()
|
||||
if (is(Operator, next)) {
|
||||
const result = next.apply(stack)
|
||||
stack.unshift(result)
|
||||
} else {
|
||||
stack.push(next)
|
||||
}
|
||||
}
|
||||
|
||||
return stack.every(identity)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,6 +242,10 @@ export class Alias extends Identifier {
|
|||
super(value)
|
||||
this.alias = alias
|
||||
}
|
||||
|
||||
pluck(node) {
|
||||
return { [this.alias.valueOf()]: this.value.from(node) }
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectPath extends Identifier {
|
||||
|
@ -164,11 +255,16 @@ export class ObjectPath extends Identifier {
|
|||
}
|
||||
|
||||
toString() {
|
||||
return [this.valueOf()].concat(path).join('.')
|
||||
return [this.valueOf()].concat(this.path).join('.')
|
||||
}
|
||||
|
||||
from(component) {
|
||||
return this.path.reduce((acc, v) => v.from(acc), component)
|
||||
from(node) {
|
||||
return path(this.toString(), node)
|
||||
}
|
||||
|
||||
pluck(node) {
|
||||
const values = this.path.reduce((acc, v) => ({ [v.valueOf()]: v.pluck(acc) }), node[this.valueOf()])
|
||||
return { [this.valueOf()]: values }
|
||||
}
|
||||
|
||||
equals(value) {
|
||||
|
@ -236,65 +332,33 @@ export class Relationship {
|
|||
}
|
||||
}
|
||||
|
||||
const isExactly = (Type, value) => Object.getPrototypeOf(value) === Type.prototype
|
||||
|
||||
export class ReturnValues {
|
||||
constructor(...args) {
|
||||
this.values = args.map(x => isExactly(Identifier, x) ? new ObjectIdentifier(x) : x)
|
||||
this.values = args
|
||||
}
|
||||
|
||||
#isTag(type) {
|
||||
type && Object.getOwnPropertySymbols(type).find(
|
||||
(s) => s.description === 'tagStore'
|
||||
) || false
|
||||
from(values) {
|
||||
// TODO: pass with entries
|
||||
const returns = Object.keys(values).map(key => this.#pluck(values, key))
|
||||
return Object.assign({}, ...returns)
|
||||
}
|
||||
|
||||
#forComponentType(value, type) {
|
||||
if (this.#isTag(type)) {
|
||||
return {}
|
||||
} else if (typeof type === 'function') {
|
||||
return type
|
||||
} else {
|
||||
return entity => value.from(type)[entity]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: adjust froms to accent entity, return data with built up objects as necessary
|
||||
from(components) {
|
||||
const values = components.map(cmp => ({ ...cmp, rv: this.findName(cmp.name) }))
|
||||
.map(({ type, rv }) => new ReturnValue(rv, this.#forComponentType(rv, type)))
|
||||
|
||||
return entity => values.reduce((acc, rv) => Object.assign(acc, rv.withEntity(entity)), {})
|
||||
#pluck(values, name) {
|
||||
return this.findName(name)
|
||||
.map(value => value.pluck(values))
|
||||
.reduce(mergeRightDeep, {})
|
||||
}
|
||||
|
||||
find(fn) {
|
||||
return this.values.find(fn)
|
||||
}
|
||||
|
||||
findName(name) {
|
||||
return this.find(value => value.equals(name))
|
||||
}
|
||||
|
||||
filter(fn) {
|
||||
return this.values.filter(fn)
|
||||
}
|
||||
|
||||
includes(value) {
|
||||
return this.find(x => x.equals(value))
|
||||
}
|
||||
}
|
||||
|
||||
class ReturnValue {
|
||||
#value
|
||||
#getter
|
||||
|
||||
constructor(value, getter) {
|
||||
this.#value = value
|
||||
this.#getter = getter
|
||||
}
|
||||
|
||||
withEntity(entity) {
|
||||
return assocPath(this.#value.toString(), this.#getter(entity), {})
|
||||
findName(value) {
|
||||
return this.filter(x => x.equals(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,10 +369,21 @@ export class KeyValuePair {
|
|||
}
|
||||
}
|
||||
|
||||
export class Match {
|
||||
constructor(value) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
export class Query {
|
||||
constructor(use, match, returnValues) {
|
||||
constructor(use, matches, filters, returnValues) {
|
||||
this.use = use
|
||||
this.match = match
|
||||
this.matches = matches
|
||||
this.filters = filters
|
||||
this.returnValues = returnValues
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { identifier, ws } from './common.js'
|
||||
import { identifier, Keyword, ws } from './common.js'
|
||||
import { map, noCaseString, seq, skip } from '../parser.js'
|
||||
import { SelectedGraph } from './types.js'
|
||||
|
||||
const keyword = noCaseString('use')
|
||||
export const useKeyword = noCaseString('use')
|
||||
|
||||
export const useClause = map(
|
||||
([graph]) => new SelectedGraph(graph),
|
||||
seq(skip(keyword), ws, identifier)
|
||||
seq(skip(Keyword.Use), ws, identifier)
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 { always, any, char, many1, manyUntil, map, map1, noCaseString, seq, skip, string } from '../parser.js'
|
||||
import { keyword, Keyword, 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
|
||||
|
@ -30,7 +30,6 @@ 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)))
|
||||
export const params = map(construct(Filter), manyUntil(keyword, word(expr)))
|
||||
|
||||
const keyword = noCaseString('where')
|
||||
export const whereClause = seq(skip(keyword), ws, params)
|
||||
export const whereClause = seq(skip(Keyword.Where), ws, params)
|
||||
|
|
74
src/query.js
74
src/query.js
|
@ -1,32 +1,31 @@
|
|||
import { defineQuery, hasComponent } from 'bitecs'
|
||||
import { query as q } from './query-parser/index.js'
|
||||
import { parseAll } from './parser.js'
|
||||
import { curry, of, is, map, path, pipe, always, assocPath, prop, identity } from './fn.js'
|
||||
import { curry, of, is, map, always, assocPath, prop, identity, mergeRightDeep, reduce } from './fn.js'
|
||||
import { Relationship } from './query-parser/types.js'
|
||||
|
||||
const nodeComponent = component => node => {
|
||||
const label = node.label?.value
|
||||
const name = node.name?.value
|
||||
const type = label && component[node.label.value] || identity
|
||||
const label = node.label?.valueOf()
|
||||
const name = node.name?.valueOf()
|
||||
const type = label && component[label] || identity
|
||||
|
||||
return { name, label, type }
|
||||
}
|
||||
|
||||
const notEntity = ({ label }) => label != null && label !== 'Entity'
|
||||
const notEntity = ({ label }) => label != null && label.valueOf() !== 'Entity'
|
||||
|
||||
const prepareQuery = (query, component) => {
|
||||
const { match, returnValues } = query
|
||||
const prepareQuery = (match, component) => {
|
||||
const relationship = is(Relationship, match)
|
||||
|
||||
if (relationship) {
|
||||
const { from: f, to: t, edge: e } = match
|
||||
const [from, to, edge] = map(q => prepareQuery({ match: q, returnValues }, component), [f, t, e])
|
||||
const [from, to, edge] = map(match => prepareQuery(match, component), [f, t, e])
|
||||
|
||||
return { from, to, edge, relationship, schema: query }
|
||||
} else {
|
||||
const components = match.components.map(nodeComponent(component))
|
||||
const types = components.filter(notEntity).map(prop('type'))
|
||||
return { components, relationship, getReturns: returnValues.from(components), query: defineQuery(types), schema: query }
|
||||
return { components, relationship, query: defineQuery(types), schema: match }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,27 +94,47 @@ const executeQuery = curry((query, world) => {
|
|||
}
|
||||
})
|
||||
|
||||
const isTag = (type) => (
|
||||
type && Object.getOwnPropertySymbols(type).find(
|
||||
(s) => s.description === 'tagStore'
|
||||
) || false
|
||||
)
|
||||
|
||||
const resolveNode = curry((node, obj) => {
|
||||
const { entity, query: { getReturns } } = node
|
||||
return { ...obj, ...getReturns(entity) }
|
||||
const getValue = curry((type, entity) => {
|
||||
if (isTag(type)) {
|
||||
return {}
|
||||
} else if (typeof type === 'function') {
|
||||
return type(entity)
|
||||
} else {
|
||||
return Object.fromEntries(
|
||||
Object.entries(type).map(([k, v]) => [k, v[entity]])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const resolveNode = node => {
|
||||
const values = node.query.components.map(({ name, type }) => ({ [name]: getValue(type, node.entity) }))
|
||||
return {
|
||||
...node,
|
||||
values: Object.assign({}, ...values)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveRelationship = edges => {
|
||||
const { entity, query } = edges
|
||||
const Edge = query.edge.components[0].type
|
||||
|
||||
const to = query.to?.from ?? query.to
|
||||
return pipe(
|
||||
return [
|
||||
resolveNode({ entity: Edge.from[entity], query: query.from }),
|
||||
resolveNode({ entity, query: query.edge }),
|
||||
resolveNode({ entity: Edge.to[entity], query: to }),
|
||||
)({})
|
||||
]
|
||||
}
|
||||
|
||||
const resolveReturns = query => {
|
||||
const resolveValues = query => {
|
||||
return query.map(edges => {
|
||||
const returns = edges.map(edge => {
|
||||
const values = edges.map(edge => {
|
||||
if (edge.query.relationship) {
|
||||
return resolveRelationship(edge)
|
||||
} else {
|
||||
|
@ -123,17 +142,28 @@ const resolveReturns = query => {
|
|||
}
|
||||
})
|
||||
|
||||
return Object.assign({}, ...returns)
|
||||
return values.flat()
|
||||
})
|
||||
}
|
||||
|
||||
const filterValues = curry((filters, values) => {
|
||||
return values.filter(v => filters.every(f => f.passes(v)))
|
||||
})
|
||||
|
||||
const resolveReturns = curry((returnValues, values) => {
|
||||
return values.map(x => x.map(y => returnValues.from(y.values)))
|
||||
})
|
||||
|
||||
export const query = curry((input, { world, component }) => {
|
||||
return parseAll(q, input).map(({ use, ...rest }) => {
|
||||
const useWorld = world[use?.value ?? 'default']
|
||||
const preparedQuery = prepareQuery(rest, component)
|
||||
return parseAll(q, input).map(({ use, matches, filters, returnValues, ...rest }) => {
|
||||
const useWorld = world[use?.valueOf() ?? 'default']
|
||||
const match = matches[0].valueOf()
|
||||
const preparedQuery = prepareQuery(match, component)
|
||||
const results = executeQuery(preparedQuery, useWorld)
|
||||
const returns = resolveReturns(results)
|
||||
return returns
|
||||
const values = resolveValues(results)
|
||||
const filtered = filterValues(filters, values)
|
||||
const returns = resolveReturns(returnValues, filtered)
|
||||
return map(reduce(mergeRightDeep, {}), returns)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -33,13 +33,16 @@ export class Iterator {
|
|||
}
|
||||
|
||||
done() {
|
||||
return !!this.peek()
|
||||
return this.peek() == null
|
||||
}
|
||||
|
||||
take(n) {
|
||||
let accumulator = []
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
if(this.done()) {
|
||||
break
|
||||
}
|
||||
accumulator.push(this.next())
|
||||
}
|
||||
|
||||
|
@ -54,10 +57,10 @@ export class Iterator {
|
|||
|
||||
class ArrayIterator extends Iterator {
|
||||
static { Iterator.handledTypes.set(Array, ArrayIterator) }
|
||||
#index = -1
|
||||
#index = 0
|
||||
#value
|
||||
|
||||
constructor(value, index = -1) {
|
||||
constructor(value, index = 0) {
|
||||
super()
|
||||
|
||||
if (!Array.isArray) {
|
||||
|
@ -70,8 +73,9 @@ class ArrayIterator extends Iterator {
|
|||
|
||||
next() {
|
||||
if (this.#index <= this.#value.length) {
|
||||
const result = this.#value[this.#index]
|
||||
this.#index += 1
|
||||
return this.#value[this.#index]
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,6 +97,8 @@ class StringIterator extends ArrayIterator {
|
|||
}
|
||||
|
||||
export class Stream extends Iterator {
|
||||
iterator
|
||||
|
||||
constructor(value) {
|
||||
super()
|
||||
if (!value.next || !typeof value.next === 'function') {
|
||||
|
@ -114,6 +120,10 @@ export class Stream extends Iterator {
|
|||
return this.iterator.peek()
|
||||
}
|
||||
|
||||
done() {
|
||||
return this.iterator.done()
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Stream(this.iterator.clone())
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { describe, it } from 'node:test'
|
|||
import assert from '../assert.js'
|
||||
import { node, edge, matchClause } from '../../src/query-parser/match.js'
|
||||
import { makeDirectedEdge, makeEdge, makeNode, makeRelationship } from '../utils.js'
|
||||
import { Match } from '../../src/query-parser/types.js'
|
||||
|
||||
describe('node', () => {
|
||||
it('should match a node with a name, label, and properties', () => {
|
||||
|
@ -69,58 +70,58 @@ describe('edge', () => {
|
|||
describe('MATCH keyword', () => {
|
||||
it('should match a single node with no relationships', () => {
|
||||
assert.parseOk(matchClause, 'MATCH (:Label)', ([actual]) => {
|
||||
const expected = makeNode(undefined, 'Label')
|
||||
const expected = new Match(makeNode(undefined, 'Label'))
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
assert.parseOk(matchClause, 'MATCH (node:Label)', ([actual]) => {
|
||||
const expected = makeNode('node', 'Label')
|
||||
const expected = new Match(makeNode('node', 'Label'))
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
assert.parseOk(matchClause, 'MATCH (node:Label { prop: true, value: "test" })', ([actual]) => {
|
||||
const expected = makeNode('node', 'Label', [['prop', true], ['value', 'test']])
|
||||
const expected = new Match(makeNode('node', 'Label', [['prop', true], ['value', 'test']]))
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('should match nodes with a relationship to another another node', () => {
|
||||
assert.parseOk(matchClause, 'MATCH (:Node)-[:Edge]->(:Node)', ([actual]) => {
|
||||
const expected = makeRelationship(
|
||||
const expected = new Match(makeRelationship(
|
||||
makeNode(undefined, 'Node'),
|
||||
makeDirectedEdge(undefined, 'Edge', 1),
|
||||
makeNode(undefined, 'Node'),
|
||||
)
|
||||
))
|
||||
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
assert.parseOk(matchClause, 'MATCH (a:Node)-[e:Edge]->(b:Node)', ([actual]) => {
|
||||
const expected = makeRelationship(
|
||||
const expected = new Match(makeRelationship(
|
||||
makeNode('a', 'Node'),
|
||||
makeDirectedEdge('e', 'Edge', 1),
|
||||
makeNode('b', 'Node'),
|
||||
)
|
||||
))
|
||||
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
assert.parseOk(matchClause, 'MATCH (a:Node { db: 0.7 })-[e:Edge]->(b:Node { db: 0.95 })', ([actual]) => {
|
||||
const expected = makeRelationship(
|
||||
const expected = new Match(makeRelationship(
|
||||
makeNode('a', 'Node', [['db', 0.7]]),
|
||||
makeDirectedEdge('e', 'Edge', 1),
|
||||
makeNode('b', 'Node', [['db', 0.95]]),
|
||||
)
|
||||
))
|
||||
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
assert.parseOk(matchClause, 'MATCH (:Node { db: 0.7 })-[:Edge]->(:Node { db: 0.95 })', ([actual]) => {
|
||||
const expected = makeRelationship(
|
||||
const expected = new Match(makeRelationship(
|
||||
makeNode(undefined, 'Node', [['db', 0.7]]),
|
||||
makeDirectedEdge(undefined, 'Edge', 1),
|
||||
makeNode(undefined, 'Node', [['db', 0.95]]),
|
||||
)
|
||||
))
|
||||
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
@ -128,7 +129,7 @@ describe('MATCH keyword', () => {
|
|||
|
||||
it('should handle multiple relationships', () => {
|
||||
assert.parseOk(matchClause, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', ([actual]) => {
|
||||
const expected = makeRelationship(
|
||||
const expected = new Match(makeRelationship(
|
||||
makeNode('player', 'Player'),
|
||||
makeDirectedEdge(undefined, 'Knows', 1),
|
||||
makeRelationship(
|
||||
|
@ -136,7 +137,7 @@ describe('MATCH keyword', () => {
|
|||
makeDirectedEdge(undefined, 'Knows', 1),
|
||||
makeNode('b', 'NPC'),
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
assert.deepEqual(actual, expected)
|
||||
})
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
import { describe, it } from 'node:test'
|
||||
import assert from '../assert.js'
|
||||
import { query } from '../../src/query-parser/index.js'
|
||||
import { Alias, Identifier, ObjectPath, Query, ReturnValues, SelectedGraph } from '../../src/query-parser/types.js'
|
||||
import { Alias, Identifier, Match, ObjectPath, Query, ReturnValues, SelectedGraph } from '../../src/query-parser/types.js'
|
||||
import { makeNode, makeRelationship, makeRightEdge } from '../utils.js'
|
||||
import { map } from '../../src/fn.js'
|
||||
|
||||
const path = (...v) => new ObjectPath(...v)
|
||||
const alias = (...v) => new Alias(...v)
|
||||
const path = (...x) => new ObjectPath(...x)
|
||||
const alias = (...x) => new Alias(...x)
|
||||
const identifier = n => new Identifier(n)
|
||||
const graph = n => new SelectedGraph(identifier(n))
|
||||
const returnVals = (...v) => new ReturnValues(...v)
|
||||
const q = (u, m, rv) => new Query(u, m, rv)
|
||||
const returnVals = (...x) => new ReturnValues(...x)
|
||||
const matches = (...args) => map(x => new Match(x), args)
|
||||
const q = (u, m, w, rv) => new Query(u, m, w, rv)
|
||||
|
||||
describe('query', () => {
|
||||
it('should match a node', () => {
|
||||
assert.parseOk(query, 'MATCH (node:Label) RETURN node', ([actual]) => {
|
||||
const expected = q(
|
||||
undefined, // no use clause
|
||||
makeNode('node', 'Label'),
|
||||
matches(makeNode('node', 'Label')),
|
||||
[],
|
||||
returnVals(identifier('node'))
|
||||
)
|
||||
|
||||
|
@ -28,11 +31,12 @@ describe('query', () => {
|
|||
assert.parseOk(query, 'MATCH (rown:Creature)-[:Petpats]->(kbity:NetCat) RETURN rown, kbity', ([actual]) => {
|
||||
const expected = q(
|
||||
undefined, // no use clause
|
||||
makeRelationship(
|
||||
matches(makeRelationship(
|
||||
makeNode('rown', 'Creature'),
|
||||
makeRightEdge(undefined, 'Petpats'),
|
||||
makeNode('kbity', 'NetCat'),
|
||||
),
|
||||
)),
|
||||
[],
|
||||
returnVals(identifier('rown'), identifier('kbity'))
|
||||
)
|
||||
|
||||
|
@ -44,7 +48,8 @@ describe('query', () => {
|
|||
assert.parseOk(query, 'USE aminals MATCH (kbity:Cat) RETURN kbity', ([actual]) => {
|
||||
const expected = q(
|
||||
graph('aminals'),
|
||||
makeNode('kbity', 'Cat'),
|
||||
matches(makeNode('kbity', 'Cat')),
|
||||
[],
|
||||
returnVals(identifier('kbity'))
|
||||
)
|
||||
|
||||
|
@ -56,11 +61,12 @@ describe('query', () => {
|
|||
assert.parseOk(query, 'USE aminals MATCH (kbity:Cat { type: "cute" })-[:Eats { schedule: "daily" }]->(snacc:Feesh { type: "vegan", weight: 0.7 }) RETURN kbity.name AS name, snacc AS food', ([actual]) => {
|
||||
const expected = q(
|
||||
graph('aminals'),
|
||||
makeRelationship(
|
||||
matches(makeRelationship(
|
||||
makeNode('kbity', 'Cat', [['type', 'cute']]),
|
||||
makeRightEdge(undefined, 'Eats', [['schedule', 'daily']]),
|
||||
makeNode('snacc', 'Feesh', [['type', 'vegan'], ['weight', 0.7]])
|
||||
),
|
||||
)),
|
||||
[],
|
||||
returnVals(
|
||||
alias(path(identifier('kbity'), identifier('name')), identifier('name')),
|
||||
alias(identifier('snacc'), identifier('food'))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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'
|
||||
import { And, Equal, Filter, GreaterThan, GreaterThanEqual, Identifier, LesserThan, LesserThanEqual, Literal, ObjectPath, Or, Property } from '../../src/query-parser/types.js'
|
||||
import { parse } from '../../src/parser.js'
|
||||
|
||||
const l = x => new Literal(x)
|
||||
describe('where operators', () => {
|
||||
|
@ -82,9 +83,25 @@ describe('where operators', () => {
|
|||
|
||||
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)
|
||||
assert.parseOk(whereClause, 'WHERE h.max > 500 AND (h.current < 250 OR a.value = 10) OR a.value <= 2', ([actual]) => {
|
||||
const expected = [
|
||||
new ObjectPath(new Identifier('h'), new Property('max')),
|
||||
new Literal(500),
|
||||
GreaterThan,
|
||||
new ObjectPath(new Identifier('h'), new Property('current')),
|
||||
new Literal(250),
|
||||
LesserThan,
|
||||
new ObjectPath(new Identifier('a'), new Property('value')),
|
||||
Or,
|
||||
new Literal(10),
|
||||
Equal,
|
||||
And,
|
||||
new ObjectPath(new Identifier('a'), new Property('value')),
|
||||
Or,
|
||||
new Literal(2),
|
||||
LesserThanEqual
|
||||
]
|
||||
assert.deepEqual(actual.values, expected)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -11,16 +11,19 @@ const create = (world, ...components) => {
|
|||
return entity
|
||||
}
|
||||
|
||||
const relate = (world, a, type, ...b) => {
|
||||
return b.map(v => {
|
||||
const relate = (world, from, type, ...b) => {
|
||||
return b.map(to => {
|
||||
const edge = addEntity(world)
|
||||
addComponent(world, type, edge)
|
||||
type.from[edge] = a
|
||||
type.to[edge] = v
|
||||
update(type, { from, to }, edge)
|
||||
return edge
|
||||
})
|
||||
}
|
||||
|
||||
const update = (type, values, entity) => {
|
||||
Object.entries(values).forEach(([k, v]) => { type[k][entity] = v })
|
||||
}
|
||||
|
||||
describe('query', () => {
|
||||
before(() => {
|
||||
const world = { default: createWorld() }
|
||||
|
@ -55,7 +58,7 @@ describe('query', () => {
|
|||
relate(world, b, Knows, a) // 9
|
||||
relate(world, c, Knows, player) // 10
|
||||
|
||||
// tag components should return an object object
|
||||
// tag components should return an empty object
|
||||
assert.deepEqual(
|
||||
query('MATCH (player:Player) RETURN player', engine).unwrap(),
|
||||
[{ player: {} }]
|
||||
|
@ -88,12 +91,12 @@ describe('query', () => {
|
|||
|
||||
// an unspecified component should return an Entity
|
||||
assert.deepEqual(
|
||||
query('MATCH (e, h:Health) WHERE e, h.max', engine).unwrap(),
|
||||
query('MATCH (e, h:Health) RETURN e, h.max', engine).unwrap(),
|
||||
[{ e: 11, h: { max: 50 } }]
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
query('MATCH (e:Entity, h:Health) WHERE e, h.max', engine).unwrap(),
|
||||
query('MATCH (e:Entity, h:Health) RETURN e, h.max', engine).unwrap(),
|
||||
[{ e: 11, h: { max: 50 } }]
|
||||
)
|
||||
})
|
||||
|
@ -101,14 +104,28 @@ describe('query', () => {
|
|||
it('should support filtering', () => {
|
||||
const world = engine.world.default
|
||||
const { Player, Health } = engine.component
|
||||
const player = create(world, Player, Health)
|
||||
const player = create(world, Player, Health) // 12
|
||||
const enemy = create(world, Health) // 13
|
||||
|
||||
Health.max[player] = 50
|
||||
Health.current[player] = 25
|
||||
update(Health, { max: 50, current: 25 }, player)
|
||||
update(Health, { max: 50, current: 35 }, enemy)
|
||||
|
||||
assert.deepEqual(
|
||||
query('MATCH (h:Health) WHERE h.current < 30 RETURN h.max', engine).unwrap(),
|
||||
[{ e: 11, h: { max: 50 } }]
|
||||
query('MATCH (e, h:Health) WHERE h.current < 30 RETURN e, h.max', engine).unwrap(),
|
||||
[{ e: 12, h: { max: 50 } }]
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
query('MATCH (e, h:Health) WHERE e = 13 AND h.max = 50 OR h.current < 25 RETURN e, h.max', engine).unwrap(),
|
||||
[{ e: 13, h: { max: 50 } }]
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
query('MATCH (e, h:Health) WHERE h.max = 50 OR h.current < 50 RETURN e, h.max AS maxHealth', engine).unwrap(),
|
||||
[
|
||||
{ e: 12, maxHealth: 50 },
|
||||
{ e: 13, maxHealth: 50 }
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue