basic query engine
This commit is contained in:
parent
07ef20a036
commit
f843146bf2
15 changed files with 198 additions and 5 deletions
|
@ -12,6 +12,8 @@ export function curry(func) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const flip = fn => curry((a, b) => fn(b, a))
|
||||||
|
|
||||||
// predicates
|
// predicates
|
||||||
export const and = (...booleans) => booleans.every(identity)
|
export const and = (...booleans) => booleans.every(identity)
|
||||||
export const or = (...booleans) => booleans.some(identity)
|
export const or = (...booleans) => booleans.some(identity)
|
||||||
|
@ -20,6 +22,7 @@ export const isOk = r => !is(Result, r) || r.isOk()
|
||||||
export const construct = Type => args => new Type(...args)
|
export const construct = Type => args => new Type(...args)
|
||||||
|
|
||||||
export const of = value => Array.isArray(value) ? value : [value]
|
export const of = value => Array.isArray(value) ? value : [value]
|
||||||
|
export const concat = curry((a, b) => b.concat(a))
|
||||||
export const head = value => value[0]
|
export const head = value => value[0]
|
||||||
export const tail = value => value.slice(1)
|
export const tail = value => value.slice(1)
|
||||||
export const prop = curry((p, v) => v[p])
|
export const prop = curry((p, v) => v[p])
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Result } from './result.js'
|
import { Result } from './result.js'
|
||||||
import { Iterator, Stream } from './stream.js'
|
import { Iterator, Stream } from './stream.js'
|
||||||
import { curry, join, of } from './fn.js'
|
import { curry, head, join, of } from './fn.js'
|
||||||
|
import { pipe } from 'bitecs'
|
||||||
|
|
||||||
class ParseError extends Error {
|
class ParseError extends Error {
|
||||||
constructor(message, state, source) {
|
constructor(message, state, source) {
|
||||||
|
@ -205,9 +206,10 @@ export const maybe = curry((parser, state) => {
|
||||||
return result.isOk() ? result : succeed([], state)
|
return result.isOk() ? result : succeed([], state)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const eof = (state) => {
|
export const eof = state => {
|
||||||
return rest[1].done() ? succeed([], state) : fail('eof did not match', state)
|
return state[1].done() ? succeed([], state) : fail('eof did not match', state)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parse = curry((parser, input) => parser(ParserState(input)))
|
export const parse = curry((parser, input) => pipe(ParserState, parser)(input))
|
||||||
|
export const parseAll = curry((parser, input) => parse(seq(parser, eof), input).map(pipe(head, head)))
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,12 @@ const hasUseClause = pipe(
|
||||||
)
|
)
|
||||||
|
|
||||||
const constructQuery = construct(Query)
|
const constructQuery = construct(Query)
|
||||||
|
const noUseQuery = pipe(prepend(undefined), constructQuery)
|
||||||
|
|
||||||
const collect = ifElse(
|
const collect = ifElse(
|
||||||
hasUseClause,
|
hasUseClause,
|
||||||
constructQuery,
|
constructQuery,
|
||||||
pipe(prepend(undefined), constructQuery)
|
noUseQuery
|
||||||
)
|
)
|
||||||
|
|
||||||
export const query = map(
|
export const query = map(
|
|
@ -84,6 +84,10 @@ export class Identifier {
|
||||||
constructor(value) {
|
constructor(value) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get literal() {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Alias {
|
export class Alias {
|
118
src/query.js
Normal file
118
src/query.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import { defineQuery, hasComponent, pipe } from 'bitecs'
|
||||||
|
import { query as q } from './query-parser/index.js'
|
||||||
|
import { parseAll } from './parser.js'
|
||||||
|
import { curry, of, is, reduce, flip, concat, map } from './fn.js'
|
||||||
|
import { Edge, Node, Relationship } from './query-parser/types.js'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* necessary engine structure
|
||||||
|
* worlds: World[]
|
||||||
|
* Component: Component[]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isEmpty = obj => {
|
||||||
|
for (const _ in obj) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareQuery = (query, component) => {
|
||||||
|
const { match, returnValues } = query
|
||||||
|
const relationship = is(Relationship, match)
|
||||||
|
|
||||||
|
if (relationship) {
|
||||||
|
const { from: f, to: t, edge: e } = match
|
||||||
|
const [from, to, edge] = [f, t, e].map(q => prepareQuery({ match: q, returnValues }, component))
|
||||||
|
|
||||||
|
return { from, to, edge, relationship, schema: query }
|
||||||
|
} else {
|
||||||
|
const label = match.label?.value?.value
|
||||||
|
const name = match.name?.value?.value
|
||||||
|
const type = component[label]
|
||||||
|
|
||||||
|
return { name, label, type, relationship, query: defineQuery([type]), schema: query }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEndNode = end => end.relationship ? end.from : end
|
||||||
|
|
||||||
|
const matches = (world, type, query, set = new Set()) => entity => {
|
||||||
|
if (set.has(entity)) {
|
||||||
|
return true
|
||||||
|
} else if (hasComponent(world, type, entity) && query.includes(entity)) {
|
||||||
|
set.add(entity)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryRelationship = (from, edge, to, world) => {
|
||||||
|
const matchesFrom = matches(world, from.type, from.results)
|
||||||
|
const matchesTo = matches(world, to.type, to.results)
|
||||||
|
|
||||||
|
const Edge = edge.type
|
||||||
|
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < edge.results.length; i++) {
|
||||||
|
const eid = edge.results[i]
|
||||||
|
|
||||||
|
if (matchesFrom(Edge.from[eid]) && matchesTo(Edge.to[eid])) {
|
||||||
|
result.push(eid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const cat = flip(concat)
|
||||||
|
|
||||||
|
const executeQuery = (query, world) => {
|
||||||
|
if (!query.relationship) {
|
||||||
|
return map(of, query.query(world))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { from, to: _to, edge, returnValues } = query
|
||||||
|
const to = getEndNode(_to)
|
||||||
|
|
||||||
|
const Edge = edge.type
|
||||||
|
|
||||||
|
const edges = queryRelationship(
|
||||||
|
{ type: from.type, results: from.query(world) },
|
||||||
|
{ type: Edge, results: edge.query(world) },
|
||||||
|
{ type: to.type, results: to.query(world) },
|
||||||
|
world
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (_to.relationship) {
|
||||||
|
return reduce((acc, eid) => {
|
||||||
|
const next = {
|
||||||
|
..._to,
|
||||||
|
from: {
|
||||||
|
...to,
|
||||||
|
query: () => of(Edge.to[eid])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = map(cat([eid]), executeQuery(next, world))
|
||||||
|
return [...acc, ...results]
|
||||||
|
}, [], edges)
|
||||||
|
} else {
|
||||||
|
return map(of, edges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const query = curry((input, engine) => {
|
||||||
|
return parseAll(q, input).map(({ use, ...rest }) => {
|
||||||
|
|
||||||
|
const worldName = use && use.value || 'default'
|
||||||
|
const world = engine.world[worldName]
|
||||||
|
const prepared = prepareQuery(rest, engine.component)
|
||||||
|
const results = executeQuery(prepared, world)
|
||||||
|
// TODO: handle return value mapping
|
||||||
|
return results
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
65
tests/query.test.js
Normal file
65
tests/query.test.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { before, describe, it } from 'node:test'
|
||||||
|
import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
|
||||||
|
import assert from './assert.js'
|
||||||
|
import { query } from '../src/query.js'
|
||||||
|
|
||||||
|
let engine = {}
|
||||||
|
describe('query', () => {
|
||||||
|
before(() => {
|
||||||
|
const world = { default: createWorld() }
|
||||||
|
const component = {
|
||||||
|
Player: defineComponent(null, 10),
|
||||||
|
NPC: defineComponent(null, 10),
|
||||||
|
Knows: defineComponent({
|
||||||
|
from: Types.eid,
|
||||||
|
to: Types.eid
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine = { world, component }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('i just need a test runner', () => {
|
||||||
|
const w = engine.world.default
|
||||||
|
const { Player, NPC, Knows } = engine.component
|
||||||
|
|
||||||
|
const create = (...components) => {
|
||||||
|
const eid = addEntity(w)
|
||||||
|
components.forEach(cmp => addComponent(w, cmp, eid))
|
||||||
|
return eid
|
||||||
|
}
|
||||||
|
|
||||||
|
const knows = (a, b) => {
|
||||||
|
const edge = addEntity(w)
|
||||||
|
addComponent(w, Knows, edge)
|
||||||
|
Knows.from[edge] = a
|
||||||
|
Knows.to[edge] = b
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = create(Player) // 0
|
||||||
|
const a = create(NPC) // 1
|
||||||
|
const b = create(NPC) // 2
|
||||||
|
const c = create(NPC) // 3
|
||||||
|
//const d = create(NPC)
|
||||||
|
//const e = create(NPC)
|
||||||
|
//const f = create(NPC)
|
||||||
|
//const g = create(NPC)
|
||||||
|
|
||||||
|
knows(player, a) // 4
|
||||||
|
knows(player, c) // 5
|
||||||
|
|
||||||
|
knows(a, player) // 6
|
||||||
|
knows(a, b) // 7
|
||||||
|
knows(a, c) // 8
|
||||||
|
|
||||||
|
knows(b, a) // 9
|
||||||
|
|
||||||
|
knows(c, player) // 10
|
||||||
|
//knows(c, b) // 11
|
||||||
|
|
||||||
|
const result = query('MATCH (player:NPC) RETURN player, a, b', engine)
|
||||||
|
console.log(result.unwrap())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in a new issue