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",
|
"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": "",
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
pluck(node) {
|
||||||
constructor(value) {
|
const key = this.valueOf()
|
||||||
super(value)
|
return { [key]: node[key] }
|
||||||
}
|
|
||||||
|
|
||||||
from(component) {
|
|
||||||
return new Proxy(component, {
|
|
||||||
get(target, entity, _receiver) {
|
|
||||||
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,22 +74,33 @@ 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,45 +109,131 @@ export class Operator {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
74
src/query.js
74
src/query.js
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -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 }
|
||||||
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue