add relationship and query datatypes
This commit is contained in:
parent
6dc2c1f6db
commit
bab47f8a95
11 changed files with 154 additions and 44 deletions
|
@ -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
|
||||
|
|
25
src/query/index.js
Normal file
25
src/query/index.js
Normal 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
|
||||
)
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
40
tests/query/query.test.js
Normal file
40
tests/query/query.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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]))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue