add where clauses and aliasing

This commit is contained in:
Rowan 2024-11-27 21:20:43 -06:00
parent 0411e34f10
commit d9ebc1e875
15 changed files with 393 additions and 216 deletions

View file

@ -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": "",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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