From bab47f8a9543648d61424ac2f3eaaca48a2835e1 Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Mon, 18 Nov 2024 16:06:58 -0600 Subject: [PATCH] add relationship and query datatypes --- src/fn.js | 1 + src/query/index.js | 25 +++++++++++++++++++ src/query/match.js | 31 ++++++++++++++++++++---- src/query/return.js | 2 +- src/query/types.js | 21 ++++++++++++++-- src/query/use.js | 4 ++-- tests/query/match.test.js | 49 ++++++++++++++++++++------------------ tests/query/query.test.js | 40 +++++++++++++++++++++++++++++++ tests/query/return.test.js | 16 ++++++------- tests/query/use.test.js | 6 ++--- tests/utils.js | 3 ++- 11 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 src/query/index.js create mode 100644 tests/query/query.test.js diff --git a/src/fn.js b/src/fn.js index fbb38fd..9aae410 100644 --- a/src/fn.js +++ b/src/fn.js @@ -27,6 +27,7 @@ export const map = curry((fn, v) => v.map(fn)) export const filter = curry((fn, v) => v.filter(fn)) export const find = curry((fn, v) => v.find(fn)) export const join = curry((sep, v) => v.join(sep)) +export const rev = v => v.slice().reverse() export const pipe = (...f) => v => f.reduce(apply, v) export const identity = x => x diff --git a/src/query/index.js b/src/query/index.js new file mode 100644 index 0000000..4c29655 --- /dev/null +++ b/src/query/index.js @@ -0,0 +1,25 @@ +import { head, is } from '../fn.js' +import { map, maybe, seq } from '../parser.js' +import { matchClause } from './match.js' +import { returnClause } from './return.js' +import { Query, SelectedGraph } from './types.js' +import { useClause } from './use.js' + +const collect = args => { + return args + if (is(SelectedGraph, head(args))) { + return new Query(...args) + } else { + return new Query(undefined, ...args) + } +} + +export const query = map( + collect, + seq( + maybe(useClause), + matchClause, + returnClause + ) +) + diff --git a/src/query/match.js b/src/query/match.js index f9e7eb9..4f8a50f 100644 --- a/src/query/match.js +++ b/src/query/match.js @@ -1,6 +1,6 @@ import { identifier, literal, Symbol, ws } from './common.js' -import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge } from './types.js' -import { curry, is } from '../fn.js' +import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship } from './types.js' +import { curry, filter, is } from '../fn.js' import { many, maybe, map, seq, skip, between, noCaseString, separated, list, any } from '../parser.js' const { Bracket, Colon, Comma, Hyphen } = Symbol @@ -51,14 +51,37 @@ const relationshipRight = map(([edge, direction]) => DirectedEdge.fromEdge(edge, direction), seq(skip(Hyphen), edge, arrowRight) ) + const relationshipLeft = map(([direction, edge]) => DirectedEdge.fromEdge(edge, direction), seq(arrowLeft, edge, skip(Hyphen)) ) + const relationship = seq(any(relationshipRight, relationshipLeft), node) +const makeRelationship = args => { + const len = args.length - 1 + + if (len <= 0) { + return args + } + + let right = args[len] + + for (let i = len; i > 0; i -= 2) { + const edge = args[i - 1] + const left = args[i - 2] + right = new Relationship(left, edge, right) + } + + return right +} + const keyword = noCaseString('match') -const params = seq(node, many(relationship)) +const params = map( + makeRelationship, + seq(node, many(relationship)) +) -export const statement = seq(skip(keyword), ws, params) +export const matchClause = seq(skip(keyword), ws, params) diff --git a/src/query/return.js b/src/query/return.js index 4633c79..9869323 100644 --- a/src/query/return.js +++ b/src/query/return.js @@ -9,5 +9,5 @@ const params = map( seq(list(seq(ws, Symbol.Comma, ws), value)) ) -export const statement = seq(skip(keyword), ws, params) +export const returnClause = seq(skip(keyword), ws, params) diff --git a/src/query/types.js b/src/query/types.js index b672123..28debcd 100644 --- a/src/query/types.js +++ b/src/query/types.js @@ -52,9 +52,18 @@ export class DirectedEdge extends Edge { } } -export class ReturnValues extends Array { +export class Relationship { + constructor(left, edge, right) { + const [from, to] = edge.direction === Direction.Right ? [left, right] : [right, left] + this.from = from + this.to = to + this.edge = edge + } +} + +export class ReturnValues { constructor(...args) { - super(...args) + this.values = args } } @@ -90,3 +99,11 @@ export class ObjectPath { this.path = path } } + +export class Query { + constructor(use, match, returnValues) { + this.use = use + this.match = match + this.returnValues = returnValues + } +} diff --git a/src/query/use.js b/src/query/use.js index 67a3b9f..ad96d18 100644 --- a/src/query/use.js +++ b/src/query/use.js @@ -1,10 +1,10 @@ import { identifier, ws } from './common.js' -import { map, noCaseString, parse, seq, skip } from '../parser.js' +import { map, noCaseString, seq, skip } from '../parser.js' import { SelectedGraph } from './types.js' const keyword = noCaseString('use') -export const statement = map( +export const useClause = map( ([graph]) => new SelectedGraph(graph), seq(skip(keyword), ws, identifier) ) diff --git a/tests/query/match.test.js b/tests/query/match.test.js index 1da9446..ce7ab0a 100644 --- a/tests/query/match.test.js +++ b/tests/query/match.test.js @@ -1,7 +1,8 @@ +import util from 'node:util' import { describe, it } from 'node:test' import assert from '../assert.js' -import { node, edge, statement } from '../../src/query/match.js' -import { makeDirectedEdge, makeEdge, makeNode } from '../utils.js' +import { node, edge, matchClause } from '../../src/query/match.js' +import { makeDirectedEdge, makeEdge, makeNode, makeRelationship } from '../utils.js' describe('node', () => { it('should match a node with a name, label, and properties', () => { @@ -68,73 +69,75 @@ describe('edge', () => { describe('MATCH keyword', () => { it('should match a single node with no relationships', () => { - assert.parseOk(statement, 'MATCH (:Label)', ([actual]) => { + assert.parseOk(matchClause, 'MATCH (:Label)', ([actual]) => { const expected = makeNode(undefined, 'Label') assert.deepEqual(actual, expected) }) - assert.parseOk(statement, 'MATCH (node:Label)', ([actual]) => { + assert.parseOk(matchClause, 'MATCH (node:Label)', ([actual]) => { const expected = makeNode('node', 'Label') assert.deepEqual(actual, expected) }) - assert.parseOk(statement, 'MATCH (node:Label { prop: true, value: "test" })', ([actual]) => { + assert.parseOk(matchClause, 'MATCH (node:Label { prop: true, value: "test" })', ([actual]) => { const expected = makeNode('node', 'Label', [['prop', true], ['value', 'test']]) assert.deepEqual(actual, expected) }) }) it('should match nodes with a relationship to another another node', () => { - assert.parseOk(statement, 'MATCH (:Node)-[:Edge]->(:Node)', actual => { - const expected = [ + assert.parseOk(matchClause, 'MATCH (:Node)-[:Edge]->(:Node)', ([actual]) => { + const expected = makeRelationship( makeNode(undefined, 'Node'), makeDirectedEdge(undefined, 'Edge', 1), makeNode(undefined, 'Node'), - ] + ) assert.deepEqual(actual, expected) }) - assert.parseOk(statement, 'MATCH (a:Node)-[e:Edge]->(b:Node)', actual => { - const expected = [ + assert.parseOk(matchClause, 'MATCH (a:Node)-[e:Edge]->(b:Node)', ([actual]) => { + const expected = makeRelationship( makeNode('a', 'Node'), makeDirectedEdge('e', 'Edge', 1), makeNode('b', 'Node'), - ] + ) assert.deepEqual(actual, expected) }) - assert.parseOk(statement, 'MATCH (a:Node { db: 0.7 })-[e:Edge]->(b:Node { db: 0.95 })', actual => { - const expected = [ + assert.parseOk(matchClause, 'MATCH (a:Node { db: 0.7 })-[e:Edge]->(b:Node { db: 0.95 })', ([actual]) => { + const expected = makeRelationship( makeNode('a', 'Node', [['db', 0.7]]), makeDirectedEdge('e', 'Edge', 1), makeNode('b', 'Node', [['db', 0.95]]), - ] + ) assert.deepEqual(actual, expected) }) - assert.parseOk(statement, 'MATCH (:Node { db: 0.7 })-[:Edge]->(:Node { db: 0.95 })', actual => { - const expected = [ + assert.parseOk(matchClause, 'MATCH (:Node { db: 0.7 })-[:Edge]->(:Node { db: 0.95 })', ([actual]) => { + const expected = makeRelationship( makeNode(undefined, 'Node', [['db', 0.7]]), makeDirectedEdge(undefined, 'Edge', 1), makeNode(undefined, 'Node', [['db', 0.95]]), - ] + ) assert.deepEqual(actual, expected) }) }) it('should handle multiple relationships', () => { - assert.parseOk(statement, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', actual => { - const expected = [ + assert.parseOk(matchClause, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', ([actual]) => { + const expected = makeRelationship( makeNode('player', 'Player'), makeDirectedEdge(undefined, 'Knows', 1), - makeNode('a', 'NPC'), - makeDirectedEdge(undefined, 'Knows', 1), - makeNode('b', 'NPC'), - ] + makeRelationship( + makeNode('a', 'NPC'), + makeDirectedEdge(undefined, 'Knows', 1), + makeNode('b', 'NPC'), + ) + ) assert.deepEqual(actual, expected) }) diff --git a/tests/query/query.test.js b/tests/query/query.test.js new file mode 100644 index 0000000..217671f --- /dev/null +++ b/tests/query/query.test.js @@ -0,0 +1,40 @@ +import { describe, it } from 'node:test' +import assert from '../assert.js' +import { query } from '../../src/query/index.js' +import { Identifier, Query, ReturnValues } from '../../src/query/types.js' +import { makeEdge, makeNode } from '../utils.js' + +const i = n => new Identifier(n) +const rv = (...v) => new ReturnValues(...v) +const q = (u, m, rv) => new Query(u, m, rv) + +describe('query', () => { + //it('should match a node', () => { + // assert.parseOk(query, 'MATCH (node:Label) RETURN node', ([actual]) => { + // const expected = q( + // undefined, // no use clause + // makeNode('node', 'Label'), + // rv(i('node')) + // ) + + // assert.deepEqual(actual, expected) + // }) + //}) + + it('should match a relationship', () => { + assert.parseOk(query, 'MATCH (rown:Creature)-[:PETPATS]->(kbity:NetCat) RETURN rown, kbity', (actual) => { + console.log(actual) + //const expected = q( + // undefined, // no use clause + // [ + // makeNode('rown', 'Creature'), + // makeEdge(undefined, 'PETPATS'), + // makeNode('kbity', 'NetCat'), + // ], + // rv(i('rown'), i('kbity')) + //) + + //assert.deepEqual(actual, expected) + }) + }) +}) diff --git a/tests/query/return.test.js b/tests/query/return.test.js index 21217c6..285f922 100644 --- a/tests/query/return.test.js +++ b/tests/query/return.test.js @@ -1,27 +1,27 @@ import { describe, it } from 'node:test' import assert from '../assert.js' -import { statement } from '../../src/query/return.js' -import { Alias, Identifier, Literal } from '../../src/query/types.js' +import { returnClause } from '../../src/query/return.js' +import { Alias, Identifier, Literal, ReturnValues } from '../../src/query/types.js' describe('RETURN keyword', () => { it('should collect a single value for a query to return', () => { - assert.parseOk(statement, 'RETURN folklore AS f', ([actual]) => { - assert.deepEqual(actual, new Alias(new Identifier('folklore'), new Identifier('f'))) + assert.parseOk(returnClause, 'RETURN folklore AS f', ([actual]) => { + assert.deepEqual(actual, new ReturnValues(new Alias(new Identifier('folklore'), new Identifier('f')))) }) }) it('should collect multiple values for a query to return', () => { - assert.parseOk(statement, 'RETURN sybil, mercury, rowan', actual => { - assert.deepStrictEqual(actual, ['sybil', 'mercury', 'rowan'].map(x => new Identifier(x))) + assert.parseOk(returnClause, 'RETURN sybil, mercury, rowan', ([actual]) => { + assert.deepStrictEqual(actual, new ReturnValues(...['sybil', 'mercury', 'rowan'].map(x => new Identifier(x)))) }) }) it('should accept mixed values and aliases', () => { - assert.parseOk(statement, 'RETURN 19589 AS sybil, mercury AS vex, rowan', actual => { + assert.parseOk(returnClause, 'RETURN 19589 AS sybil, mercury AS vex, rowan', ([actual]) => { const sybil = new Alias(new Literal(19589), new Identifier('sybil')) const mercury = new Alias(new Identifier('mercury'), new Identifier('vex')) const rowan = new Identifier('rowan') - assert.deepStrictEqual(actual, [sybil, mercury, rowan]) + assert.deepStrictEqual(actual, new ReturnValues(...[sybil, mercury, rowan])) }) }) }) diff --git a/tests/query/use.test.js b/tests/query/use.test.js index 0e7b3a8..81bc543 100644 --- a/tests/query/use.test.js +++ b/tests/query/use.test.js @@ -1,17 +1,17 @@ import { describe, it } from 'node:test' import assert from '../assert.js' -import { statement } from '../../src/query/use.js' +import { useClause } from '../../src/query/use.js' import { Identifier } from '../../src/query/types.js' describe('USE keyword', () => { it('should select a graph to query', () => { - assert.parseOk(statement, 'USE default', ([actual]) => { + assert.parseOk(useClause, 'USE default', ([actual]) => { assert.deepEqual(actual.identifier, new Identifier('default')) }) }) it('should return an error if no graph identifier is provided', () => { - assert.parseErr(statement, 'USE') + assert.parseErr(useClause, 'USE') }) }) diff --git a/tests/utils.js b/tests/utils.js index 1839b8e..ab93cf4 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,4 +1,4 @@ -import { KeyValuePair, Name, Label, Identifier, Literal, Edge, Node, DirectedEdge } from '../src/query/types.js' +import { KeyValuePair, Name, Label, Identifier, Literal, Edge, Node, DirectedEdge, Relationship } from '../src/query/types.js' export const keyValuePair = ([k, v]) => new KeyValuePair(new Name(new Identifier(k)), new Literal(v)) @@ -11,4 +11,5 @@ export const graphObject = (name, label, props = [], Type = Node) => new Type( export const makeNode = graphObject export const makeEdge = (name, label, props = []) => graphObject(name, label, props, Edge) export const makeDirectedEdge = (name, label, direction, props = []) => DirectedEdge.fromEdge(makeEdge(name, label, props), direction) +export const makeRelationship = (from, edge, to) => new Relationship(from, edge, to)