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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.30"
3
+ version = "1.0.31"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 = list(self._parse_expressions(AliasOption.REQUIRED))
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 = list(self._parse_expressions(AliasOption.OPTIONAL))
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 = list(self._parse_expressions(AliasOption.OPTIONAL))
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
- ) -> Iterator[Expression]:
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
- self._state.variables[reference.identifier] = expression
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
- self._state.variables[alias.get_alias()] = expression
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
+ }