resolve values

This commit is contained in:
Rowan 2024-11-24 22:27:08 -06:00
parent efe4971ccc
commit 3f1686d084
4 changed files with 161 additions and 82 deletions

View file

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

View file

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

View file

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

View file

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