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", "type": "module",
"scripts": { "scripts": {
"start": "node src/index.js", "start": "node src/index.js",
"test": "node --test ./tests/**/*.test.js" "test": "node --test ./tests/*.test.js ./tests/**/*.test.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View file

@ -209,8 +209,23 @@ export const maybe = curry((parser, state) => {
return result.isOk() ? result : succeed([], 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 => { 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)) export const parse = curry((parser, input) => pipe(ParserState, parser)(input))

View file

@ -1,5 +1,5 @@
import { join } from '../fn.js' import { join, values } from '../fn.js'
import { alpha, alphanumeric, any, char, digit, list, many, map, maybe, noCaseString, separated, seq, skip, string, until } from '../parser.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' import { Alias, Identifier, Literal, ObjectPath, Property } from './types.js'
export const Symbols = Object.freeze({ export const Symbols = Object.freeze({
@ -33,7 +33,14 @@ export const Symbols = Object.freeze({
Quote: char('"') 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 collect = parser => map(join(''), parser)
export const quoted = collect(seq(skip(Symbols.Quote), until(Symbols.Quote), skip(Symbols.Quote))) export const quoted = collect(seq(skip(Symbols.Quote), until(Symbols.Quote), skip(Symbols.Quote)))
export const number = seq(digit, many(digit)) export const number = seq(digit, many(digit))
@ -57,4 +64,5 @@ export const baseValue = any(literal, accessor, identifier)
const as = noCaseString('as') const as = noCaseString('as')
export const alias = map(([value, name]) => new Alias(value, name), seq(baseValue, word(skip(as)), identifier)) export const alias = map(([value, name]) => new Alias(value, name), seq(baseValue, word(skip(as)), identifier))
export const value = any(alias, baseValue) 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 { map, maybe, seq } from '../parser.js'
import { word } from './common.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 { 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( const use = find(is(SelectedGraph))
head, const match = filter(is(Match))
is(SelectedGraph) const where = filter(is(Filter))
) const rv = find(is(ReturnValues))
const constructQuery = construct(Query) const collect = clauses => {
const noUseQuery = pipe(prepend(undefined), constructQuery) return new Query(
use(clauses),
const collect = ifElse( match(clauses),
hasUseClause, where(clauses),
constructQuery, rv(clauses)
noUseQuery )
) }
export const query = map( export const query = map(
collect, collect,
seq( seq(
maybe(useClause), maybe(word(useClause)),
word(matchClause), word(matchClause),
maybe(word(whereClause)),
word(returnClause) word(returnClause)
) )
) )

View file

@ -1,9 +1,9 @@
import { identifier, literal, Symbol, ws } from './common.js' import { identifier, Keyword, literal, Symbols, ws } from './common.js'
import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component } from './types.js' import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component, Match } from './types.js'
import { construct, curry, is } from '../fn.js' import { construct, curry, head, is } from '../fn.js'
import { many, maybe, map, seq, skip, between, noCaseString, separated, list, any } from '../parser.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( const name = map(
construct(Name), construct(Name),
@ -67,7 +67,7 @@ const makeRelationship = args => {
const len = args.length - 1 const len = args.length - 1
if (len <= 0) { if (len <= 0) {
return args return new Match(head(args))
} }
let right = args[len] let right = args[len]
@ -78,14 +78,13 @@ const makeRelationship = args => {
right = new Relationship(left, edge, right) right = new Relationship(left, edge, right)
} }
return right return new Match(right)
} }
const keyword = noCaseString('match')
const params = map( const params = map(
makeRelationship, makeRelationship,
seq(node, many(relationship)) 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 { ReturnValues } from './types.js'
import { list, map, noCaseString, seq, skip } from '../parser.js' import { list, map, seq, skip } from '../parser.js'
import { value, Symbol, ws } from './common.js' import { value, Symbols, ws, Keyword } from './common.js'
const keyword = noCaseString('return')
const params = map( const params = map(
x => new ReturnValues(...x), 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 { class Value {
#value value
constructor(value) { constructor(value) {
this.#value = value this.value = value
} }
toString() { toString() {
return this.#value.toString() return this.value.toString()
} }
valueOf() { valueOf() {
return this.#value?.valueOf() ?? this.#value return this.value?.valueOf() ?? this.value
} }
equals(other) { equals(other) {
@ -20,9 +21,9 @@ class Value {
return false return false
} }
const value = this.value const value = this.valueOf()
return value === other return value === other
|| value === other.value || value === other.valueOf()
} }
} }
@ -31,24 +32,13 @@ export class Identifier extends Value {
super(value) super(value)
} }
from(component) { from(node) {
return component return node[this.valueOf()]
}
}
export class ObjectIdentifier extends Identifier {
constructor(value) {
super(value)
} }
from(component) { pluck(node) {
return new Proxy(component, { const key = this.valueOf()
get(target, entity, _receiver) { return { [key]: node[key] }
return Object.fromEntries(
Object.entries(target).map(([k, v]) => [k, v[entity]])
)
}
})
} }
} }
@ -57,8 +47,8 @@ export class Property extends Identifier {
super(value) super(value)
} }
from(component) { pluck(node) {
return component[this.value] return node[this.valueOf()]
} }
} }
@ -84,69 +74,166 @@ export class Literal {
constructor(value) { constructor(value) {
this.value = value this.value = value
} }
from(node) {
return this.valueOf()
}
valueOf() {
return this.value.valueOf()
}
} }
export const None = new Literal(null) export const None = new Literal(null)
export class Operator { export class Function {
#fn #fn
constructor(fn, length = fn.length) { constructor(name, fn, length = fn.length) {
this.name = name
this.#fn = fn this.#fn = fn
this.length = length this.length = length
} }
#pop(n, stack) { #pop(stack) {
let result = [] 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.push(stack.pop())
} }
result.reverse() result.reverse()
return result return result
} }
apply(stack) { apply(stack) {
const args = this.#pop(this.length, stack) const args = this.#pop(stack)
return this.#fn.apply(undefined, args) return this.#fn.apply(undefined, args)
} }
} }
export class UnaryExpression { const Associativity = Object.freeze({
constructor(op, value) { Left: 0,
this.op = op Right: 1
this.value = value })
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 { export const Is = new Operator('IS', (x, y) => typeof x === y, 2, Associativity.Right)
constructor(left, op, right) { export const Not = new Operator('NOT', x => !x, 2, Associativity.Right)
this.left = left export const And = new Operator('AND', (x, y) => x && y, 11)
this.op = op export const Or = new Operator('OR', (x, y) => x || y, 12)
this.right = right 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 Is = new Operator((x, y) => typeof x === y) export const LesserThanEqual = new Operator('LTE', (x, y) => x <= y, 6)
export const Not = new Operator(x => !x) export const Equal = new Operator('EQ', (x, y) => x === y, 7)
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 { export class Filter {
constructor(...args) { constructor(...args) {
this.values = this.#shufflingYard(args) this.values = this.#shuntingYard(args)
} }
#shufflingYard(args) { #takeWhile(fn, stack) {
// do the thing const result = []
return args 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) super(value)
this.alias = alias this.alias = alias
} }
pluck(node) {
return { [this.alias.valueOf()]: this.value.from(node) }
}
} }
export class ObjectPath extends Identifier { export class ObjectPath extends Identifier {
@ -164,11 +255,16 @@ export class ObjectPath extends Identifier {
} }
toString() { toString() {
return [this.valueOf()].concat(path).join('.') return [this.valueOf()].concat(this.path).join('.')
} }
from(component) { from(node) {
return this.path.reduce((acc, v) => v.from(acc), component) 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) { equals(value) {
@ -236,65 +332,33 @@ export class Relationship {
} }
} }
const isExactly = (Type, value) => Object.getPrototypeOf(value) === Type.prototype
export class ReturnValues { export class ReturnValues {
constructor(...args) { constructor(...args) {
this.values = args.map(x => isExactly(Identifier, x) ? new ObjectIdentifier(x) : x) this.values = args
} }
#isTag(type) { from(values) {
type && Object.getOwnPropertySymbols(type).find( // TODO: pass with entries
(s) => s.description === 'tagStore' const returns = Object.keys(values).map(key => this.#pluck(values, key))
) || false return Object.assign({}, ...returns)
} }
#forComponentType(value, type) { #pluck(values, name) {
if (this.#isTag(type)) { return this.findName(name)
return {} .map(value => value.pluck(values))
} else if (typeof type === 'function') { .reduce(mergeRightDeep, {})
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)), {})
} }
find(fn) { find(fn) {
return this.values.find(fn) return this.values.find(fn)
} }
findName(name) {
return this.find(value => value.equals(name))
}
filter(fn) { filter(fn) {
return this.values.filter(fn) return this.values.filter(fn)
} }
includes(value) { findName(value) {
return this.find(x => x.equals(value)) return this.filter(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), {})
} }
} }
@ -305,10 +369,21 @@ export class KeyValuePair {
} }
} }
export class Match {
constructor(value) {
this.value = value
}
valueOf() {
return this.value
}
}
export class Query { export class Query {
constructor(use, match, returnValues) { constructor(use, matches, filters, returnValues) {
this.use = use this.use = use
this.match = match this.matches = matches
this.filters = filters
this.returnValues = returnValues 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 { map, noCaseString, seq, skip } from '../parser.js'
import { SelectedGraph } from './types.js' import { SelectedGraph } from './types.js'
const keyword = noCaseString('use') export const useKeyword = noCaseString('use')
export const useClause = map( export const useClause = map(
([graph]) => new SelectedGraph(graph), ([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 { construct } from '../fn.js'
import { always, any, char, many1, map, map1, noCaseString, seq, skip, string } from '../parser.js' import { always, any, char, many1, manyUntil, map, map1, noCaseString, seq, skip, string } from '../parser.js'
import { Symbols, value, word, ws } from './common.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' import { And, Equal, Filter, GreaterThan, GreaterThanEqual, LesserThan, LesserThanEqual, Literal, None, Not, Or, Xor } from './types.js'
const { Left, Right } = Symbols.Bracket.Round const { Left, Right } = Symbols.Bracket.Round
@ -30,7 +30,6 @@ export const not = always(Not, noCaseString('not'))
export const unary = any(not) export const unary = any(not)
export const expr = any(lparen, rparen, compare, logic, unary, value) 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.Where), ws, params)
export const whereClause = seq(skip(keyword), ws, params)

View file

@ -1,32 +1,31 @@
import { defineQuery, hasComponent } from 'bitecs' import { defineQuery, hasComponent } from 'bitecs'
import { query as q } from './query-parser/index.js' import { query as q } from './query-parser/index.js'
import { parseAll } from './parser.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' import { Relationship } from './query-parser/types.js'
const nodeComponent = component => node => { const nodeComponent = component => node => {
const label = node.label?.value const label = node.label?.valueOf()
const name = node.name?.value const name = node.name?.valueOf()
const type = label && component[node.label.value] || identity const type = label && component[label] || identity
return { name, label, type } return { name, label, type }
} }
const notEntity = ({ label }) => label != null && label !== 'Entity' const notEntity = ({ label }) => label != null && label.valueOf() !== 'Entity'
const prepareQuery = (query, component) => { const prepareQuery = (match, component) => {
const { match, returnValues } = query
const relationship = is(Relationship, match) const relationship = is(Relationship, match)
if (relationship) { if (relationship) {
const { from: f, to: t, edge: e } = match 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 } return { from, to, edge, relationship, schema: query }
} else { } else {
const components = match.components.map(nodeComponent(component)) const components = match.components.map(nodeComponent(component))
const types = components.filter(notEntity).map(prop('type')) 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 getValue = curry((type, entity) => {
const { entity, query: { getReturns } } = node if (isTag(type)) {
return { ...obj, ...getReturns(entity) } 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 resolveRelationship = edges => {
const { entity, query } = edges const { entity, query } = edges
const Edge = query.edge.components[0].type const Edge = query.edge.components[0].type
const to = query.to?.from ?? query.to const to = query.to?.from ?? query.to
return pipe( return [
resolveNode({ entity: Edge.from[entity], query: query.from }), resolveNode({ entity: Edge.from[entity], query: query.from }),
resolveNode({ entity, query: query.edge }), resolveNode({ entity, query: query.edge }),
resolveNode({ entity: Edge.to[entity], query: to }), resolveNode({ entity: Edge.to[entity], query: to }),
)({}) ]
} }
const resolveReturns = query => { const resolveValues = query => {
return query.map(edges => { return query.map(edges => {
const returns = edges.map(edge => { const values = edges.map(edge => {
if (edge.query.relationship) { if (edge.query.relationship) {
return resolveRelationship(edge) return resolveRelationship(edge)
} else { } 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 }) => { export const query = curry((input, { world, component }) => {
return parseAll(q, input).map(({ use, ...rest }) => { return parseAll(q, input).map(({ use, matches, filters, returnValues, ...rest }) => {
const useWorld = world[use?.value ?? 'default'] const useWorld = world[use?.valueOf() ?? 'default']
const preparedQuery = prepareQuery(rest, component) const match = matches[0].valueOf()
const preparedQuery = prepareQuery(match, component)
const results = executeQuery(preparedQuery, useWorld) const results = executeQuery(preparedQuery, useWorld)
const returns = resolveReturns(results) const values = resolveValues(results)
return returns 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() { done() {
return !!this.peek() return this.peek() == null
} }
take(n) { take(n) {
let accumulator = [] let accumulator = []
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
if(this.done()) {
break
}
accumulator.push(this.next()) accumulator.push(this.next())
} }
@ -54,10 +57,10 @@ export class Iterator {
class ArrayIterator extends Iterator { class ArrayIterator extends Iterator {
static { Iterator.handledTypes.set(Array, ArrayIterator) } static { Iterator.handledTypes.set(Array, ArrayIterator) }
#index = -1 #index = 0
#value #value
constructor(value, index = -1) { constructor(value, index = 0) {
super() super()
if (!Array.isArray) { if (!Array.isArray) {
@ -70,8 +73,9 @@ class ArrayIterator extends Iterator {
next() { next() {
if (this.#index <= this.#value.length) { if (this.#index <= this.#value.length) {
const result = this.#value[this.#index]
this.#index += 1 this.#index += 1
return this.#value[this.#index] return result
} }
} }
@ -93,6 +97,8 @@ class StringIterator extends ArrayIterator {
} }
export class Stream extends Iterator { export class Stream extends Iterator {
iterator
constructor(value) { constructor(value) {
super() super()
if (!value.next || !typeof value.next === 'function') { if (!value.next || !typeof value.next === 'function') {
@ -114,6 +120,10 @@ export class Stream extends Iterator {
return this.iterator.peek() return this.iterator.peek()
} }
done() {
return this.iterator.done()
}
clone() { clone() {
return new Stream(this.iterator.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 assert from '../assert.js'
import { node, edge, matchClause } from '../../src/query-parser/match.js' import { node, edge, matchClause } from '../../src/query-parser/match.js'
import { makeDirectedEdge, makeEdge, makeNode, makeRelationship } from '../utils.js' import { makeDirectedEdge, makeEdge, makeNode, makeRelationship } from '../utils.js'
import { Match } from '../../src/query-parser/types.js'
describe('node', () => { describe('node', () => {
it('should match a node with a name, label, and properties', () => { it('should match a node with a name, label, and properties', () => {
@ -69,58 +70,58 @@ describe('edge', () => {
describe('MATCH keyword', () => { describe('MATCH keyword', () => {
it('should match a single node with no relationships', () => { it('should match a single node with no relationships', () => {
assert.parseOk(matchClause, 'MATCH (:Label)', ([actual]) => { assert.parseOk(matchClause, 'MATCH (:Label)', ([actual]) => {
const expected = makeNode(undefined, 'Label') const expected = new Match(makeNode(undefined, 'Label'))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(matchClause, 'MATCH (node:Label)', ([actual]) => { assert.parseOk(matchClause, 'MATCH (node:Label)', ([actual]) => {
const expected = makeNode('node', 'Label') const expected = new Match(makeNode('node', 'Label'))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(matchClause, 'MATCH (node:Label { prop: true, value: "test" })', ([actual]) => { 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) assert.deepEqual(actual, expected)
}) })
}) })
it('should match nodes with a relationship to another another node', () => { it('should match nodes with a relationship to another another node', () => {
assert.parseOk(matchClause, 'MATCH (:Node)-[:Edge]->(:Node)', ([actual]) => { assert.parseOk(matchClause, 'MATCH (:Node)-[:Edge]->(:Node)', ([actual]) => {
const expected = makeRelationship( const expected = new Match(makeRelationship(
makeNode(undefined, 'Node'), makeNode(undefined, 'Node'),
makeDirectedEdge(undefined, 'Edge', 1), makeDirectedEdge(undefined, 'Edge', 1),
makeNode(undefined, 'Node'), makeNode(undefined, 'Node'),
) ))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(matchClause, 'MATCH (a:Node)-[e:Edge]->(b:Node)', ([actual]) => { assert.parseOk(matchClause, 'MATCH (a:Node)-[e:Edge]->(b:Node)', ([actual]) => {
const expected = makeRelationship( const expected = new Match(makeRelationship(
makeNode('a', 'Node'), makeNode('a', 'Node'),
makeDirectedEdge('e', 'Edge', 1), makeDirectedEdge('e', 'Edge', 1),
makeNode('b', 'Node'), makeNode('b', 'Node'),
) ))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(matchClause, 'MATCH (a:Node { db: 0.7 })-[e:Edge]->(b:Node { db: 0.95 })', ([actual]) => { 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]]), makeNode('a', 'Node', [['db', 0.7]]),
makeDirectedEdge('e', 'Edge', 1), makeDirectedEdge('e', 'Edge', 1),
makeNode('b', 'Node', [['db', 0.95]]), makeNode('b', 'Node', [['db', 0.95]]),
) ))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(matchClause, 'MATCH (:Node { db: 0.7 })-[:Edge]->(:Node { db: 0.95 })', ([actual]) => { 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]]), makeNode(undefined, 'Node', [['db', 0.7]]),
makeDirectedEdge(undefined, 'Edge', 1), makeDirectedEdge(undefined, 'Edge', 1),
makeNode(undefined, 'Node', [['db', 0.95]]), makeNode(undefined, 'Node', [['db', 0.95]]),
) ))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
@ -128,7 +129,7 @@ describe('MATCH keyword', () => {
it('should handle multiple relationships', () => { it('should handle multiple relationships', () => {
assert.parseOk(matchClause, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', ([actual]) => { assert.parseOk(matchClause, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', ([actual]) => {
const expected = makeRelationship( const expected = new Match(makeRelationship(
makeNode('player', 'Player'), makeNode('player', 'Player'),
makeDirectedEdge(undefined, 'Knows', 1), makeDirectedEdge(undefined, 'Knows', 1),
makeRelationship( makeRelationship(
@ -136,7 +137,7 @@ describe('MATCH keyword', () => {
makeDirectedEdge(undefined, 'Knows', 1), makeDirectedEdge(undefined, 'Knows', 1),
makeNode('b', 'NPC'), makeNode('b', 'NPC'),
) )
) ))
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })

View file

@ -1,22 +1,25 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from '../assert.js' import assert from '../assert.js'
import { query } from '../../src/query-parser/index.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 { makeNode, makeRelationship, makeRightEdge } from '../utils.js'
import { map } from '../../src/fn.js'
const path = (...v) => new ObjectPath(...v) const path = (...x) => new ObjectPath(...x)
const alias = (...v) => new Alias(...v) const alias = (...x) => new Alias(...x)
const identifier = n => new Identifier(n) const identifier = n => new Identifier(n)
const graph = n => new SelectedGraph(identifier(n)) const graph = n => new SelectedGraph(identifier(n))
const returnVals = (...v) => new ReturnValues(...v) const returnVals = (...x) => new ReturnValues(...x)
const q = (u, m, rv) => new Query(u, m, rv) const matches = (...args) => map(x => new Match(x), args)
const q = (u, m, w, rv) => new Query(u, m, w, rv)
describe('query', () => { describe('query', () => {
it('should match a node', () => { it('should match a node', () => {
assert.parseOk(query, 'MATCH (node:Label) RETURN node', ([actual]) => { assert.parseOk(query, 'MATCH (node:Label) RETURN node', ([actual]) => {
const expected = q( const expected = q(
undefined, // no use clause undefined, // no use clause
makeNode('node', 'Label'), matches(makeNode('node', 'Label')),
[],
returnVals(identifier('node')) returnVals(identifier('node'))
) )
@ -28,11 +31,12 @@ describe('query', () => {
assert.parseOk(query, 'MATCH (rown:Creature)-[:Petpats]->(kbity:NetCat) RETURN rown, kbity', ([actual]) => { assert.parseOk(query, 'MATCH (rown:Creature)-[:Petpats]->(kbity:NetCat) RETURN rown, kbity', ([actual]) => {
const expected = q( const expected = q(
undefined, // no use clause undefined, // no use clause
makeRelationship( matches(makeRelationship(
makeNode('rown', 'Creature'), makeNode('rown', 'Creature'),
makeRightEdge(undefined, 'Petpats'), makeRightEdge(undefined, 'Petpats'),
makeNode('kbity', 'NetCat'), makeNode('kbity', 'NetCat'),
), )),
[],
returnVals(identifier('rown'), identifier('kbity')) returnVals(identifier('rown'), identifier('kbity'))
) )
@ -44,7 +48,8 @@ describe('query', () => {
assert.parseOk(query, 'USE aminals MATCH (kbity:Cat) RETURN kbity', ([actual]) => { assert.parseOk(query, 'USE aminals MATCH (kbity:Cat) RETURN kbity', ([actual]) => {
const expected = q( const expected = q(
graph('aminals'), graph('aminals'),
makeNode('kbity', 'Cat'), matches(makeNode('kbity', 'Cat')),
[],
returnVals(identifier('kbity')) 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]) => { 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( const expected = q(
graph('aminals'), graph('aminals'),
makeRelationship( matches(makeRelationship(
makeNode('kbity', 'Cat', [['type', 'cute']]), makeNode('kbity', 'Cat', [['type', 'cute']]),
makeRightEdge(undefined, 'Eats', [['schedule', 'daily']]), makeRightEdge(undefined, 'Eats', [['schedule', 'daily']]),
makeNode('snacc', 'Feesh', [['type', 'vegan'], ['weight', 0.7]]) makeNode('snacc', 'Feesh', [['type', 'vegan'], ['weight', 0.7]])
), )),
[],
returnVals( returnVals(
alias(path(identifier('kbity'), identifier('name')), identifier('name')), alias(path(identifier('kbity'), identifier('name')), identifier('name')),
alias(identifier('snacc'), identifier('food')) alias(identifier('snacc'), identifier('food'))

View file

@ -1,7 +1,8 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from '../assert.js' 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, 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) const l = x => new Literal(x)
describe('where operators', () => { describe('where operators', () => {
@ -82,9 +83,25 @@ describe('where operators', () => {
describe('WHERE keyword', () => { describe('WHERE keyword', () => {
it('handles complex filters', () => { it('handles complex filters', () => {
assert.parseOk(whereClause, 'WHERE 1.1 > 5 AND (1 = 500 OR 2 > 3) OR 5 <= 2', ([actual]) => { assert.parseOk(whereClause, 'WHERE h.max > 500 AND (h.current < 250 OR a.value = 10) OR a.value <= 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)) const expected = [
assert.deepEqual(actual, 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 return entity
} }
const relate = (world, a, type, ...b) => { const relate = (world, from, type, ...b) => {
return b.map(v => { return b.map(to => {
const edge = addEntity(world) const edge = addEntity(world)
addComponent(world, type, edge) addComponent(world, type, edge)
type.from[edge] = a update(type, { from, to }, edge)
type.to[edge] = v
return edge return edge
}) })
} }
const update = (type, values, entity) => {
Object.entries(values).forEach(([k, v]) => { type[k][entity] = v })
}
describe('query', () => { describe('query', () => {
before(() => { before(() => {
const world = { default: createWorld() } const world = { default: createWorld() }
@ -55,7 +58,7 @@ describe('query', () => {
relate(world, b, Knows, a) // 9 relate(world, b, Knows, a) // 9
relate(world, c, Knows, player) // 10 relate(world, c, Knows, player) // 10
// tag components should return an object object // tag components should return an empty object
assert.deepEqual( assert.deepEqual(
query('MATCH (player:Player) RETURN player', engine).unwrap(), query('MATCH (player:Player) RETURN player', engine).unwrap(),
[{ player: {} }] [{ player: {} }]
@ -88,12 +91,12 @@ describe('query', () => {
// an unspecified component should return an Entity // an unspecified component should return an Entity
assert.deepEqual( 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 } }] [{ e: 11, h: { max: 50 } }]
) )
assert.deepEqual( 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 } }] [{ e: 11, h: { max: 50 } }]
) )
}) })
@ -101,14 +104,28 @@ describe('query', () => {
it('should support filtering', () => { it('should support filtering', () => {
const world = engine.world.default const world = engine.world.default
const { Player, Health } = engine.component 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 update(Health, { max: 50, current: 25 }, player)
Health.current[player] = 25 update(Health, { max: 50, current: 35 }, enemy)
assert.deepEqual( assert.deepEqual(
query('MATCH (h:Health) WHERE h.current < 30 RETURN h.max', engine).unwrap(), query('MATCH (e, h:Health) WHERE h.current < 30 RETURN e, h.max', engine).unwrap(),
[{ e: 11, h: { max: 50 } }] [{ 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 }
]
) )
}) })
}) })