diff --git a/src/fn.js b/src/fn.js index 4491065..573c706 100644 --- a/src/fn.js +++ b/src/fn.js @@ -12,6 +12,8 @@ export function curry(func) { } } +export const flip = fn => curry((a, b) => fn(b, a)) + // predicates export const and = (...booleans) => booleans.every(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 of = value => Array.isArray(value) ? value : [value] +export const concat = curry((a, b) => b.concat(a)) export const head = value => value[0] export const tail = value => value.slice(1) export const prop = curry((p, v) => v[p]) diff --git a/src/parser.js b/src/parser.js index e5b355d..bc202a5 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,6 +1,7 @@ import { Result } from './result.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 { constructor(message, state, source) { @@ -205,9 +206,10 @@ export const maybe = curry((parser, state) => { return result.isOk() ? result : succeed([], state) }) -export const eof = (state) => { - return rest[1].done() ? succeed([], state) : fail('eof did not match', state) +export const eof = 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))) diff --git a/src/query/common.js b/src/query-parser/common.js similarity index 100% rename from src/query/common.js rename to src/query-parser/common.js diff --git a/src/query/index.js b/src/query-parser/index.js similarity index 89% rename from src/query/index.js rename to src/query-parser/index.js index 3517a2b..170b3ab 100644 --- a/src/query/index.js +++ b/src/query-parser/index.js @@ -12,11 +12,12 @@ const hasUseClause = pipe( ) const constructQuery = construct(Query) +const noUseQuery = pipe(prepend(undefined), constructQuery) const collect = ifElse( hasUseClause, constructQuery, - pipe(prepend(undefined), constructQuery) + noUseQuery ) export const query = map( diff --git a/src/query/match.js b/src/query-parser/match.js similarity index 100% rename from src/query/match.js rename to src/query-parser/match.js diff --git a/src/query/return.js b/src/query-parser/return.js similarity index 100% rename from src/query/return.js rename to src/query-parser/return.js diff --git a/src/query/types.js b/src/query-parser/types.js similarity index 97% rename from src/query/types.js rename to src/query-parser/types.js index 28debcd..f950620 100644 --- a/src/query/types.js +++ b/src/query-parser/types.js @@ -84,6 +84,10 @@ export class Identifier { constructor(value) { this.value = value } + + get literal() { + return this.value + } } export class Alias { diff --git a/src/query/use.js b/src/query-parser/use.js similarity index 100% rename from src/query/use.js rename to src/query-parser/use.js diff --git a/src/query.js b/src/query.js new file mode 100644 index 0000000..958d608 --- /dev/null +++ b/src/query.js @@ -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 + }) +}) + diff --git a/tests/query/common.test.js b/tests/query-parser/common.test.js similarity index 100% rename from tests/query/common.test.js rename to tests/query-parser/common.test.js diff --git a/tests/query/match.test.js b/tests/query-parser/match.test.js similarity index 100% rename from tests/query/match.test.js rename to tests/query-parser/match.test.js diff --git a/tests/query/query.test.js b/tests/query-parser/query.test.js similarity index 100% rename from tests/query/query.test.js rename to tests/query-parser/query.test.js diff --git a/tests/query/return.test.js b/tests/query-parser/return.test.js similarity index 100% rename from tests/query/return.test.js rename to tests/query-parser/return.test.js diff --git a/tests/query/use.test.js b/tests/query-parser/use.test.js similarity index 100% rename from tests/query/use.test.js rename to tests/query-parser/use.test.js diff --git a/tests/query.test.js b/tests/query.test.js new file mode 100644 index 0000000..c27106f --- /dev/null +++ b/tests/query.test.js @@ -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()) + }) +}) +