flowquery 1.0.40 → 1.0.41
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.
- package/dist/flowquery.min.js +1 -1
- package/dist/parsing/parser.d.ts +7 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +43 -29
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/parsing/parser.py +21 -7
- package/flowquery-py/tests/compute/test_runner.py +56 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/parsing/parser.ts +47 -31
- package/tests/compute/runner.test.ts +47 -0
|
@@ -214,7 +214,7 @@ class Parser(BaseParser):
|
|
|
214
214
|
distinct = True
|
|
215
215
|
self.set_next_token()
|
|
216
216
|
self._expect_and_skip_whitespace_and_comments()
|
|
217
|
-
expressions =
|
|
217
|
+
expressions = self._parse_expressions(AliasOption.REQUIRED)
|
|
218
218
|
if len(expressions) == 0:
|
|
219
219
|
raise ValueError("Expected expression")
|
|
220
220
|
if distinct or any(expr.has_reducers() for expr in expressions):
|
|
@@ -254,7 +254,7 @@ class Parser(BaseParser):
|
|
|
254
254
|
distinct = True
|
|
255
255
|
self.set_next_token()
|
|
256
256
|
self._expect_and_skip_whitespace_and_comments()
|
|
257
|
-
expressions =
|
|
257
|
+
expressions = self._parse_expressions(AliasOption.OPTIONAL)
|
|
258
258
|
if len(expressions) == 0:
|
|
259
259
|
raise ValueError("Expected expression")
|
|
260
260
|
if distinct or any(expr.has_reducers() for expr in expressions):
|
|
@@ -353,7 +353,7 @@ class Parser(BaseParser):
|
|
|
353
353
|
self._expect_previous_token_to_be_whitespace_or_comment()
|
|
354
354
|
self.set_next_token()
|
|
355
355
|
self._expect_and_skip_whitespace_and_comments()
|
|
356
|
-
expressions =
|
|
356
|
+
expressions = self._parse_expressions(AliasOption.OPTIONAL)
|
|
357
357
|
if len(expressions) == 0:
|
|
358
358
|
raise ValueError("Expected at least one expression")
|
|
359
359
|
call.yielded = expressions # type: ignore[assignment]
|
|
@@ -791,16 +791,30 @@ class Parser(BaseParser):
|
|
|
791
791
|
|
|
792
792
|
def _parse_expressions(
|
|
793
793
|
self, alias_option: AliasOption = AliasOption.NOT_ALLOWED
|
|
794
|
-
) ->
|
|
794
|
+
) -> List[Expression]:
|
|
795
|
+
"""Parse a comma-separated list of expressions with deferred variable
|
|
796
|
+
registration. Aliases set by earlier expressions in the same clause
|
|
797
|
+
won't shadow variables needed by later expressions
|
|
798
|
+
(e.g. ``RETURN a.x AS a, a.y AS b``)."""
|
|
799
|
+
parsed = list(self.__parse_expressions(alias_option))
|
|
800
|
+
for expression, variable_name in parsed:
|
|
801
|
+
if variable_name is not None:
|
|
802
|
+
self._state.variables[variable_name] = expression
|
|
803
|
+
return [expression for expression, _ in parsed]
|
|
804
|
+
|
|
805
|
+
def __parse_expressions(
|
|
806
|
+
self, alias_option: AliasOption
|
|
807
|
+
) -> Iterator[Tuple[Expression, Optional[str]]]:
|
|
795
808
|
while True:
|
|
796
809
|
expression = self._parse_expression()
|
|
797
810
|
if expression is not None:
|
|
811
|
+
variable_name: Optional[str] = None
|
|
798
812
|
alias = self._parse_alias()
|
|
799
813
|
if isinstance(expression.first_child(), Reference) and alias is None:
|
|
800
814
|
reference = expression.first_child()
|
|
801
815
|
assert isinstance(reference, Reference) # For type narrowing
|
|
802
816
|
expression.set_alias(reference.identifier)
|
|
803
|
-
|
|
817
|
+
variable_name = reference.identifier
|
|
804
818
|
elif (alias_option == AliasOption.REQUIRED and
|
|
805
819
|
alias is None and
|
|
806
820
|
not isinstance(expression.first_child(), Reference)):
|
|
@@ -809,8 +823,8 @@ class Parser(BaseParser):
|
|
|
809
823
|
raise ValueError("Alias not allowed")
|
|
810
824
|
elif alias_option in (AliasOption.OPTIONAL, AliasOption.REQUIRED) and alias is not None:
|
|
811
825
|
expression.set_alias(alias.get_alias())
|
|
812
|
-
|
|
813
|
-
yield expression
|
|
826
|
+
variable_name = alias.get_alias()
|
|
827
|
+
yield expression, variable_name
|
|
814
828
|
else:
|
|
815
829
|
break
|
|
816
830
|
self._skip_whitespace_and_comments()
|
|
@@ -4339,4 +4339,59 @@ class TestRunner:
|
|
|
4339
4339
|
match = Runner("MATCH (n:PyKeepNode) RETURN n")
|
|
4340
4340
|
await match.run()
|
|
4341
4341
|
assert len(match.results) == 1
|
|
4342
|
-
assert match.results[0]["n"]["name"] == "Keep"
|
|
4342
|
+
assert match.results[0]["n"]["name"] == "Keep"
|
|
4343
|
+
|
|
4344
|
+
@pytest.mark.asyncio
|
|
4345
|
+
async def test_return_alias_shadowing_graph_variable(self):
|
|
4346
|
+
"""Test that RETURN alias doesn't shadow graph variable in same clause.
|
|
4347
|
+
|
|
4348
|
+
When RETURN mentor.displayName AS mentor is followed by mentor.jobTitle,
|
|
4349
|
+
the alias 'mentor' should not overwrite the graph node variable before
|
|
4350
|
+
subsequent expressions are parsed.
|
|
4351
|
+
"""
|
|
4352
|
+
await Runner(
|
|
4353
|
+
"""
|
|
4354
|
+
CREATE VIRTUAL (:PyMentorUser) AS {
|
|
4355
|
+
UNWIND [
|
|
4356
|
+
{id: 1, displayName: 'Alice Smith', jobTitle: 'Senior Engineer', department: 'Engineering'},
|
|
4357
|
+
{id: 2, displayName: 'Bob Jones', jobTitle: 'Staff Engineer', department: 'Engineering'},
|
|
4358
|
+
{id: 3, displayName: 'Chloe Dubois', jobTitle: 'Junior Engineer', department: 'Engineering'}
|
|
4359
|
+
] AS record
|
|
4360
|
+
RETURN record.id AS id, record.displayName AS displayName, record.jobTitle AS jobTitle, record.department AS department
|
|
4361
|
+
}
|
|
4362
|
+
"""
|
|
4363
|
+
).run()
|
|
4364
|
+
|
|
4365
|
+
await Runner(
|
|
4366
|
+
"""
|
|
4367
|
+
CREATE VIRTUAL (:PyMentorUser)-[:PY_MENTORS]-(:PyMentorUser) AS {
|
|
4368
|
+
UNWIND [
|
|
4369
|
+
{left_id: 1, right_id: 3},
|
|
4370
|
+
{left_id: 2, right_id: 3}
|
|
4371
|
+
] AS record
|
|
4372
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
4373
|
+
}
|
|
4374
|
+
"""
|
|
4375
|
+
).run()
|
|
4376
|
+
|
|
4377
|
+
runner = Runner(
|
|
4378
|
+
"""
|
|
4379
|
+
MATCH (mentor:PyMentorUser)-[:PY_MENTORS]->(mentee:PyMentorUser)
|
|
4380
|
+
WHERE mentee.displayName = "Chloe Dubois"
|
|
4381
|
+
RETURN mentor.displayName AS mentor, mentor.jobTitle AS mentorJobTitle, mentor.department AS mentorDepartment
|
|
4382
|
+
"""
|
|
4383
|
+
)
|
|
4384
|
+
await runner.run()
|
|
4385
|
+
results = runner.results
|
|
4386
|
+
|
|
4387
|
+
assert len(results) == 2
|
|
4388
|
+
assert results[0] == {
|
|
4389
|
+
"mentor": "Alice Smith",
|
|
4390
|
+
"mentorJobTitle": "Senior Engineer",
|
|
4391
|
+
"mentorDepartment": "Engineering",
|
|
4392
|
+
}
|
|
4393
|
+
assert results[1] == {
|
|
4394
|
+
"mentor": "Bob Jones",
|
|
4395
|
+
"mentorJobTitle": "Staff Engineer",
|
|
4396
|
+
"mentorDepartment": "Engineering",
|
|
4397
|
+
}
|