diff --git a/src/fn.js b/src/fn.js index ebaa050..17c0b36 100644 --- a/src/fn.js +++ b/src/fn.js @@ -43,6 +43,7 @@ export const converge = (fn, tfns) => curryN( export const isArray = Array.isArray export const isNil = v => !v export const is = curry((type, value) => value instanceof type) +export const isString = value => is(String, value) || typeof value === 'string' // branching export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v)) diff --git a/src/query-parser/types.js b/src/query-parser/types.js index f950620..28776ce 100644 --- a/src/query-parser/types.js +++ b/src/query-parser/types.js @@ -1,21 +1,89 @@ -export class Name { +import { is } from '../fn.js' + +class Value { + #value + + constructor(value) { + this.#value = value + } + + get value() { + return this.#value?.value ?? this.#value + } + + equals(other) { + if (other == null) { + return false + } + + const value = this.value + return value === other + || value === other.value + } +} + +export class Identifier extends Value { + constructor(value) { + super(value) + } +} + +export class Name extends Identifier { + constructor(value) { + super(value) + } +} + +export class Label extends Identifier { + constructor(value) { + super(value) + } +} + +export class SelectedGraph extends Identifier { + constructor(value) { + super(value) + } +} + +export class Literal { constructor(value) { this.value = value } } -export class Label { - constructor(value) { - this.value = value + +export class Alias extends Identifier { + constructor(value, alias) { + super(value) + this.alias = alias } } -export class SelectedGraph { - constructor(identifier) { - this.identifier = identifier +export class ObjectPath extends Identifier { + constructor(value, ...path) { + super(value) + this.path = path + } + + toString() { + return this.path.reduce((acc, val) => acc + val, this.value) + } + + from(obj) { + return this.path.reduce((obj, key) => obj && obj[key], obj) + } + + equals(value) { + return value != null + && is(ObjectPath, value) + && this.path.length === value.path.length + && super.equals(value) + && this.path.every((x, i) => x === value.path[i]) } } + class GraphObject { constructor(name, label, properties = []) { this.name = name @@ -65,6 +133,14 @@ export class ReturnValues { constructor(...args) { this.values = args } + + find(fn) { + return this.values.find(fn) + } + + includes(value) { + return this.find(value.equals.bind(value)) + } } export class KeyValuePair { @@ -74,36 +150,6 @@ export class KeyValuePair { } } -export class Literal { - constructor(value) { - this.value = value - } -} - -export class Identifier { - constructor(value) { - this.value = value - } - - get literal() { - return this.value - } -} - -export class Alias { - constructor(value, alias) { - this.value = value - this.alias = alias - } -} - -export class ObjectPath { - constructor(value, ...path) { - this.value = value - this.path = path - } -} - export class Query { constructor(use, match, returnValues) { this.use = use diff --git a/src/query.js b/src/query.js index 7387f9f..4b9ea27 100644 --- a/src/query.js +++ b/src/query.js @@ -1,7 +1,7 @@ import { defineQuery, hasComponent } from 'bitecs' import { query as q } from './query-parser/index.js' import { parseAll } from './parser.js' -import { curry, of, is, reduce, flip, concat, map, when, assoc, mergeLeft, isNil, path, prop, ifElse, pipe, always, assocPath, prepend, orDefault, applyTo, lens, over } from './fn.js' +import { curry, of, is, map, when, assoc, isNil, path, pipe, always, assocPath, prop } from './fn.js' import { Relationship } from './query-parser/types.js' const prepareQuery = (query, component) => { @@ -14,9 +14,9 @@ const prepareQuery = (query, component) => { return { from, to, edge, relationship, schema: query } } else { - const label = path('label.value.value', match) - const name = path('name.value.value', match) - const type = component[label] + const label = match.label.value + const name = match.name.value + const type = component[match.label.value] return { name, label, type, relationship, query: defineQuery([type]), schema: query } } @@ -57,7 +57,7 @@ const queryRelationship = (query, world) => { return result } -const assembleQuery = (query, entities) => entities.map(entity => ([{ entity, query }])) +const assembleQuery = (query, entities) => map(entity => ([{ entity, query }]), entities) const executeQuery = curry((query, world) => { if (!query.relationship) { @@ -84,26 +84,39 @@ const executeQuery = curry((query, world) => { } }) -const includes = curry((val, arr) => !isNil(val) && arr.includes(val)) - -const maybeAssoc = curry((pred, key, value, obj) => - when(pred, assoc(key, value), obj) +const isTag = type => ( + Object.getOwnPropertySymbols(type).find( + (s) => s.description === 'tagStore' + ) ) +const getValue = (type, entity) => { + if (isTag(type)) { + return {} + } else { + return Object.fromEntries( + Object.entries(type).map(([k, v]) => [k, v[entity]]) + ) + } +} + const resolveNode = curry((node, obj) => { - const name = path('query.schema.match.name.value.value', node) - const returns = path('query.schema.returnValues.values', node).map(r => r.value) - return maybeAssoc(always(includes(name, returns)), name, node.entity, obj) + const name = path('query.schema.match.name', node) + const returns = path('query.schema.returnValues', node) + const { entity, query: { type } } = node + const value = getValue(type, entity) + return returns.includes(name) ? { ...obj, [name.value]: value } : obj }) const resolveRelationship = edges => { const { entity, query } = edges const Edge = query.edge.type + const to = query.to?.from ?? query.to return pipe( resolveNode({ entity: Edge.from[entity], query: query.from }), resolveNode({ entity, query: query.edge }), - resolveNode({ entity: Edge.to[entity], query: query.to }), + resolveNode({ entity: Edge.to[entity], query: to }), )({}) } @@ -126,7 +139,8 @@ export const query = curry((input, { world, component }) => { const useWorld = world[use?.value ?? 'default'] const preparedQuery = prepareQuery(rest, component) const results = executeQuery(preparedQuery, useWorld) - return resolveReturns(results) + const returns = resolveReturns(results) + return returns }) }) diff --git a/tests/query.test.js b/tests/query.test.js index 8636992..eeeb972 100644 --- a/tests/query.test.js +++ b/tests/query.test.js @@ -2,14 +2,34 @@ import { beforeEach, describe, it } from 'node:test' import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs' import assert from './assert.js' import { query } from '../src/query.js' +import { identity } from '../src/fn.js' let engine = {} + +const create = (world, ...components) => { + const entity = addEntity(world) + components.forEach(cmp => addComponent(world, cmp, entity)) + return entity +} + +const relate = (world, a, type, ...b) => { + return b.map(v => { + const edge = addEntity(world) + addComponent(world, type, edge) + type.from[edge] = a + type.to[edge] = v + return edge + }) +} + describe('query', () => { beforeEach(() => { const world = { default: createWorld() } const component = { + Entity: identity, Player: defineComponent(null, 10), NPC: defineComponent(null, 10), + Health: defineComponent({ current: Types.ui8, max: Types.ui8 }), Knows: defineComponent({ from: Types.eid, to: Types.eid @@ -19,56 +39,54 @@ describe('query', () => { engine = { world, component } }) - it('i just need a test runner', () => { - const w = engine.world.default + it('should execute basic queries', () => { + const world = 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 player = create(world, Player) // 0 + const a = create(world, NPC) // 1 + const b = create(world, NPC) // 2 + const c = create(world, NPC) // 3 - const relate = (a, type, ...b) => { - return b.map(v => { - const edge = addEntity(w) - addComponent(w, type, edge) - type.from[edge] = a - type.to[edge] = v - return edge - }) - } - - const player = create(Player) // 0 - const a = create(NPC) // 1 - const b = create(NPC) // 2 - const c = create(NPC) // 3 - - relate(player, Knows, a, c) // 4, 5 - relate(a, Knows, player, b, c) // 6, 7, 8 - relate(b, Knows, a) // 9 - relate(c, Knows, player) // 10 + relate(world, player, Knows, a, c) // 4, 5 + relate(world, a, Knows, player, b, c) // 6, 7, 8 + relate(world, b, Knows, a) // 9 + relate(world, c, Knows, player) // 10 assert.deepEqual( query('MATCH (player:Player) RETURN player', engine).unwrap(), - [{ player: 0 }] + [{ player: {} }] ) assert.deepEqual( query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(), [ - { player: 0, e1: 4, a: 1 }, - { player: 0, e1: 5, a: 3 } + { player: {}, e1: { from: 0, to: 1 }, a: {} }, + { player: {}, e1: { from: 0, to: 3 }, a: {} } ] ) assert.deepEqual( query('MATCH (player:Player)-[e1:Knows]->(a:NPC)-[e2:Knows]->(b:NPC) RETURN player, e1, a, e2, b', engine).unwrap(), [ - { player: 0, e1: 4, a: 1, e2: 7, b: 2 }, - { player: 0, e1: 4, a: 1, e2: 8, b: 3 } + { player: {}, e1: { from: 0, to: 1 }, a: {}, e2: { from: 1, to: 2 }, b: {} }, + { player: {}, e1: { from: 0, to: 1 }, a: {}, e2: { from: 1, to: 3 }, b: {} } ] ) }) + + it('should match multiple components', () => { + const world = engine.world.default + const { Player, Health } = engine.component + const player = create(world, Player, Health) + + Health.max[player] = 50 + Health.current[player] = 25 + + assert.deepEqual( + query('MATCH (h:Health) return h.max', engine).unwrap(), + [{ h: { current: 25, max: 50 } }] + ) + }) })