add relationship and query datatypes

This commit is contained in:
Rowan 2024-11-18 16:06:58 -06:00
parent 6dc2c1f6db
commit bab47f8a95
11 changed files with 154 additions and 44 deletions

View file

@ -27,6 +27,7 @@ export const map = curry((fn, v) => v.map(fn))
export const filter = curry((fn, v) => v.filter(fn)) export const filter = curry((fn, v) => v.filter(fn))
export const find = curry((fn, v) => v.find(fn)) export const find = curry((fn, v) => v.find(fn))
export const join = curry((sep, v) => v.join(sep)) 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 pipe = (...f) => v => f.reduce(apply, v)
export const identity = x => x export const identity = x => x

25
src/query/index.js Normal file
View file

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

View file

@ -1,6 +1,6 @@
import { identifier, literal, Symbol, ws } from './common.js' import { identifier, literal, Symbol, ws } from './common.js'
import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge } from './types.js' import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship } from './types.js'
import { curry, is } from '../fn.js' import { curry, filter, is } from '../fn.js'
import { many, maybe, map, seq, skip, between, noCaseString, separated, list, any } from '../parser.js' import { many, maybe, map, seq, skip, between, noCaseString, separated, list, any } from '../parser.js'
const { Bracket, Colon, Comma, Hyphen } = Symbol const { Bracket, Colon, Comma, Hyphen } = Symbol
@ -51,14 +51,37 @@ const relationshipRight = map(([edge, direction]) =>
DirectedEdge.fromEdge(edge, direction), DirectedEdge.fromEdge(edge, direction),
seq(skip(Hyphen), edge, arrowRight) seq(skip(Hyphen), edge, arrowRight)
) )
const relationshipLeft = map(([direction, edge]) => const relationshipLeft = map(([direction, edge]) =>
DirectedEdge.fromEdge(edge, direction), DirectedEdge.fromEdge(edge, direction),
seq(arrowLeft, edge, skip(Hyphen)) seq(arrowLeft, edge, skip(Hyphen))
) )
const relationship = seq(any(relationshipRight, relationshipLeft), node) 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 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)

View file

@ -9,5 +9,5 @@ const params = map(
seq(list(seq(ws, Symbol.Comma, ws), value)) seq(list(seq(ws, Symbol.Comma, ws), value))
) )
export const statement = seq(skip(keyword), ws, params) export const returnClause = seq(skip(keyword), ws, params)

View file

@ -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) { constructor(...args) {
super(...args) this.values = args
} }
} }
@ -90,3 +99,11 @@ export class ObjectPath {
this.path = path this.path = path
} }
} }
export class Query {
constructor(use, match, returnValues) {
this.use = use
this.match = match
this.returnValues = returnValues
}
}

View file

@ -1,10 +1,10 @@
import { identifier, ws } from './common.js' 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' import { SelectedGraph } from './types.js'
const keyword = noCaseString('use') const keyword = noCaseString('use')
export const statement = map( export const useClause = map(
([graph]) => new SelectedGraph(graph), ([graph]) => new SelectedGraph(graph),
seq(skip(keyword), ws, identifier) seq(skip(keyword), ws, identifier)
) )

View file

@ -1,7 +1,8 @@
import util from 'node:util'
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from '../assert.js' import assert from '../assert.js'
import { node, edge, statement } from '../../src/query/match.js' import { node, edge, matchClause } from '../../src/query/match.js'
import { makeDirectedEdge, makeEdge, makeNode } from '../utils.js' import { makeDirectedEdge, makeEdge, makeNode, makeRelationship } from '../utils.js'
describe('node', () => { describe('node', () => {
it('should match a node with a name, label, and properties', () => { it('should match a node with a name, label, and properties', () => {
@ -68,73 +69,75 @@ describe('edge', () => {
describe('MATCH keyword', () => { describe('MATCH keyword', () => {
it('should match a single node with no relationships', () => { 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') const expected = makeNode(undefined, 'Label')
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(statement, 'MATCH (node:Label)', ([actual]) => { assert.parseOk(matchClause, 'MATCH (node:Label)', ([actual]) => {
const expected = makeNode('node', 'Label') const expected = makeNode('node', 'Label')
assert.deepEqual(actual, expected) 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']]) const expected = makeNode('node', 'Label', [['prop', true], ['value', 'test']])
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
}) })
it('should match nodes with a relationship to another another node', () => { it('should match nodes with a relationship to another another node', () => {
assert.parseOk(statement, 'MATCH (:Node)-[:Edge]->(:Node)', actual => { assert.parseOk(matchClause, 'MATCH (:Node)-[:Edge]->(:Node)', ([actual]) => {
const expected = [ const expected = makeRelationship(
makeNode(undefined, 'Node'), makeNode(undefined, 'Node'),
makeDirectedEdge(undefined, 'Edge', 1), makeDirectedEdge(undefined, 'Edge', 1),
makeNode(undefined, 'Node'), makeNode(undefined, 'Node'),
] )
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(statement, 'MATCH (a:Node)-[e:Edge]->(b:Node)', actual => { assert.parseOk(matchClause, 'MATCH (a:Node)-[e:Edge]->(b:Node)', ([actual]) => {
const expected = [ const expected = makeRelationship(
makeNode('a', 'Node'), makeNode('a', 'Node'),
makeDirectedEdge('e', 'Edge', 1), makeDirectedEdge('e', 'Edge', 1),
makeNode('b', 'Node'), makeNode('b', 'Node'),
] )
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(statement, 'MATCH (a:Node { db: 0.7 })-[e:Edge]->(b:Node { db: 0.95 })', actual => { assert.parseOk(matchClause, 'MATCH (a:Node { db: 0.7 })-[e:Edge]->(b:Node { db: 0.95 })', ([actual]) => {
const expected = [ const expected = makeRelationship(
makeNode('a', 'Node', [['db', 0.7]]), makeNode('a', 'Node', [['db', 0.7]]),
makeDirectedEdge('e', 'Edge', 1), makeDirectedEdge('e', 'Edge', 1),
makeNode('b', 'Node', [['db', 0.95]]), makeNode('b', 'Node', [['db', 0.95]]),
] )
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
assert.parseOk(statement, 'MATCH (:Node { db: 0.7 })-[:Edge]->(:Node { db: 0.95 })', actual => { assert.parseOk(matchClause, 'MATCH (:Node { db: 0.7 })-[:Edge]->(:Node { db: 0.95 })', ([actual]) => {
const expected = [ const expected = makeRelationship(
makeNode(undefined, 'Node', [['db', 0.7]]), makeNode(undefined, 'Node', [['db', 0.7]]),
makeDirectedEdge(undefined, 'Edge', 1), makeDirectedEdge(undefined, 'Edge', 1),
makeNode(undefined, 'Node', [['db', 0.95]]), makeNode(undefined, 'Node', [['db', 0.95]]),
] )
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })
}) })
it('should handle multiple relationships', () => { it('should handle multiple relationships', () => {
assert.parseOk(statement, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', actual => { assert.parseOk(matchClause, 'MATCH (player:Player)-[:Knows]->(a:NPC)-[:Knows]->(b:NPC)', ([actual]) => {
const expected = [ const expected = makeRelationship(
makeNode('player', 'Player'), makeNode('player', 'Player'),
makeDirectedEdge(undefined, 'Knows', 1), makeDirectedEdge(undefined, 'Knows', 1),
makeRelationship(
makeNode('a', 'NPC'), makeNode('a', 'NPC'),
makeDirectedEdge(undefined, 'Knows', 1), makeDirectedEdge(undefined, 'Knows', 1),
makeNode('b', 'NPC'), makeNode('b', 'NPC'),
] )
)
assert.deepEqual(actual, expected) assert.deepEqual(actual, expected)
}) })

40
tests/query/query.test.js Normal file
View file

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

View file

@ -1,27 +1,27 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from '../assert.js' import assert from '../assert.js'
import { statement } from '../../src/query/return.js' import { returnClause } from '../../src/query/return.js'
import { Alias, Identifier, Literal } from '../../src/query/types.js' import { Alias, Identifier, Literal, ReturnValues } from '../../src/query/types.js'
describe('RETURN keyword', () => { describe('RETURN keyword', () => {
it('should collect a single value for a query to return', () => { it('should collect a single value for a query to return', () => {
assert.parseOk(statement, 'RETURN folklore AS f', ([actual]) => { assert.parseOk(returnClause, 'RETURN folklore AS f', ([actual]) => {
assert.deepEqual(actual, new Alias(new Identifier('folklore'), new Identifier('f'))) assert.deepEqual(actual, new ReturnValues(new Alias(new Identifier('folklore'), new Identifier('f'))))
}) })
}) })
it('should collect multiple values for a query to return', () => { it('should collect multiple values for a query to return', () => {
assert.parseOk(statement, 'RETURN sybil, mercury, rowan', actual => { assert.parseOk(returnClause, 'RETURN sybil, mercury, rowan', ([actual]) => {
assert.deepStrictEqual(actual, ['sybil', 'mercury', 'rowan'].map(x => new Identifier(x))) assert.deepStrictEqual(actual, new ReturnValues(...['sybil', 'mercury', 'rowan'].map(x => new Identifier(x))))
}) })
}) })
it('should accept mixed values and aliases', () => { 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 sybil = new Alias(new Literal(19589), new Identifier('sybil'))
const mercury = new Alias(new Identifier('mercury'), new Identifier('vex')) const mercury = new Alias(new Identifier('mercury'), new Identifier('vex'))
const rowan = new Identifier('rowan') const rowan = new Identifier('rowan')
assert.deepStrictEqual(actual, [sybil, mercury, rowan]) assert.deepStrictEqual(actual, new ReturnValues(...[sybil, mercury, rowan]))
}) })
}) })
}) })

View file

@ -1,17 +1,17 @@
import { describe, it } from 'node:test' import { describe, it } from 'node:test'
import assert from '../assert.js' 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' import { Identifier } from '../../src/query/types.js'
describe('USE keyword', () => { describe('USE keyword', () => {
it('should select a graph to query', () => { 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')) assert.deepEqual(actual.identifier, new Identifier('default'))
}) })
}) })
it('should return an error if no graph identifier is provided', () => { it('should return an error if no graph identifier is provided', () => {
assert.parseErr(statement, 'USE') assert.parseErr(useClause, 'USE')
}) })
}) })

View file

@ -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)) 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 makeNode = graphObject
export const makeEdge = (name, label, props = []) => graphObject(name, label, props, Edge) 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 makeDirectedEdge = (name, label, direction, props = []) => DirectedEdge.fromEdge(makeEdge(name, label, props), direction)
export const makeRelationship = (from, edge, to) => new Relationship(from, edge, to)