flowquery 1.0.27 → 1.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/graph/relationship.d.ts.map +1 -1
  3. package/dist/graph/relationship.js +5 -1
  4. package/dist/graph/relationship.js.map +1 -1
  5. package/dist/parsing/base_parser.d.ts +1 -1
  6. package/dist/parsing/base_parser.d.ts.map +1 -1
  7. package/dist/parsing/base_parser.js.map +1 -1
  8. package/dist/parsing/expressions/operator.d.ts +37 -1
  9. package/dist/parsing/expressions/operator.d.ts.map +1 -1
  10. package/dist/parsing/expressions/operator.js +121 -2
  11. package/dist/parsing/expressions/operator.js.map +1 -1
  12. package/dist/parsing/functions/function_factory.d.ts +1 -0
  13. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  14. package/dist/parsing/functions/function_factory.js +1 -0
  15. package/dist/parsing/functions/function_factory.js.map +1 -1
  16. package/dist/parsing/functions/string_distance.d.ts +7 -0
  17. package/dist/parsing/functions/string_distance.d.ts.map +1 -0
  18. package/dist/parsing/functions/string_distance.js +84 -0
  19. package/dist/parsing/functions/string_distance.js.map +1 -0
  20. package/dist/parsing/parser.d.ts +6 -0
  21. package/dist/parsing/parser.d.ts.map +1 -1
  22. package/dist/parsing/parser.js +109 -11
  23. package/dist/parsing/parser.js.map +1 -1
  24. package/dist/tokenization/keyword.d.ts +4 -1
  25. package/dist/tokenization/keyword.d.ts.map +1 -1
  26. package/dist/tokenization/keyword.js +3 -0
  27. package/dist/tokenization/keyword.js.map +1 -1
  28. package/dist/tokenization/token.d.ts +6 -0
  29. package/dist/tokenization/token.d.ts.map +1 -1
  30. package/dist/tokenization/token.js +18 -0
  31. package/dist/tokenization/token.js.map +1 -1
  32. package/docs/flowquery.min.js +1 -1
  33. package/flowquery-py/pyproject.toml +1 -1
  34. package/flowquery-py/src/graph/relationship.py +5 -1
  35. package/flowquery-py/src/parsing/expressions/__init__.py +4 -0
  36. package/flowquery-py/src/parsing/expressions/operator.py +102 -0
  37. package/flowquery-py/src/parsing/functions/__init__.py +2 -0
  38. package/flowquery-py/src/parsing/functions/string_distance.py +88 -0
  39. package/flowquery-py/src/parsing/parser.py +111 -14
  40. package/flowquery-py/src/tokenization/keyword.py +3 -0
  41. package/flowquery-py/src/tokenization/token.py +21 -0
  42. package/flowquery-py/tests/compute/test_runner.py +366 -1
  43. package/flowquery-py/tests/parsing/test_expression.py +121 -1
  44. package/flowquery-py/tests/parsing/test_parser.py +203 -0
  45. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  46. package/package.json +1 -1
  47. package/src/graph/relationship.ts +4 -1
  48. package/src/parsing/base_parser.ts +1 -1
  49. package/src/parsing/expressions/operator.ts +129 -1
  50. package/src/parsing/functions/function_factory.ts +1 -0
  51. package/src/parsing/functions/string_distance.ts +80 -0
  52. package/src/parsing/parser.ts +120 -10
  53. package/src/tokenization/keyword.ts +3 -0
  54. package/src/tokenization/token.ts +24 -0
  55. package/tests/compute/runner.test.ts +333 -0
  56. package/tests/parsing/expression.test.ts +150 -16
  57. package/tests/parsing/parser.test.ts +200 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "A declarative query language for data processing pipelines.",
5
5
  "main": "dist/index.node.js",
6
6
  "types": "dist/index.node.d.ts",
@@ -133,7 +133,7 @@ class Relationship extends ASTNode {
133
133
  const followId = isLeft ? "left_id" : "right_id";
134
134
  while (findMatch(left_id, hop)) {
135
135
  const data: RelationshipRecord = this._data?.current(hop) as RelationshipRecord;
136
- if (hop >= this.hops!.min) {
136
+ if (hop + 1 >= this.hops!.min) {
137
137
  this.setValue(this);
138
138
  if (!this._matchesProperties(hop)) {
139
139
  continue;
@@ -146,6 +146,9 @@ class Relationship extends ASTNode {
146
146
  await this.find(data[followId], hop + 1);
147
147
  }
148
148
  this._matches.pop();
149
+ } else {
150
+ // Below minimum hops: traverse the edge without yielding a match
151
+ await this.find(data[followId], hop + 1);
149
152
  }
150
153
  }
151
154
  // Restore original source node
@@ -9,7 +9,7 @@ import Tokenizer from "../tokenization/tokenizer";
9
9
  */
10
10
  class BaseParser {
11
11
  private tokens: Token[] = <Token[]>[];
12
- private tokenIndex: number = 0;
12
+ protected tokenIndex: number = 0;
13
13
 
14
14
  constructor(tokens: Token[] | null = null) {
15
15
  if (tokens !== null) {
@@ -172,7 +172,126 @@ class Is extends Operator {
172
172
  super(-1, true);
173
173
  }
174
174
  public value(): number {
175
- return this.lhs.value() === this.rhs.value() ? 1 : 0;
175
+ return this.lhs.value() == this.rhs.value() ? 1 : 0;
176
+ }
177
+ }
178
+
179
+ class IsNot extends Operator {
180
+ constructor() {
181
+ super(-1, true);
182
+ }
183
+ public value(): number {
184
+ return this.lhs.value() != this.rhs.value() ? 1 : 0;
185
+ }
186
+ }
187
+
188
+ class In extends Operator {
189
+ constructor() {
190
+ super(-1, true);
191
+ }
192
+ public value(): number {
193
+ const list = this.rhs.value();
194
+ if (!Array.isArray(list)) {
195
+ throw new Error("Right operand of IN must be a list");
196
+ }
197
+ return list.includes(this.lhs.value()) ? 1 : 0;
198
+ }
199
+ }
200
+
201
+ class NotIn extends Operator {
202
+ constructor() {
203
+ super(-1, true);
204
+ }
205
+ public value(): number {
206
+ const list = this.rhs.value();
207
+ if (!Array.isArray(list)) {
208
+ throw new Error("Right operand of NOT IN must be a list");
209
+ }
210
+ return list.includes(this.lhs.value()) ? 0 : 1;
211
+ }
212
+ }
213
+
214
+ class Contains extends Operator {
215
+ constructor() {
216
+ super(0, true);
217
+ }
218
+ public value(): number {
219
+ const str = this.lhs.value();
220
+ const search = this.rhs.value();
221
+ if (typeof str !== "string" || typeof search !== "string") {
222
+ throw new Error("CONTAINS requires string operands");
223
+ }
224
+ return str.includes(search) ? 1 : 0;
225
+ }
226
+ }
227
+
228
+ class NotContains extends Operator {
229
+ constructor() {
230
+ super(0, true);
231
+ }
232
+ public value(): number {
233
+ const str = this.lhs.value();
234
+ const search = this.rhs.value();
235
+ if (typeof str !== "string" || typeof search !== "string") {
236
+ throw new Error("NOT CONTAINS requires string operands");
237
+ }
238
+ return str.includes(search) ? 0 : 1;
239
+ }
240
+ }
241
+
242
+ class StartsWith extends Operator {
243
+ constructor() {
244
+ super(0, true);
245
+ }
246
+ public value(): number {
247
+ const str = this.lhs.value();
248
+ const search = this.rhs.value();
249
+ if (typeof str !== "string" || typeof search !== "string") {
250
+ throw new Error("STARTS WITH requires string operands");
251
+ }
252
+ return str.startsWith(search) ? 1 : 0;
253
+ }
254
+ }
255
+
256
+ class NotStartsWith extends Operator {
257
+ constructor() {
258
+ super(0, true);
259
+ }
260
+ public value(): number {
261
+ const str = this.lhs.value();
262
+ const search = this.rhs.value();
263
+ if (typeof str !== "string" || typeof search !== "string") {
264
+ throw new Error("NOT STARTS WITH requires string operands");
265
+ }
266
+ return str.startsWith(search) ? 0 : 1;
267
+ }
268
+ }
269
+
270
+ class EndsWith extends Operator {
271
+ constructor() {
272
+ super(0, true);
273
+ }
274
+ public value(): number {
275
+ const str = this.lhs.value();
276
+ const search = this.rhs.value();
277
+ if (typeof str !== "string" || typeof search !== "string") {
278
+ throw new Error("ENDS WITH requires string operands");
279
+ }
280
+ return str.endsWith(search) ? 1 : 0;
281
+ }
282
+ }
283
+
284
+ class NotEndsWith extends Operator {
285
+ constructor() {
286
+ super(0, true);
287
+ }
288
+ public value(): number {
289
+ const str = this.lhs.value();
290
+ const search = this.rhs.value();
291
+ if (typeof str !== "string" || typeof search !== "string") {
292
+ throw new Error("NOT ENDS WITH requires string operands");
293
+ }
294
+ return str.endsWith(search) ? 0 : 1;
176
295
  }
177
296
  }
178
297
 
@@ -194,4 +313,13 @@ export {
194
313
  Or,
195
314
  Not,
196
315
  Is,
316
+ IsNot,
317
+ In,
318
+ NotIn,
319
+ Contains,
320
+ NotContains,
321
+ StartsWith,
322
+ NotStartsWith,
323
+ EndsWith,
324
+ NotEndsWith,
197
325
  };
@@ -21,6 +21,7 @@ import "./round";
21
21
  import "./schema";
22
22
  import "./size";
23
23
  import "./split";
24
+ import "./string_distance";
24
25
  import "./stringify";
25
26
  // Import built-in functions to ensure their @FunctionDef decorators run
26
27
  import "./sum";
@@ -0,0 +1,80 @@
1
+ import Function from "./function";
2
+ import { FunctionDef } from "./function_metadata";
3
+
4
+ /**
5
+ * Computes the normalized Levenshtein distance between two strings.
6
+ * The Levenshtein distance is the minimum number of single-character edits
7
+ * (insertions, deletions, or substitutions) required to change one string into the other.
8
+ * The result is normalized to [0, 1] by dividing by the length of the longer string.
9
+ *
10
+ * @param a - First string
11
+ * @param b - Second string
12
+ * @returns The normalized Levenshtein distance (0 = identical, 1 = completely different)
13
+ */
14
+ function levenshteinDistance(a: string, b: string): number {
15
+ const m = a.length;
16
+ const n = b.length;
17
+
18
+ // Both empty strings are identical
19
+ if (m === 0 && n === 0) return 0;
20
+
21
+ // Create a matrix of size (m+1) x (n+1)
22
+ const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
23
+
24
+ // Base cases: transforming empty string to/from a prefix
25
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
26
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
27
+
28
+ // Fill in the rest of the matrix
29
+ for (let i = 1; i <= m; i++) {
30
+ for (let j = 1; j <= n; j++) {
31
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
32
+ dp[i][j] = Math.min(
33
+ dp[i - 1][j] + 1, // deletion
34
+ dp[i][j - 1] + 1, // insertion
35
+ dp[i - 1][j - 1] + cost // substitution
36
+ );
37
+ }
38
+ }
39
+
40
+ // Normalize by the length of the longer string
41
+ return dp[m][n] / Math.max(m, n);
42
+ }
43
+
44
+ @FunctionDef({
45
+ description:
46
+ "Computes the normalized Levenshtein distance between two strings. Returns a value in [0, 1] where 0 means identical and 1 means completely different.",
47
+ category: "scalar",
48
+ parameters: [
49
+ { name: "string1", description: "First string", type: "string" },
50
+ { name: "string2", description: "Second string", type: "string" },
51
+ ],
52
+ output: {
53
+ description: "Normalized Levenshtein distance (0 = identical, 1 = completely different)",
54
+ type: "number",
55
+ example: 0.43,
56
+ },
57
+ examples: [
58
+ "RETURN string_distance('kitten', 'sitting')",
59
+ "WITH 'hello' AS a, 'hallo' AS b RETURN string_distance(a, b)",
60
+ ],
61
+ })
62
+ class StringDistance extends Function {
63
+ constructor() {
64
+ super("string_distance");
65
+ this._expectedParameterCount = 2;
66
+ }
67
+
68
+ public value(): any {
69
+ const str1 = this.getChildren()[0].value();
70
+ const str2 = this.getChildren()[1].value();
71
+ if (typeof str1 !== "string" || typeof str2 !== "string") {
72
+ throw new Error(
73
+ "Invalid arguments for string_distance function: both arguments must be strings"
74
+ );
75
+ }
76
+ return levenshteinDistance(str1, str2);
77
+ }
78
+ }
79
+
80
+ export default StringDistance;
@@ -24,7 +24,19 @@ import RangeLookup from "./data_structures/range_lookup";
24
24
  import Expression from "./expressions/expression";
25
25
  import FString from "./expressions/f_string";
26
26
  import Identifier from "./expressions/identifier";
27
- import { Not } from "./expressions/operator";
27
+ import {
28
+ Contains,
29
+ EndsWith,
30
+ In,
31
+ Is,
32
+ IsNot,
33
+ Not,
34
+ NotContains,
35
+ NotEndsWith,
36
+ NotIn,
37
+ NotStartsWith,
38
+ StartsWith,
39
+ } from "./expressions/operator";
28
40
  import Reference from "./expressions/reference";
29
41
  import String from "./expressions/string";
30
42
  import AggregateFunction from "./functions/aggregate_function";
@@ -434,10 +446,7 @@ class Parser extends BaseParser {
434
446
  let node = new Node();
435
447
  node.properties = new Map(this.parseProperties());
436
448
  node.label = label!;
437
- if (label !== null && identifier !== null) {
438
- node.identifier = identifier;
439
- this.variables.set(identifier, node);
440
- } else if (identifier !== null) {
449
+ if (identifier !== null && this.variables.has(identifier)) {
441
450
  let reference = this.variables.get(identifier);
442
451
  // Resolve through Expression -> Reference -> Node (e.g., after WITH)
443
452
  if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
@@ -450,6 +459,9 @@ class Parser extends BaseParser {
450
459
  throw new Error(`Undefined node reference: ${identifier}`);
451
460
  }
452
461
  node = new NodeReference(node, reference);
462
+ } else if (identifier !== null) {
463
+ node.identifier = identifier;
464
+ this.variables.set(identifier, node);
453
465
  }
454
466
  if (!this.token.isRightParenthesis()) {
455
467
  throw new Error("Expected closing parenthesis for node definition");
@@ -632,10 +644,7 @@ class Parser extends BaseParser {
632
644
  let relationship = new Relationship();
633
645
  relationship.direction = direction;
634
646
  relationship.properties = properties;
635
- if (type !== null && variable !== null) {
636
- relationship.identifier = variable;
637
- this.variables.set(variable, relationship);
638
- } else if (variable !== null) {
647
+ if (variable !== null && this.variables.has(variable)) {
639
648
  let reference = this.variables.get(variable);
640
649
  // Resolve through Expression -> Reference -> Relationship (e.g., after WITH)
641
650
  if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
@@ -648,6 +657,9 @@ class Parser extends BaseParser {
648
657
  throw new Error(`Undefined relationship reference: ${variable}`);
649
658
  }
650
659
  relationship = new RelationshipReference(relationship, reference);
660
+ } else if (variable !== null) {
661
+ relationship.identifier = variable;
662
+ this.variables.set(variable, relationship);
651
663
  }
652
664
  if (hops !== null) {
653
665
  relationship.hops = hops;
@@ -853,7 +865,25 @@ class Parser extends BaseParser {
853
865
  }
854
866
  this.skipWhitespaceAndComments();
855
867
  if (this.token.isOperator()) {
856
- expression.addNode(this.token.node);
868
+ if (this.token.isIs()) {
869
+ expression.addNode(this.parseIsOperator());
870
+ } else {
871
+ expression.addNode(this.token.node);
872
+ }
873
+ } else if (this.token.isIn()) {
874
+ expression.addNode(this.parseInOperator());
875
+ } else if (this.token.isContains()) {
876
+ expression.addNode(this.parseContainsOperator());
877
+ } else if (this.token.isStarts()) {
878
+ expression.addNode(this.parseStartsWithOperator());
879
+ } else if (this.token.isEnds()) {
880
+ expression.addNode(this.parseEndsWithOperator());
881
+ } else if (this.token.isNot()) {
882
+ const notOp = this.parseNotOperator();
883
+ if (notOp === null) {
884
+ break;
885
+ }
886
+ expression.addNode(notOp);
857
887
  } else {
858
888
  break;
859
889
  }
@@ -866,6 +896,86 @@ class Parser extends BaseParser {
866
896
  return null;
867
897
  }
868
898
 
899
+ private parseIsOperator(): Is | IsNot {
900
+ // Current token is IS. Look ahead for NOT to produce IS NOT.
901
+ const savedIndex = this.tokenIndex;
902
+ this.setNextToken();
903
+ this.skipWhitespaceAndComments();
904
+ if (this.token.isNot()) {
905
+ return new IsNot();
906
+ }
907
+ // Not IS NOT — restore position to IS so the outer loop's setNextToken advances past it.
908
+ this.tokenIndex = savedIndex;
909
+ return new Is();
910
+ }
911
+
912
+ private parseInOperator(): In | NotIn {
913
+ // Current token is IN. Advance past it so the outer loop's setNextToken moves correctly.
914
+ return new In();
915
+ }
916
+
917
+ private parseContainsOperator(): Contains {
918
+ return new Contains();
919
+ }
920
+
921
+ private parseStartsWithOperator(): StartsWith {
922
+ // Current token is STARTS. Look ahead for WITH.
923
+ const savedIndex = this.tokenIndex;
924
+ this.setNextToken();
925
+ this.skipWhitespaceAndComments();
926
+ if (this.token.isWith()) {
927
+ return new StartsWith();
928
+ }
929
+ this.tokenIndex = savedIndex;
930
+ throw new Error("Expected WITH after STARTS");
931
+ }
932
+
933
+ private parseEndsWithOperator(): EndsWith {
934
+ // Current token is ENDS. Look ahead for WITH.
935
+ const savedIndex = this.tokenIndex;
936
+ this.setNextToken();
937
+ this.skipWhitespaceAndComments();
938
+ if (this.token.isWith()) {
939
+ return new EndsWith();
940
+ }
941
+ this.tokenIndex = savedIndex;
942
+ throw new Error("Expected WITH after ENDS");
943
+ }
944
+
945
+ private parseNotOperator(): NotIn | NotContains | NotStartsWith | NotEndsWith | null {
946
+ // Current token is NOT. Look ahead for IN, CONTAINS, STARTS WITH, or ENDS WITH.
947
+ const savedIndex = this.tokenIndex;
948
+ this.setNextToken();
949
+ this.skipWhitespaceAndComments();
950
+ if (this.token.isIn()) {
951
+ return new NotIn();
952
+ }
953
+ if (this.token.isContains()) {
954
+ return new NotContains();
955
+ }
956
+ if (this.token.isStarts()) {
957
+ this.setNextToken();
958
+ this.skipWhitespaceAndComments();
959
+ if (this.token.isWith()) {
960
+ return new NotStartsWith();
961
+ }
962
+ this.tokenIndex = savedIndex;
963
+ return null;
964
+ }
965
+ if (this.token.isEnds()) {
966
+ this.setNextToken();
967
+ this.skipWhitespaceAndComments();
968
+ if (this.token.isWith()) {
969
+ return new NotEndsWith();
970
+ }
971
+ this.tokenIndex = savedIndex;
972
+ return null;
973
+ }
974
+ // Not a recognized NOT operator — restore position and let the outer loop break.
975
+ this.tokenIndex = savedIndex;
976
+ return null;
977
+ }
978
+
869
979
  private parseLookup(node: ASTNode): ASTNode {
870
980
  let variable: ASTNode = node;
871
981
  let lookup: Lookup | RangeLookup | null = null;
@@ -39,6 +39,9 @@ enum Keyword {
39
39
  END = "END",
40
40
  NULL = "NULL",
41
41
  IN = "IN",
42
+ CONTAINS = "CONTAINS",
43
+ STARTS = "STARTS",
44
+ ENDS = "ENDS",
42
45
  }
43
46
 
44
47
  export default Keyword;
@@ -627,6 +627,30 @@ class Token {
627
627
  return this._type === TokenType.KEYWORD && this._value === Keyword.IN;
628
628
  }
629
629
 
630
+ public static get CONTAINS(): Token {
631
+ return new Token(TokenType.KEYWORD, Keyword.CONTAINS);
632
+ }
633
+
634
+ public isContains(): boolean {
635
+ return this._type === TokenType.KEYWORD && this._value === Keyword.CONTAINS;
636
+ }
637
+
638
+ public static get STARTS(): Token {
639
+ return new Token(TokenType.KEYWORD, Keyword.STARTS);
640
+ }
641
+
642
+ public isStarts(): boolean {
643
+ return this._type === TokenType.KEYWORD && this._value === Keyword.STARTS;
644
+ }
645
+
646
+ public static get ENDS(): Token {
647
+ return new Token(TokenType.KEYWORD, Keyword.ENDS);
648
+ }
649
+
650
+ public isEnds(): boolean {
651
+ return this._type === TokenType.KEYWORD && this._value === Keyword.ENDS;
652
+ }
653
+
630
654
  public static get PIPE(): Token {
631
655
  return new Token(TokenType.KEYWORD, Operator.PIPE);
632
656
  }