From 84e5bb4c02aebb30d238f6a5370fd424b745f6bc Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Mon, 18 Nov 2024 19:10:29 -0600 Subject: [PATCH] finalize dsl parser for now --- src/fn.js | 4 ++ src/parser.js | 1 + src/query.js | 3 -- src/query/common.js | 2 +- src/query/index.js | 27 ++++++++----- src/query/match.js | 6 +-- tests/query/match.test.js | 1 - tests/query/query.test.js | 85 +++++++++++++++++++++++++++------------ tests/utils.js | 2 + 9 files changed, 86 insertions(+), 45 deletions(-) delete mode 100644 src/query.js diff --git a/src/fn.js b/src/fn.js index 9aae410..4491065 100644 --- a/src/fn.js +++ b/src/fn.js @@ -17,6 +17,8 @@ export const and = (...booleans) => booleans.every(identity) export const or = (...booleans) => booleans.some(identity) export const isOk = r => !is(Result, r) || r.isOk() +export const construct = Type => args => new Type(...args) + export const of = value => Array.isArray(value) ? value : [value] export const head = value => value[0] export const tail = value => value.slice(1) @@ -29,6 +31,8 @@ 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 prepend = curry((value, iter) => [value, ...iter]) +export const append = curry((value, iter) => [...iter, value]) export const identity = x => x diff --git a/src/parser.js b/src/parser.js index e5b355d..30b8e17 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,3 +1,4 @@ +import { inspect } from 'node:util' import { Result } from './result.js' import { Iterator, Stream } from './stream.js' import { curry, join, of } from './fn.js' diff --git a/src/query.js b/src/query.js deleted file mode 100644 index e98bc09..0000000 --- a/src/query.js +++ /dev/null @@ -1,3 +0,0 @@ -// MATCH (a:Label)-[e:Label]->(b:Label) -// RETURN a, b -// diff --git a/src/query/common.js b/src/query/common.js index 4e87775..c557177 100644 --- a/src/query/common.js +++ b/src/query/common.js @@ -36,7 +36,7 @@ export const collect = parser => map(join(''), parser) export const quoted = collect(seq(skip(Symbol.Quote), until(Symbol.Quote), skip(Symbol.Quote))) export const number = seq(digit, many(digit)) export const signed = seq(maybe(any(Symbol.Plus, Symbol.Hyphen)), number) -export const ws = skip(many(Symbol.Space)) +export const ws = skip(many(any(Symbol.Newline, Symbol.Space))) export const identifier = map(([x]) => new Identifier(x), collect(seq(alpha, many(alphanumeric)))) const float = map(([n]) => parseFloat(n, 10), collect(seq(signed, Symbol.Period, number))) diff --git a/src/query/index.js b/src/query/index.js index 4c29655..3517a2b 100644 --- a/src/query/index.js +++ b/src/query/index.js @@ -1,25 +1,30 @@ -import { head, is } from '../fn.js' +import { construct, head, ifElse, is, pipe, prepend } from '../fn.js' import { map, maybe, seq } from '../parser.js' +import { word } from './common.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) - } -} +const hasUseClause = pipe( + head, + is(SelectedGraph) +) + +const constructQuery = construct(Query) + +const collect = ifElse( + hasUseClause, + constructQuery, + pipe(prepend(undefined), constructQuery) +) export const query = map( collect, seq( maybe(useClause), - matchClause, - returnClause + word(matchClause), + word(returnClause) ) ) diff --git a/src/query/match.js b/src/query/match.js index 4f8a50f..2a80521 100644 --- a/src/query/match.js +++ b/src/query/match.js @@ -1,12 +1,12 @@ import { identifier, literal, Symbol, ws } from './common.js' import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship } from './types.js' -import { curry, filter, is } from '../fn.js' +import { construct, 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 const name = map( - ([x]) => new Name(x), + construct(Name), identifier ) @@ -21,7 +21,7 @@ const bracketed = curry((value, { Left, Right }, state) => ( )) const kvp = map( - ([k, v]) => new KeyValuePair(k, v), + construct(KeyValuePair), separated(name, trim(Colon), literal) ) export const kvps = list(trim(Comma), kvp) diff --git a/tests/query/match.test.js b/tests/query/match.test.js index ce7ab0a..e239bc7 100644 --- a/tests/query/match.test.js +++ b/tests/query/match.test.js @@ -1,4 +1,3 @@ -import util from 'node:util' import { describe, it } from 'node:test' import assert from '../assert.js' import { node, edge, matchClause } from '../../src/query/match.js' diff --git a/tests/query/query.test.js b/tests/query/query.test.js index 217671f..fa9a250 100644 --- a/tests/query/query.test.js +++ b/tests/query/query.test.js @@ -1,40 +1,73 @@ 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' +import { Alias, Identifier, ObjectPath, Query, ReturnValues, SelectedGraph } from '../../src/query/types.js' +import { makeNode, makeRelationship, makeRightEdge } from '../utils.js' -const i = n => new Identifier(n) -const rv = (...v) => new ReturnValues(...v) +const path = (...v) => new ObjectPath(...v) +const alias = (...v) => new Alias(...v) +const identifier = n => new Identifier(n) +const graph = n => new SelectedGraph(identifier(n)) +const returnVals = (...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')) - // ) + it('should match a node', () => { + assert.parseOk(query, 'MATCH (node:Label) RETURN node', ([actual]) => { + const expected = q( + undefined, // no use clause + makeNode('node', 'Label'), + returnVals(identifier('node')) + ) - // assert.deepEqual(actual, expected) - // }) - //}) + 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.parseOk(query, 'MATCH (rown:Creature)-[:Petpats]->(kbity:NetCat) RETURN rown, kbity', ([actual]) => { + const expected = q( + undefined, // no use clause + makeRelationship( + makeNode('rown', 'Creature'), + makeRightEdge(undefined, 'Petpats'), + makeNode('kbity', 'NetCat'), + ), + returnVals(identifier('rown'), identifier('kbity')) + ) - //assert.deepEqual(actual, expected) + assert.deepEqual(actual, expected) + }) + }) + + it('should support the USE clause', () => { + assert.parseOk(query, 'USE aminals MATCH (kbity:Cat) RETURN kbity', ([actual]) => { + const expected = q( + graph('aminals'), + makeNode('kbity', 'Cat'), + returnVals(identifier('kbity')) + ) + + assert.deepEqual(actual, expected) + }) + }) + + it('should support complex queries', () => { + assert.parseOk(query, 'USE aminals MATCH (kbity:Cat { type: "cute" })-[:Eats { schedule: "daily" }]->(snacc:Feesh { type: "vegan", weight: 0.7 }) RETURN kbity.name AS name, snacc AS food', ([actual]) => { + const expected = q( + graph('aminals'), + makeRelationship( + makeNode('kbity', 'Cat', [['type', 'cute']]), + makeRightEdge(undefined, 'Eats', [['schedule', 'daily']]), + makeNode('snacc', 'Feesh', [['type', 'vegan'], ['weight', 0.7]]) + ), + returnVals( + alias(path(identifier('kbity'), identifier('name')), identifier('name')), + alias(identifier('snacc'), identifier('food')) + ) + ) + + assert.deepEqual(actual, expected) }) }) }) diff --git a/tests/utils.js b/tests/utils.js index ab93cf4..69ca0ec 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -11,5 +11,7 @@ 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 makeLeftEdge = (name, label, props = []) => DirectedEdge.fromEdge(makeEdge(name, label, props), 0) +export const makeRightEdge = (name, label, props = []) => DirectedEdge.fromEdge(makeEdge(name, label, props), 1) export const makeRelationship = (from, edge, to) => new Relationship(from, edge, to)