flowquery 1.0.36 → 1.0.38

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 (73) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/parsing/functions/coalesce.d.ts +0 -1
  3. package/dist/parsing/functions/coalesce.d.ts.map +1 -1
  4. package/dist/parsing/functions/coalesce.js +0 -1
  5. package/dist/parsing/functions/coalesce.js.map +1 -1
  6. package/dist/parsing/functions/date.d.ts +0 -2
  7. package/dist/parsing/functions/date.d.ts.map +1 -1
  8. package/dist/parsing/functions/date.js +0 -2
  9. package/dist/parsing/functions/date.js.map +1 -1
  10. package/dist/parsing/functions/datetime.d.ts +0 -2
  11. package/dist/parsing/functions/datetime.d.ts.map +1 -1
  12. package/dist/parsing/functions/datetime.js +0 -2
  13. package/dist/parsing/functions/datetime.js.map +1 -1
  14. package/dist/parsing/functions/localdatetime.d.ts +0 -2
  15. package/dist/parsing/functions/localdatetime.d.ts.map +1 -1
  16. package/dist/parsing/functions/localdatetime.js +0 -2
  17. package/dist/parsing/functions/localdatetime.js.map +1 -1
  18. package/dist/parsing/functions/localtime.d.ts +0 -2
  19. package/dist/parsing/functions/localtime.d.ts.map +1 -1
  20. package/dist/parsing/functions/localtime.js +0 -2
  21. package/dist/parsing/functions/localtime.js.map +1 -1
  22. package/dist/parsing/functions/temporal_utils.js +1 -1
  23. package/dist/parsing/functions/time.d.ts +0 -2
  24. package/dist/parsing/functions/time.d.ts.map +1 -1
  25. package/dist/parsing/functions/time.js +0 -2
  26. package/dist/parsing/functions/time.js.map +1 -1
  27. package/dist/parsing/functions/timestamp.d.ts +0 -2
  28. package/dist/parsing/functions/timestamp.d.ts.map +1 -1
  29. package/dist/parsing/functions/timestamp.js +1 -4
  30. package/dist/parsing/functions/timestamp.js.map +1 -1
  31. package/dist/parsing/operations/group_by.d.ts.map +1 -1
  32. package/dist/parsing/operations/group_by.js +3 -2
  33. package/dist/parsing/operations/group_by.js.map +1 -1
  34. package/dist/parsing/operations/limit.d.ts +2 -0
  35. package/dist/parsing/operations/limit.d.ts.map +1 -1
  36. package/dist/parsing/operations/limit.js +6 -0
  37. package/dist/parsing/operations/limit.js.map +1 -1
  38. package/dist/parsing/operations/return.d.ts +3 -0
  39. package/dist/parsing/operations/return.d.ts.map +1 -1
  40. package/dist/parsing/operations/return.js +10 -0
  41. package/dist/parsing/operations/return.js.map +1 -1
  42. package/dist/parsing/parser.d.ts.map +1 -1
  43. package/dist/parsing/parser.js +13 -10
  44. package/dist/parsing/parser.js.map +1 -1
  45. package/docs/flowquery.min.js +1 -1
  46. package/flowquery-py/pyproject.toml +1 -1
  47. package/flowquery-py/src/parsing/functions/coalesce.py +1 -2
  48. package/flowquery-py/src/parsing/functions/date_.py +0 -2
  49. package/flowquery-py/src/parsing/functions/datetime_.py +0 -2
  50. package/flowquery-py/src/parsing/functions/localdatetime.py +0 -2
  51. package/flowquery-py/src/parsing/functions/localtime.py +0 -2
  52. package/flowquery-py/src/parsing/functions/temporal_utils.py +1 -1
  53. package/flowquery-py/src/parsing/functions/time_.py +0 -2
  54. package/flowquery-py/src/parsing/functions/timestamp.py +1 -3
  55. package/flowquery-py/src/parsing/operations/limit.py +7 -0
  56. package/flowquery-py/src/parsing/operations/return_op.py +14 -0
  57. package/flowquery-py/src/parsing/parser.py +13 -9
  58. package/flowquery-py/tests/compute/test_runner.py +81 -2
  59. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  60. package/package.json +1 -1
  61. package/src/parsing/functions/coalesce.ts +0 -1
  62. package/src/parsing/functions/date.ts +0 -2
  63. package/src/parsing/functions/datetime.ts +0 -2
  64. package/src/parsing/functions/localdatetime.ts +0 -2
  65. package/src/parsing/functions/localtime.ts +0 -2
  66. package/src/parsing/functions/temporal_utils.ts +1 -1
  67. package/src/parsing/functions/time.ts +0 -2
  68. package/src/parsing/functions/timestamp.ts +1 -5
  69. package/src/parsing/operations/group_by.ts +4 -2
  70. package/src/parsing/operations/limit.ts +7 -1
  71. package/src/parsing/operations/return.ts +11 -0
  72. package/src/parsing/parser.ts +12 -10
  73. package/tests/compute/runner.test.ts +69 -2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.26"
3
+ version = "1.0.28"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -22,7 +22,6 @@ class Coalesce(Function):
22
22
  """Coalesce function.
23
23
 
24
24
  Returns the first non-null value from a list of expressions.
25
- Equivalent to Neo4j's coalesce() function.
26
25
  """
27
26
 
28
27
  def __init__(self) -> None:
@@ -37,7 +36,7 @@ class Coalesce(Function):
37
36
  try:
38
37
  val = child.value()
39
38
  except (KeyError, AttributeError):
40
- # Treat missing properties/keys as null, matching Neo4j behavior
39
+ # Treat missing properties/keys as null
41
40
  val = None
42
41
  if val is not None:
43
42
  return val
@@ -42,8 +42,6 @@ class DateFunction(Function):
42
42
  Returns a date value (no time component).
43
43
  When called with no arguments, returns the current date.
44
44
  When called with a string argument, parses it as an ISO 8601 date.
45
-
46
- Equivalent to Neo4j's date() function.
47
45
  """
48
46
 
49
47
  def __init__(self) -> None:
@@ -43,8 +43,6 @@ class Datetime(Function):
43
43
  When called with no arguments, returns the current UTC datetime.
44
44
  When called with a string argument, parses it as an ISO 8601 datetime.
45
45
  When called with a map argument, constructs a datetime from components.
46
-
47
- Equivalent to Neo4j's datetime() function.
48
46
  """
49
47
 
50
48
  def __init__(self) -> None:
@@ -41,8 +41,6 @@ class LocalDatetime(Function):
41
41
  Returns a local datetime value (date + time, no timezone offset).
42
42
  When called with no arguments, returns the current local datetime.
43
43
  When called with a string argument, parses it as an ISO 8601 datetime.
44
-
45
- Equivalent to Neo4j's localdatetime() function.
46
44
  """
47
45
 
48
46
  def __init__(self) -> None:
@@ -38,8 +38,6 @@ class LocalTime(Function):
38
38
  Returns a local time value (no timezone offset).
39
39
  When called with no arguments, returns the current local time.
40
40
  When called with a string argument, parses it.
41
-
42
- Equivalent to Neo4j's localtime() function.
43
41
  """
44
42
 
45
43
  def __init__(self) -> None:
@@ -9,7 +9,7 @@ from typing import Any, Dict
9
9
 
10
10
 
11
11
  def iso_day_of_week(d: date) -> int:
12
- """Computes the ISO day of the week (1 = Monday, 7 = Sunday) matching Neo4j convention."""
12
+ """Computes the ISO day of the week (1 = Monday, 7 = Sunday)."""
13
13
  return d.isoweekday()
14
14
 
15
15
 
@@ -38,8 +38,6 @@ class Time(Function):
38
38
  Returns a time value (with timezone offset awareness).
39
39
  When called with no arguments, returns the current UTC time.
40
40
  When called with a string argument, parses it.
41
-
42
- Equivalent to Neo4j's time() function.
43
41
  """
44
42
 
45
43
  def __init__(self) -> None:
@@ -9,8 +9,7 @@ from .function_metadata import FunctionDef
9
9
 
10
10
  @FunctionDef({
11
11
  "description": (
12
- "Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z). "
13
- "Equivalent to Neo4j's timestamp() function."
12
+ "Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z)."
14
13
  ),
15
14
  "category": "scalar",
16
15
  "parameters": [],
@@ -28,7 +27,6 @@ class Timestamp(Function):
28
27
  """Timestamp function.
29
28
 
30
29
  Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
31
- Equivalent to Neo4j's timestamp() function.
32
30
  """
33
31
 
34
32
  def __init__(self) -> None:
@@ -11,6 +11,13 @@ class Limit(Operation):
11
11
  self._count = 0
12
12
  self._limit = limit
13
13
 
14
+ @property
15
+ def is_limit_reached(self) -> bool:
16
+ return self._count >= self._limit
17
+
18
+ def increment(self) -> None:
19
+ self._count += 1
20
+
14
21
  async def run(self) -> None:
15
22
  if self._count >= self._limit:
16
23
  return
@@ -4,6 +4,7 @@ import copy
4
4
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
5
5
 
6
6
  from ..ast_node import ASTNode
7
+ from .limit import Limit
7
8
  from .projection import Projection
8
9
 
9
10
  if TYPE_CHECKING:
@@ -24,6 +25,7 @@ class Return(Projection):
24
25
  super().__init__(expressions)
25
26
  self._where: Optional['Where'] = None
26
27
  self._results: List[Dict[str, Any]] = []
28
+ self._limit: Optional[Limit] = None
27
29
 
28
30
  @property
29
31
  def where(self) -> Any:
@@ -35,9 +37,19 @@ class Return(Projection):
35
37
  def where(self, where: 'Where') -> None:
36
38
  self._where = where
37
39
 
40
+ @property
41
+ def limit(self) -> Optional[Limit]:
42
+ return self._limit
43
+
44
+ @limit.setter
45
+ def limit(self, limit: Limit) -> None:
46
+ self._limit = limit
47
+
38
48
  async def run(self) -> None:
39
49
  if not self.where:
40
50
  return
51
+ if self._limit is not None and self._limit.is_limit_reached:
52
+ return
41
53
  record: Dict[str, Any] = {}
42
54
  for expression, alias in self.expressions():
43
55
  raw = expression.value()
@@ -45,6 +57,8 @@ class Return(Projection):
45
57
  value = copy.deepcopy(raw) if isinstance(raw, (dict, list)) else raw
46
58
  record[alias] = value
47
59
  self._results.append(record)
60
+ if self._limit is not None:
61
+ self._limit.increment()
48
62
 
49
63
  async def initialize(self) -> None:
50
64
  self._results = []
@@ -116,6 +116,9 @@ class Parser(BaseParser):
116
116
  if self.token.is_union():
117
117
  break
118
118
 
119
+ if self.token.is_eof():
120
+ break
121
+
119
122
  operation = self._parse_operation()
120
123
  if operation is None and not is_sub_query:
121
124
  raise ValueError("Expected one of WITH, UNWIND, RETURN, LOAD, OR CALL")
@@ -145,8 +148,11 @@ class Parser(BaseParser):
145
148
 
146
149
  limit = self._parse_limit()
147
150
  if limit is not None:
148
- operation.add_sibling(limit)
149
- operation = limit
151
+ if isinstance(operation, Return):
152
+ operation.limit = limit
153
+ else:
154
+ operation.add_sibling(limit)
155
+ operation = limit
150
156
 
151
157
  previous = operation
152
158
 
@@ -539,13 +545,11 @@ class Parser(BaseParser):
539
545
  node.properties = dict(self._parse_properties())
540
546
  if identifier is not None and identifier in self._state.variables:
541
547
  reference = self._state.variables.get(identifier)
542
- # Resolve through Expression -> Reference -> Node (e.g., after WITH)
543
- ref_child = reference.first_child() if isinstance(reference, Expression) else None
544
- if isinstance(ref_child, Reference):
545
- inner = ref_child.referred
546
- if isinstance(inner, Node):
547
- reference = inner
548
- if reference is None or (not isinstance(reference, Node) and not isinstance(reference, Unwind)):
548
+ if reference is None or (
549
+ not isinstance(reference, Node)
550
+ and not isinstance(reference, Unwind)
551
+ and not isinstance(reference, Expression)
552
+ ):
549
553
  raise ValueError(f"Undefined node reference: {identifier}")
550
554
  node = NodeReference(node, reference)
551
555
  elif identifier is not None:
@@ -925,6 +925,20 @@ class TestRunner:
925
925
  results = runner.results
926
926
  assert len(results) == 50
927
927
 
928
+ @pytest.mark.asyncio
929
+ async def test_limit_as_last_operation(self):
930
+ """Test limit as the last operation after return."""
931
+ runner = Runner(
932
+ """
933
+ unwind range(1, 10) as i
934
+ return i
935
+ limit 5
936
+ """
937
+ )
938
+ await runner.run()
939
+ results = runner.results
940
+ assert len(results) == 5
941
+
928
942
  @pytest.mark.asyncio
929
943
  async def test_range_lookup(self):
930
944
  """Test range lookup."""
@@ -1457,6 +1471,71 @@ class TestRunner:
1457
1471
  assert results[0] == {"name1": "Person 1", "name2": "Person 2", "name3": "Person 3"}
1458
1472
  assert results[1] == {"name1": "Person 2", "name2": "Person 3", "name3": "Person 4"}
1459
1473
 
1474
+ @pytest.mark.asyncio
1475
+ async def test_match_with_aggregated_with_and_subsequent_match(self):
1476
+ """Test match with aggregated WITH followed by another match using the same node reference."""
1477
+ await Runner(
1478
+ """
1479
+ CREATE VIRTUAL (:AggUser) AS {
1480
+ unwind [
1481
+ {id: 1, name: 'Alice'},
1482
+ {id: 2, name: 'Bob'},
1483
+ {id: 3, name: 'Carol'}
1484
+ ] as record
1485
+ RETURN record.id as id, record.name as name
1486
+ }
1487
+ """
1488
+ ).run()
1489
+ await Runner(
1490
+ """
1491
+ CREATE VIRTUAL (:AggUser)-[:KNOWS]-(:AggUser) AS {
1492
+ unwind [
1493
+ {left_id: 1, right_id: 2},
1494
+ {left_id: 1, right_id: 3}
1495
+ ] as record
1496
+ RETURN record.left_id as left_id, record.right_id as right_id
1497
+ }
1498
+ """
1499
+ ).run()
1500
+ await Runner(
1501
+ """
1502
+ CREATE VIRTUAL (:AggProject) AS {
1503
+ unwind [
1504
+ {id: 1, name: 'Project A'},
1505
+ {id: 2, name: 'Project B'}
1506
+ ] as record
1507
+ RETURN record.id as id, record.name as name
1508
+ }
1509
+ """
1510
+ ).run()
1511
+ await Runner(
1512
+ """
1513
+ CREATE VIRTUAL (:AggUser)-[:WORKS_ON]-(:AggProject) AS {
1514
+ unwind [
1515
+ {left_id: 1, right_id: 1},
1516
+ {left_id: 1, right_id: 2}
1517
+ ] as record
1518
+ RETURN record.left_id as left_id, record.right_id as right_id
1519
+ }
1520
+ """
1521
+ ).run()
1522
+ match = Runner(
1523
+ """
1524
+ MATCH (u:AggUser)-[:KNOWS]->(s:AggUser)
1525
+ WITH u, count(s) as acquaintances
1526
+ MATCH (u)-[:WORKS_ON]->(p:AggProject)
1527
+ RETURN u.name as name, acquaintances, collect(p.name) as projects
1528
+ """
1529
+ )
1530
+ await match.run()
1531
+ results = match.results
1532
+ assert len(results) == 1
1533
+ assert results[0] == {
1534
+ "name": "Alice",
1535
+ "acquaintances": 2,
1536
+ "projects": ["Project A", "Project B"],
1537
+ }
1538
+
1460
1539
  @pytest.mark.asyncio
1461
1540
  async def test_match_and_return_full_node(self):
1462
1541
  """Test match and return full node."""
@@ -2232,7 +2311,7 @@ class TestRunner:
2232
2311
  }
2233
2312
  """
2234
2313
  ).run()
2235
- # When accessing b.name and b is null (no match), should return null like Neo4j
2314
+ # When accessing b.name and b is null (no match), should return null
2236
2315
  match = Runner(
2237
2316
  """
2238
2317
  MATCH (a:OptPropPerson)
@@ -3427,7 +3506,7 @@ class TestRunner:
3427
3506
  assert results[0] == {"result": "Alice"}
3428
3507
 
3429
3508
  # ============================================================
3430
- # Temporal / Time Functions (Neo4j-style)
3509
+ # Temporal / Time Functions
3431
3510
  # ============================================================
3432
3511
 
3433
3512
  @pytest.mark.asyncio