basic query engine

This commit is contained in:
Rowan 2024-11-20 03:54:35 -06:00
parent 07ef20a036
commit f843146bf2
15 changed files with 198 additions and 5 deletions

View file

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

View file

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

View file

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

View file

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