Skip to content

Commit 9bf80b6

Browse files
authored
Handles OR/AND logic for schema based completions (#591)
1 parent 23e2566 commit 9bf80b6

3 files changed

Lines changed: 177 additions & 34 deletions

File tree

packages/language-support/src/autocompletion/schemaBasedCompletions.ts

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,67 @@ export const reltypesToCompletions = (reltypes: string[] = []) =>
5151
export const allReltypeCompletions = (dbSchema: DbSchema) =>
5252
reltypesToCompletions(dbSchema.relationshipTypes);
5353

54+
function intersectChildren(
55+
relsFromLabels: Map<string, Set<string>>,
56+
children: LabelOrCondition[],
57+
): Set<string> {
58+
let intersection: Set<string> = undefined;
59+
children.forEach((c) => {
60+
intersection = intersection
61+
? (intersection = intersection.intersection(
62+
walkLabelTree(relsFromLabels, c),
63+
))
64+
: walkLabelTree(relsFromLabels, c);
65+
});
66+
return intersection ?? new Set();
67+
}
68+
69+
function uniteChildren(
70+
relsFromLabels: Map<string, Set<string>>,
71+
children: LabelOrCondition[],
72+
): Set<string> {
73+
let union: Set<string> = new Set();
74+
children.forEach(
75+
(c) => (union = union.union(walkLabelTree(relsFromLabels, c))),
76+
);
77+
return union;
78+
}
79+
80+
function walkLabelTree(
81+
relsFromLabels: Map<string, Set<string>>,
82+
labelTree: LabelOrCondition,
83+
): Set<string> {
84+
if (isLabelLeaf(labelTree)) {
85+
return relsFromLabels.get(labelTree.value);
86+
} else if (labelTree.andOr == 'and') {
87+
return intersectChildren(relsFromLabels, labelTree.children);
88+
} else {
89+
return uniteChildren(relsFromLabels, labelTree.children);
90+
}
91+
}
92+
93+
function getRelsFromLabelsSet(dbSchema: DbSchema): Map<string, Set<string>> {
94+
if (dbSchema.graphSchema) {
95+
const relsFromLabelsSet: Map<string, Set<string>> = new Map();
96+
dbSchema.graphSchema.forEach((rel) => {
97+
let currentFromLabelEntry = relsFromLabelsSet.get(rel.from);
98+
let currentToLabelEntry = relsFromLabelsSet.get(rel.to);
99+
if (!currentFromLabelEntry) {
100+
relsFromLabelsSet.set(rel.from, new Set());
101+
currentFromLabelEntry = relsFromLabelsSet.get(rel.from);
102+
}
103+
if (!currentToLabelEntry) {
104+
relsFromLabelsSet.set(rel.to, new Set());
105+
currentToLabelEntry = relsFromLabelsSet.get(rel.to);
106+
}
107+
currentToLabelEntry.add(rel.relType);
108+
currentFromLabelEntry.add(rel.relType);
109+
});
110+
return relsFromLabelsSet;
111+
}
112+
return undefined;
113+
}
114+
54115
export function completeRelationshipType(
55116
dbSchema: DbSchema,
56117
parsingResult: ParsedStatement,
@@ -96,34 +157,20 @@ export function completeRelationshipType(
96157
?.flat()
97158
.find((entry) => entry.references.includes(variable.start.start));
98159

99-
if (foundVariable === undefined) {
160+
if (
161+
foundVariable === undefined ||
162+
('children' in foundVariable.labels &&
163+
foundVariable.labels.children.length == 0)
164+
) {
100165
return allReltypeCompletions(dbSchema);
101166
}
102167

103-
const labelTreeMayHaveLabel = (
104-
labelTree: LabelOrCondition,
105-
wantedLabels: string[],
106-
) => {
107-
if (isLabelLeaf(labelTree)) {
108-
return wantedLabels.includes(labelTree.value);
109-
}
110-
// check both or & and branches
111-
return labelTree.children.some((child) =>
112-
labelTreeMayHaveLabel(child, wantedLabels),
113-
);
114-
};
115-
116-
// limitation: not checking union types properly
117168
// limitation: not direction-aware (ignores <- vs ->)
118-
// limitation: not handling multiple relationship types [r:TYPE1|TYPE2]
119169
// limitation: not checking relationship variable reuse
120-
const rels = dbSchema.graphSchema.flatMap((schema) =>
121-
labelTreeMayHaveLabel(foundVariable.labels, [schema.from, schema.to])
122-
? [schema.relType]
123-
: [],
124-
);
170+
const relsFromLabelsSet = getRelsFromLabelsSet(dbSchema);
171+
const rels = walkLabelTree(relsFromLabelsSet, foundVariable.labels);
125172

126-
return reltypesToCompletions(rels);
173+
return reltypesToCompletions(Array.from(rels));
127174
}
128175
}
129176

packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const dbSchema = {
77
'Pokemon',
88
'Trainer',
99
'Gym',
10+
'Region',
1011
'Type',
1112
'Move',
1213
'UnrelatedLabel',
@@ -16,16 +17,24 @@ const dbSchema = {
1617
'CATCHES',
1718
'TRAINS',
1819
'BATTLES',
20+
'CHALLENGES',
1921
'KNOWS',
2022
'WEAK_TO',
23+
'STRONG_AGAINST',
24+
'IS_IN',
2125
'UNRELATED_RELTYPE',
2226
],
2327
graphSchema: [
2428
{ from: 'Trainer', relType: 'CATCHES', to: 'Pokemon' },
2529
{ from: 'Trainer', relType: 'TRAINS', to: 'Pokemon' },
2630
{ from: 'Trainer', relType: 'BATTLES', to: 'Trainer' },
31+
{ from: 'Trainer', relType: 'IS_IN', to: 'Region' },
32+
{ from: 'Gym', relType: 'IS_IN', to: 'Region' },
33+
{ from: 'Trainer', relType: 'CHALLENGES', to: 'Gym' },
34+
{ from: 'Pokemon', relType: 'CHALLENGES', to: 'Gym' },
2735
{ from: 'Pokemon', relType: 'KNOWS', to: 'Move' },
2836
{ from: 'Pokemon', relType: 'WEAK_TO', to: 'Type' },
37+
{ from: 'Type', relType: 'STRONG_AGAINST', to: 'Type' },
2938
{
3039
from: 'UnrelatedLabel',
3140
relType: 'UNRELATED_RELTYPE',
@@ -236,14 +245,39 @@ RETURN [(p)-[:`;
236245
});
237246
});
238247

239-
test('Handles nodes with multiple labels properly', () => {
248+
test('Handles nodes with multiple labels properly (AND logic)', () => {
240249
const query = 'MATCH (n:Trainer) WHERE n:Pokemon MATCH (n)-[:';
241250

242251
testCompletions({
243252
query,
244253
dbSchema,
245254
computeSymbolsInfo: true,
246255
expected: [
256+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
257+
// Limitation: These should actually be excluded, but since we dont track direction yet, they are not
258+
// Note that even though we don't know the direction in the query, we can see in the graph schema that we don't
259+
// have ex. 'CATCHES' going to/from both Trainer and Pokemon, meaning it cant be going to/from a Pokemon&Trainer node
260+
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
261+
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
262+
],
263+
excluded: [
264+
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
265+
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
266+
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
267+
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
268+
],
269+
});
270+
});
271+
272+
test('Handles nodes that have one of multiple labels (OR logic)', () => {
273+
const query = 'MATCH (x) WHERE x:Trainer OR x:Pokemon MATCH (x)-[:';
274+
275+
testCompletions({
276+
query,
277+
dbSchema,
278+
computeSymbolsInfo: true,
279+
expected: [
280+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
247281
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
248282
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
249283
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
@@ -256,6 +290,50 @@ RETURN [(p)-[:`;
256290
});
257291
});
258292

293+
test('Handles deeper label trees, and mixing AND/OR logic', () => {
294+
const query = 'MATCH (x:Trainer) WHERE x:Gym OR x:Pokemon MATCH (x)-[:';
295+
// -> AND(Trainer, OR(Gym, Pokemon))
296+
testCompletions({
297+
query,
298+
dbSchema,
299+
computeSymbolsInfo: true,
300+
expected: [
301+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
302+
{ label: 'IS_IN', kind: CompletionItemKind.TypeParameter },
303+
// Limitation: These should actually be excluded, but since we dont track direction yet, they are not
304+
// Note that even though we don't know the direction in the query, we can see in the graph schema that we don't
305+
// have ex. 'CATCHES' going to/from both Trainer and Pokemon, meaning it cant be going to/from a Pokemon&Trainer node
306+
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
307+
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
308+
],
309+
excluded: [
310+
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
311+
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
312+
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
313+
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
314+
],
315+
});
316+
});
317+
318+
test('Handles AND logic with self-referencing ', () => {
319+
const query = 'MATCH (x) WHERE x:Pokemon OR x:Type MATCH (x)-[:';
320+
testCompletions({
321+
query,
322+
dbSchema,
323+
computeSymbolsInfo: true,
324+
expected: [
325+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
326+
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
327+
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
328+
{ label: 'STRONG_AGAINST', kind: CompletionItemKind.TypeParameter },
329+
],
330+
excluded: [
331+
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
332+
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
333+
],
334+
});
335+
});
336+
259337
test('Limitation: Does not handle union types, as they are not yet supported in symbol table ', () => {
260338
const query = 'MATCH (x:Pokemon|Trainer)-[r:';
261339

@@ -264,38 +342,56 @@ RETURN [(p)-[:`;
264342
dbSchema,
265343
computeSymbolsInfo: true,
266344
expected: [
267-
/* These should be included
268-
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
345+
/* Only these should be included
346+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
347+
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
269348
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
270349
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
271350
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
272351
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
273352
*/
353+
// For now, test bail working
354+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
355+
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
356+
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
357+
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
358+
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
359+
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
360+
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
274361
],
275362
excluded: [
276-
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
363+
// { label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
277364
],
278365
});
279366
});
280367

281-
test('Limitation: Does not handle AND, OR, NOT, etc. ', () => {
282-
const query = 'MATCH (x:Pokemon) WHERE x:Trainer MATCH (x)-[:';
283-
368+
test('Limitation: Does not handle NOT', () => {
369+
const query = 'MATCH (x) WHERE NOT x:Trainer MATCH (x)-[:';
284370
testCompletions({
285371
query,
286372
dbSchema,
287373
computeSymbolsInfo: true,
288-
expected: [],
289-
excluded: [
374+
expected: [
375+
// For now, test bail working
376+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
377+
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
378+
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
290379
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
291-
// no valid outgoing types something that's both a pokemon and a trainer
292-
/*
293380
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
294381
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
295382
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
383+
/* These should be included
384+
{ label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter },
296385
{ label: 'KNOWS', kind: CompletionItemKind.TypeParameter },
297386
{ label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter },
387+
*/
388+
],
389+
excluded: [
390+
/* These should be excluded
298391
{ label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter },
392+
{ label: 'CATCHES', kind: CompletionItemKind.TypeParameter },
393+
{ label: 'TRAINS', kind: CompletionItemKind.TypeParameter },
394+
{ label: 'BATTLES', kind: CompletionItemKind.TypeParameter },
299395
*/
300396
],
301397
});

packages/language-support/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const isLabelLeaf = (
7676
};
7777

7878
export type ConditionNode = {
79-
andOr: 'AND' | 'OR';
79+
andOr: 'and' | 'or';
8080
children: LabelOrCondition[];
8181
};
8282

0 commit comments

Comments
 (0)