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 isArray = Array.isArray
export const isNil = v => !v export const isNil = v => !v
export const is = curry((type, value) => value instanceof type) export const is = curry((type, value) => value instanceof type)
export const isString = value => is(String, value) || typeof value === 'string'
// branching // branching
export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v)) 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) { constructor(value) {
this.value = value this.value = value
} }
} }
export class Label {
constructor(value) { export class Alias extends Identifier {
this.value = value constructor(value, alias) {
super(value)
this.alias = alias
} }
} }
export class SelectedGraph { export class ObjectPath extends Identifier {
constructor(identifier) { constructor(value, ...path) {
this.identifier = identifier 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 { class GraphObject {
constructor(name, label, properties = []) { constructor(name, label, properties = []) {
this.name = name this.name = name
@ -65,6 +133,14 @@ export class ReturnValues {
constructor(...args) { constructor(...args) {
this.values = args this.values = args
} }
find(fn) {
return this.values.find(fn)
}
includes(value) {
return this.find(value.equals.bind(value))
}
} }
export class KeyValuePair { 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 { export class Query {
constructor(use, match, returnValues) { constructor(use, match, returnValues) {
this.use = use this.use = use

View file

@ -1,7 +1,7 @@
import { defineQuery, hasComponent } from 'bitecs' import { defineQuery, hasComponent } from 'bitecs'
import { query as q } from './query-parser/index.js' import { query as q } from './query-parser/index.js'
import { parseAll } from './parser.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' import { Relationship } from './query-parser/types.js'
const prepareQuery = (query, component) => { const prepareQuery = (query, component) => {
@ -14,9 +14,9 @@ const prepareQuery = (query, component) => {
return { from, to, edge, relationship, schema: query } return { from, to, edge, relationship, schema: query }
} else { } else {
const label = path('label.value.value', match) const label = match.label.value
const name = path('name.value.value', match) const name = match.name.value
const type = component[label] const type = component[match.label.value]
return { name, label, type, relationship, query: defineQuery([type]), schema: query } return { name, label, type, relationship, query: defineQuery([type]), schema: query }
} }
@ -57,7 +57,7 @@ const queryRelationship = (query, world) => {
return result 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) => { const executeQuery = curry((query, world) => {
if (!query.relationship) { if (!query.relationship) {
@ -84,26 +84,39 @@ const executeQuery = curry((query, world) => {
} }
}) })
const includes = curry((val, arr) => !isNil(val) && arr.includes(val)) const isTag = type => (
Object.getOwnPropertySymbols(type).find(
const maybeAssoc = curry((pred, key, value, obj) => (s) => s.description === 'tagStore'
when(pred, assoc(key, value), obj) )
) )
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 resolveNode = curry((node, obj) => {
const name = path('query.schema.match.name.value.value', node) const name = path('query.schema.match.name', node)
const returns = path('query.schema.returnValues.values', node).map(r => r.value) const returns = path('query.schema.returnValues', node)
return maybeAssoc(always(includes(name, returns)), name, node.entity, obj) const { entity, query: { type } } = node
const value = getValue(type, entity)
return returns.includes(name) ? { ...obj, [name.value]: value } : obj
}) })
const resolveRelationship = edges => { const resolveRelationship = edges => {
const { entity, query } = edges const { entity, query } = edges
const Edge = query.edge.type const Edge = query.edge.type
const to = query.to?.from ?? query.to
return pipe( return pipe(
resolveNode({ entity: Edge.from[entity], query: query.from }), resolveNode({ entity: Edge.from[entity], query: query.from }),
resolveNode({ entity, query: query.edge }), 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 useWorld = world[use?.value ?? 'default']
const preparedQuery = prepareQuery(rest, component) const preparedQuery = prepareQuery(rest, component)
const results = executeQuery(preparedQuery, useWorld) 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 { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
import assert from './assert.js' import assert from './assert.js'
import { query } from '../src/query.js' import { query } from '../src/query.js'
import { identity } from '../src/fn.js'
let engine = {} 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', () => { describe('query', () => {
beforeEach(() => { beforeEach(() => {
const world = { default: createWorld() } const world = { default: createWorld() }
const component = { const component = {
Entity: identity,
Player: defineComponent(null, 10), Player: defineComponent(null, 10),
NPC: defineComponent(null, 10), NPC: defineComponent(null, 10),
Health: defineComponent({ current: Types.ui8, max: Types.ui8 }),
Knows: defineComponent({ Knows: defineComponent({
from: Types.eid, from: Types.eid,
to: Types.eid to: Types.eid
@ -19,56 +39,54 @@ describe('query', () => {
engine = { world, component } engine = { world, component }
}) })
it('i just need a test runner', () => { it('should execute basic queries', () => {
const w = engine.world.default const world = engine.world.default
const { Player, NPC, Knows } = engine.component const { Player, NPC, Knows } = engine.component
const create = (...components) => { const player = create(world, Player) // 0
const eid = addEntity(w) const a = create(world, NPC) // 1
components.forEach(cmp => addComponent(w, cmp, eid)) const b = create(world, NPC) // 2
return eid const c = create(world, NPC) // 3
}
const relate = (a, type, ...b) => { relate(world, player, Knows, a, c) // 4, 5
return b.map(v => { relate(world, a, Knows, player, b, c) // 6, 7, 8
const edge = addEntity(w) relate(world, b, Knows, a) // 9
addComponent(w, type, edge) relate(world, c, Knows, player) // 10
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
assert.deepEqual( assert.deepEqual(
query('MATCH (player:Player) RETURN player', engine).unwrap(), query('MATCH (player:Player) RETURN player', engine).unwrap(),
[{ player: 0 }] [{ player: {} }]
) )
assert.deepEqual( assert.deepEqual(
query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(), query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(),
[ [
{ player: 0, e1: 4, a: 1 }, { player: {}, e1: { from: 0, to: 1 }, a: {} },
{ player: 0, e1: 5, a: 3 } { player: {}, e1: { from: 0, to: 3 }, a: {} }
] ]
) )
assert.deepEqual( assert.deepEqual(
query('MATCH (player:Player)-[e1:Knows]->(a:NPC)-[e2:Knows]->(b:NPC) RETURN player, e1, a, e2, b', engine).unwrap(), 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: {}, e1: { from: 0, to: 1 }, a: {}, e2: { from: 1, to: 2 }, b: {} },
{ player: 0, e1: 4, a: 1, e2: 8, b: 3 } { 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 } }]
)
})
}) })