From 0a1524644df707447e9ed73a54a590159096cd28 Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 30 Nov 2024 04:53:00 -0600 Subject: [PATCH] propertiesgit status :3 --- src/fn.js | 1 + src/query-parser/match.js | 6 ++-- src/query-parser/types.js | 12 ++++++++ src/query.js | 59 ++++++++++++++++++++++++++++----------- tests/query.test.js | 58 +++++++++++++++++++++++++++++--------- 5 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/fn.js b/src/fn.js index 69a4647..03d519b 100644 --- a/src/fn.js +++ b/src/fn.js @@ -57,6 +57,7 @@ export const all = curry((predicates, v) => every(applyTo(v), predicates)) export const any = curry((predicates, v) => some(applyTo(v), predicates)) export const isOk = value => !is(Result, value) || value.isOk() export const eq = curry((a, b) => a === b) + // classes export const construct = Type => args => new Type(...args) diff --git a/src/query-parser/match.js b/src/query-parser/match.js index aada8a7..567d553 100644 --- a/src/query-parser/match.js +++ b/src/query-parser/match.js @@ -1,7 +1,7 @@ import { identifier, Keyword, literal, Symbols, ws } from './common.js' -import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component, Match } from './types.js' +import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component, Match, Properties } from './types.js' import { construct, curry, head, is } from '../fn.js' -import { many, maybe, map, seq, skip, between, separated, list, any } from '../parser.js' +import { many, maybe, map, seq, skip, between, separated, list, any, char, noCaseString, anyChar } from '../parser.js' const { Bracket, Colon, Comma, Hyphen } = Symbols @@ -36,7 +36,7 @@ const makeObj = Type => params => { return new Type(name, label, properties) } -const component = map(makeObj(Component), seq(id, maybe(properties))) +const component = map(makeObj(Component), seq(id, ws, maybe(properties))) const components = list(trim(Comma), component) export const node = map( diff --git a/src/query-parser/types.js b/src/query-parser/types.js index 5ee8c6f..9f85828 100644 --- a/src/query-parser/types.js +++ b/src/query-parser/types.js @@ -368,6 +368,18 @@ export class KeyValuePair { this.key = key this.value = value } + + valueOf() { + return { [this.key.valueOf()]: this.value.valueOf() } + } +} + +export class Properties { + constructor(kvps) { + kvps.forEach(e => { + this[e.key] = e.value + }) + } } export class Match { diff --git a/src/query.js b/src/query.js index 354b425..76a9552 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, map, always, assocPath, prop, identity, mergeRightDeep, reduce } from './fn.js' +import { curry, of, is, map, always, assocPath, prop, identity, mergeRightDeep, pipe, filter, some, path, tap } from './fn.js' import { Relationship } from './query-parser/types.js' const nodeComponent = component => node => { @@ -9,11 +9,34 @@ const nodeComponent = component => node => { const name = node.name?.valueOf() const type = label && component[label] || identity - return { name, label, type } + return { name, label, type, properties: node.properties } } const notEntity = ({ label }) => label != null && label.valueOf() !== 'Entity' +const definePropertyQuery = components => { + const query = pipe( + filter(notEntity), + map(prop('type')), + defineQuery + )(components) + + if (some(c => c.properties.length > 0, components)) { + return world => { + const comps = filter(c => c.properties.length > 0, components) + return query(world).filter(eid => + comps.every(cmp => + cmp.properties.every(({ key, value }) => + cmp.type[key][eid] === value.valueOf() + ) + ) + ) + } + } else { + return query + } +} + const prepareQuery = (match, component) => { const relationship = is(Relationship, match) @@ -24,8 +47,9 @@ const prepareQuery = (match, component) => { return { from, to, edge, relationship, schema: query } } else { const components = match.components.map(nodeComponent(component)) - const types = components.filter(notEntity).map(prop('type')) - return { components, relationship, query: defineQuery(types), schema: match } + // const types = components.filter(notEntity).map(prop('type')) + const query = definePropertyQuery(components) + return { components, relationship, query, schema: match } } } @@ -33,14 +57,17 @@ const hasComponents = (world, types, entity) => ( types.every(type => hasComponent(world, type, entity)) ) -const matches = (world, types, results, set = new Set()) => entity => { - if (set.has(entity)) { - return true - } else if (hasComponents(world, types, entity) && (results == null || results.includes(entity))) { - set.add(entity) - return true - } else { - return false +const matches = (world, node, results, set = new Set()) => { + const types = node.components.filter(notEntity).map(prop('type')) + return entity => { + if (set.has(entity)) { + return true + } else if (hasComponents(world, types, entity) && (results == null || results.includes(entity))) { + set.add(entity) + return true + } else { + return false + } } } @@ -50,17 +77,15 @@ const queryRelationship = (query, world) => { return node.relationship ? node.from : node }) - const [fc, tc] = [from, to].map(n => n.components.filter(notEntity).map(prop('type'))) - const matchesFrom = matches(world, fc, from.query(world)) - const matchesTo = matches(world, tc) + //const [fc, tc] = [from, to].map(n => n.components.filter(notEntity).map(prop('type'))) + const matchesFrom = matches(world, from, from.query(world)) + const matchesTo = matches(world, to) const Edge = edge.components[0].type const edges = edge.query(world) const result = [] for (let i = 0; i < edges.length; i++) { const eid = edges[i] - // console.log(eid, Edge.from[eid], '->', Edge.to[eid], matchesFrom(Edge.from[eid]), matchesTo(Edge.to[eid])) - if (matchesFrom(Edge.from[eid]) && matchesTo(Edge.to[eid])) { result.push(eid) } diff --git a/tests/query.test.js b/tests/query.test.js index 80e90f4..859114a 100644 --- a/tests/query.test.js +++ b/tests/query.test.js @@ -70,16 +70,16 @@ describe('query', () => { assert.deepEqual( query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(), [ - { player: {}, e1: { from: 0, to: 1 }, a: {} }, - { player: {}, e1: { from: 0, to: 3 }, a: {} } + { player: {}, e1: { from: player, to: a }, a: {} }, + { player: {}, e1: { from: player, to: c }, a: {} } ] ) assert.deepEqual( query('MATCH (player:Player)-[e1:Knows]->(a:NPC)-[e2:Knows]->(b:NPC) RETURN player, e1, a, e2, b', engine).unwrap(), [ - { 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: {} } + { player: {}, e1: { from: player, to: a }, a: {}, e2: { from: a, to: b }, b: {} }, + { player: {}, e1: { from: player, to: a }, a: {}, e2: { from: a, to: c }, b: {} } ] ) }) @@ -95,12 +95,12 @@ describe('query', () => { // an unspecified component should return an Entity assert.deepEqual( query('MATCH (e, h:Health) RETURN e, h.max', engine).unwrap(), - [{ e: 11, h: { max: 50 } }] + [{ e: player, h: { max: 50 } }] ) assert.deepEqual( query('MATCH (e:Entity, h:Health) RETURN e, h.max', engine).unwrap(), - [{ e: 11, h: { max: 50 } }] + [{ e: player, h: { max: 50 } }] ) }) @@ -112,29 +112,29 @@ describe('query', () => { update(Health, { max: 50, current: 25 }, player) update(Health, { max: 50, current: 35 }, enemy) - const [edge] = relate(world, player, Damaged, enemy) + const [edge] = relate(world, player, Damaged, enemy) // 14 update(Damaged, { damage: 10 }, edge) assert.deepEqual( query('MATCH (e, h:Health) WHERE h.current < 30 RETURN e, h.max', engine).unwrap(), - [{ e: 12, h: { max: 50 } }] + [{ e: player, h: { max: 50 } }] ) assert.deepEqual( query('MATCH (e, h:Health) WHERE e = 13 AND h.max = 50 OR h.current < 25 RETURN e, h.max', engine).unwrap(), - [{ e: 13, h: { max: 50 } }] + [{ e: enemy, h: { max: 50 } }] ) assert.deepEqual( query('MATCH (e, h:Health) WHERE h.max = 50 OR h.current > 45 RETURN e, h.max AS maxHealth', engine).unwrap(), [ - { e: 12, maxHealth: 50 }, - { e: 13, maxHealth: 50 } + { e: player, maxHealth: 50 }, + { e: enemy, maxHealth: 50 } ] ) assert.deepEqual( query('MATCH (e, :Player, h:Health) WHERE (h.max = 50 OR h.current > 45) AND e = 12 RETURN e, h.max AS maxHealth', engine).unwrap(), - [{ e: 12, maxHealth: 50 }] + [{ e: player, maxHealth: 50 }] ) assert.deepEqual( @@ -145,7 +145,39 @@ describe('query', () => { update(Damaged, { damage: 50 }, edge) assert.deepEqual( query('MATCH (n)-[d:Damaged]->(e, h:Health) WHERE d.damage > h.current AND h.current > 0 RETURN e', engine).unwrap(), - [{ e: 13 }] + [{ e: enemy }] + ) + }) + + it('properties should match', () => { + const world = engine.world.default + const { Player, Health, Damaged } = engine.component + const player = create(world, Player, Health) + const enemy = create(world, Health) + + update(Health, { max: 50, current: 25 }, player) + update(Health, { max: 50, current: 35 }, enemy) + const [edge] = relate(world, player, Damaged, enemy) // 14 + update(Damaged, { damage: 10 }, edge) + + assert.deepEqual( + query('MATCH (e, h:Health { current: 35 }) RETURN e', engine).unwrap(), + [{ e: enemy }] + ) + + assert.deepEqual( + query('MATCH (e, h:Health { max: 50 }) RETURN e', engine).unwrap(), + [{ e: player }, { e: enemy }] + ) + + assert.deepEqual( + query('MATCH (e, h:Health { current: 35, max: 50 }) RETURN e', engine).unwrap(), + [{ e: enemy }] + ) + + assert.deepEqual( + query('MATCH (a)-[:Damaged { damage: 10 }]->(b) RETURN a, b', engine).unwrap(), + [{ a: player, b: enemy }] ) }) })