fquery 0.4__tar.gz → 0.5__tar.gz
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.
- {fquery-0.4 → fquery-0.5}/LICENSE +2 -1
- {fquery-0.4 → fquery-0.5}/PKG-INFO +34 -6
- {fquery-0.4 → fquery-0.5}/README.md +0 -1
- fquery-0.5/fquery/arrow.py +121 -0
- {fquery-0.4 → fquery-0.5}/fquery/async_utils.py +5 -1
- fquery-0.5/fquery/cypher_builder.py +194 -0
- fquery-0.5/fquery/ladybug.py +394 -0
- {fquery-0.4 → fquery-0.5}/fquery/malloy_builder.py +2 -4
- fquery-0.5/fquery/naming.py +5 -0
- fquery-0.5/fquery/pydantic.py +124 -0
- {fquery-0.4 → fquery-0.5}/fquery/query.py +45 -6
- {fquery-0.4 → fquery-0.5}/fquery/sql_builder.py +2 -4
- {fquery-0.4 → fquery-0.5}/fquery/sqlmodel.py +160 -24
- {fquery-0.4 → fquery-0.5}/fquery/view_model.py +11 -1
- {fquery-0.4 → fquery-0.5}/fquery.egg-info/PKG-INFO +34 -6
- {fquery-0.4 → fquery-0.5}/fquery.egg-info/SOURCES.txt +9 -0
- fquery-0.5/fquery.egg-info/requires.txt +43 -0
- fquery-0.5/pyproject.toml +115 -0
- {fquery-0.4 → fquery-0.5}/setup.py +17 -6
- {fquery-0.4 → fquery-0.5}/tests/__init__.py +0 -3
- {fquery-0.4 → fquery-0.5}/tests/async_test.py +5 -1
- {fquery-0.4 → fquery-0.5}/tests/mock_user.py +6 -9
- fquery-0.5/tests/test_arrow.py +175 -0
- fquery-0.5/tests/test_cypher.py +81 -0
- fquery-0.5/tests/test_ladybug.py +234 -0
- fquery-0.5/tests/test_ladybug_demo.py +16 -0
- {fquery-0.4 → fquery-0.5}/tests/test_malloy.py +1 -1
- fquery-0.5/tests/test_pydantic.py +68 -0
- fquery-0.5/tests/test_self_reference.py +30 -0
- {fquery-0.4 → fquery-0.5}/tests/test_sqlmodel.py +52 -1
- fquery-0.4/fquery/pydantic.py +0 -46
- fquery-0.4/fquery.egg-info/requires.txt +0 -13
- fquery-0.4/pyproject.toml +0 -8
- fquery-0.4/tests/test_pydantic.py +0 -34
- {fquery-0.4 → fquery-0.5}/fquery/__init__.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/aitertools.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/django.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/execute.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/fgraphql.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/polars_builder.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/resolve.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/visitor.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery/walk.py +0 -0
- {fquery-0.4 → fquery-0.5}/fquery.egg-info/dependency_links.txt +0 -0
- {fquery-0.4 → fquery-0.5}/fquery.egg-info/top_level.txt +0 -0
- {fquery-0.4 → fquery-0.5}/setup.cfg +0 -0
- {fquery-0.4 → fquery-0.5}/tests/benchmark.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/django_example_model.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/graphql_mock_user.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/test_operators.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/test_polars.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/test_sql.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/test_walk.py +0 -0
- {fquery-0.4 → fquery-0.5}/tests/test_walk_obj.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) Facebook, Inc. and its affiliates
|
|
3
|
+
Copyright (c) Facebook, Inc. and its affiliates, 2015-2020
|
|
4
|
+
Copyright (c) Arun Sharma, 2021-2025
|
|
4
5
|
|
|
5
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
7
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,17 +1,46 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fquery
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: A graph query engine
|
|
5
5
|
Home-page: https://github.com/adsharma/fquery
|
|
6
|
+
License: MIT
|
|
6
7
|
Classifier: Programming Language :: Python :: 3
|
|
7
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
9
|
Classifier: Operating System :: OS Independent
|
|
9
10
|
Requires-Python: >=3.6
|
|
10
11
|
Description-Content-Type: text/markdown
|
|
11
|
-
Provides-Extra: SQL
|
|
12
|
-
Provides-Extra: df
|
|
13
|
-
Provides-Extra: graphql
|
|
14
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aioitertools
|
|
14
|
+
Provides-Extra: fquery
|
|
15
|
+
Requires-Dist: aioitertools; extra == "fquery"
|
|
16
|
+
Provides-Extra: pydantic
|
|
17
|
+
Requires-Dist: pydantic; extra == "pydantic"
|
|
18
|
+
Provides-Extra: sql
|
|
19
|
+
Requires-Dist: pypika>=0.36.5; extra == "sql"
|
|
20
|
+
Provides-Extra: sqlmodel
|
|
21
|
+
Requires-Dist: sqlmodel>=0.0.42; extra == "sqlmodel"
|
|
22
|
+
Requires-Dist: duckdb_engine>=0.14.0; extra == "sqlmodel"
|
|
23
|
+
Requires-Dist: inflection>=0.5.1; extra == "sqlmodel"
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.36; extra == "sqlmodel"
|
|
25
|
+
Provides-Extra: graphql
|
|
26
|
+
Requires-Dist: strawberry-graphql>=0.37.1; extra == "graphql"
|
|
27
|
+
Provides-Extra: polars
|
|
28
|
+
Requires-Dist: polars>=0.12.0; extra == "polars"
|
|
29
|
+
Provides-Extra: pyarrow
|
|
30
|
+
Requires-Dist: pyarrow; extra == "pyarrow"
|
|
31
|
+
Provides-Extra: django
|
|
32
|
+
Requires-Dist: django; extra == "django"
|
|
33
|
+
Provides-Extra: malloy
|
|
34
|
+
Provides-Extra: cypher
|
|
35
|
+
Provides-Extra: ladybug
|
|
36
|
+
Requires-Dist: ladybug>=0.17.0; python_version >= "3.10" and extra == "ladybug"
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest; extra == "test"
|
|
39
|
+
Provides-Extra: df
|
|
40
|
+
Requires-Dist: polars>=0.12.0; extra == "df"
|
|
41
|
+
Dynamic: home-page
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
Dynamic: requires-python
|
|
15
44
|
|
|
16
45
|
# Overview
|
|
17
46
|
|
|
@@ -59,4 +88,3 @@ fquery with Django and get easy access to graphql functionality
|
|
|
59
88
|
This project is made available under the Apache License, version 2.0.
|
|
60
89
|
|
|
61
90
|
See [LICENSE.txt](license.txt) for details.
|
|
62
|
-
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
4
|
+
from datetime import date, datetime, time
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Dict,
|
|
9
|
+
List,
|
|
10
|
+
Tuple,
|
|
11
|
+
Type,
|
|
12
|
+
Union,
|
|
13
|
+
get_args,
|
|
14
|
+
get_origin,
|
|
15
|
+
get_type_hints,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
import pyarrow as pa
|
|
19
|
+
|
|
20
|
+
UNION_TYPES = (Union,)
|
|
21
|
+
if hasattr(types, "UnionType"):
|
|
22
|
+
UNION_TYPES = UNION_TYPES + (types.UnionType,)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TYPEMAP = {
|
|
26
|
+
bool: pa.bool_(),
|
|
27
|
+
int: pa.int64(),
|
|
28
|
+
float: pa.float64(),
|
|
29
|
+
str: pa.string(),
|
|
30
|
+
bytes: pa.binary(),
|
|
31
|
+
datetime: pa.timestamp("us"),
|
|
32
|
+
date: pa.date32(),
|
|
33
|
+
time: pa.time64("us"),
|
|
34
|
+
Decimal: pa.decimal128(38, 9),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def arrow(cls):
|
|
39
|
+
return model(dataclass(kw_only=True)(cls))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def table(cls, rows) -> pa.Table:
|
|
43
|
+
return pa.Table.from_pylist(
|
|
44
|
+
[_record(cls, row) for row in rows], schema=cls.__arrow__
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _record(cls: Type, row) -> Dict[str, Any]:
|
|
49
|
+
field_names = [field.name for field in fields(cls)]
|
|
50
|
+
if is_dataclass(row):
|
|
51
|
+
return {name: getattr(row, name) for name in field_names}
|
|
52
|
+
if isinstance(row, Mapping):
|
|
53
|
+
return {name: row[name] for name in field_names}
|
|
54
|
+
raise TypeError(f"Expected {cls.__name__} instance or mapping, got {type(row)!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_optional(origin, args) -> bool:
|
|
58
|
+
return origin in UNION_TYPES and type(None) in args
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _arrow_type(annotation) -> Tuple[pa.DataType, bool]:
|
|
62
|
+
if isinstance(annotation, pa.DataType):
|
|
63
|
+
return annotation, False
|
|
64
|
+
|
|
65
|
+
origin = get_origin(annotation)
|
|
66
|
+
args = get_args(annotation)
|
|
67
|
+
|
|
68
|
+
if _is_optional(origin, args):
|
|
69
|
+
non_none = [arg for arg in args if arg is not type(None)]
|
|
70
|
+
if len(non_none) != 1:
|
|
71
|
+
raise TypeError(f"Unsupported union type {annotation!r}")
|
|
72
|
+
arrow_type, _ = _arrow_type(non_none[0])
|
|
73
|
+
return arrow_type, True
|
|
74
|
+
|
|
75
|
+
if origin in (list, List):
|
|
76
|
+
if len(args) != 1:
|
|
77
|
+
raise TypeError(f"List fields must specify one item type: {annotation!r}")
|
|
78
|
+
item_type, _ = _arrow_type(args[0])
|
|
79
|
+
return pa.list_(item_type), False
|
|
80
|
+
|
|
81
|
+
if origin in (dict, Dict):
|
|
82
|
+
if len(args) != 2:
|
|
83
|
+
raise TypeError(
|
|
84
|
+
f"Dict fields must specify key and value types: {annotation!r}"
|
|
85
|
+
)
|
|
86
|
+
key_type, _ = _arrow_type(args[0])
|
|
87
|
+
item_type, _ = _arrow_type(args[1])
|
|
88
|
+
return pa.map_(key_type, item_type), False
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return TYPEMAP[annotation], False
|
|
92
|
+
except KeyError:
|
|
93
|
+
raise TypeError(f"Unsupported Arrow field type {annotation!r}") from None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _nullable(field, annotation_nullable: bool) -> bool:
|
|
97
|
+
if annotation_nullable:
|
|
98
|
+
return True
|
|
99
|
+
return field.default is None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _schema_field(field, annotation) -> pa.Field:
|
|
103
|
+
arrow_type, annotation_nullable = _arrow_type(annotation)
|
|
104
|
+
return pa.field(
|
|
105
|
+
field.name, arrow_type, nullable=_nullable(field, annotation_nullable)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def model(cls: Type) -> Type:
|
|
110
|
+
"""
|
|
111
|
+
Decorator to convert a dataclass to a PyArrow-backed row type.
|
|
112
|
+
"""
|
|
113
|
+
type_hints = get_type_hints(cls)
|
|
114
|
+
cls.__arrow__ = pa.schema(
|
|
115
|
+
[
|
|
116
|
+
_schema_field(field, type_hints.get(field.name, field.type))
|
|
117
|
+
for field in fields(cls)
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
cls.to_arrow = classmethod(table)
|
|
121
|
+
return cls
|
|
@@ -8,7 +8,11 @@ from contextlib import contextmanager
|
|
|
8
8
|
|
|
9
9
|
@contextmanager
|
|
10
10
|
def _get_event_loop():
|
|
11
|
-
|
|
11
|
+
try:
|
|
12
|
+
loop = asyncio.get_running_loop()
|
|
13
|
+
except RuntimeError:
|
|
14
|
+
loop = asyncio.new_event_loop()
|
|
15
|
+
asyncio.set_event_loop(loop)
|
|
12
16
|
if not loop.is_running():
|
|
13
17
|
yield loop
|
|
14
18
|
else:
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright (c) Arun Sharma, 2025
|
|
2
|
+
#
|
|
3
|
+
# This source code is licensed under the MIT license found in the
|
|
4
|
+
# LICENSE file in the root directory of this source tree.
|
|
5
|
+
import ast
|
|
6
|
+
import operator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .naming import query_type_name
|
|
10
|
+
from .visitor import Visitor
|
|
11
|
+
|
|
12
|
+
# inspired from pandas.core.computation.ops
|
|
13
|
+
_cmp_ops_syms = (">", "<", ">=", "<=", "==", "!=")
|
|
14
|
+
_cmp_ops_funcs = (
|
|
15
|
+
operator.gt,
|
|
16
|
+
operator.lt,
|
|
17
|
+
operator.ge,
|
|
18
|
+
operator.le,
|
|
19
|
+
operator.eq,
|
|
20
|
+
operator.ne,
|
|
21
|
+
)
|
|
22
|
+
_cmp_ops_dict = dict(zip(_cmp_ops_syms, _cmp_ops_funcs))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CypherBuilderVisitor(Visitor):
|
|
26
|
+
def __init__(self, id1s):
|
|
27
|
+
self.cypher = None
|
|
28
|
+
self.match_parts = []
|
|
29
|
+
self.current_node = "u"
|
|
30
|
+
self.where_clauses = []
|
|
31
|
+
self.return_clause = ""
|
|
32
|
+
self.order_by_clause = ""
|
|
33
|
+
self.limit_clause = ""
|
|
34
|
+
self.visited = set()
|
|
35
|
+
self.node_counter = 0
|
|
36
|
+
self.root_label = None
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def table_from_query(query):
|
|
40
|
+
return query_type_name(query.__class__)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _cypher_literal(value: Any) -> str:
|
|
44
|
+
if value is None:
|
|
45
|
+
return "null"
|
|
46
|
+
if isinstance(value, bool):
|
|
47
|
+
return "true" if value else "false"
|
|
48
|
+
if isinstance(value, (int, float)):
|
|
49
|
+
return str(value)
|
|
50
|
+
if isinstance(value, str):
|
|
51
|
+
escaped = value.replace("\\", "\\\\").replace("'", "\\'")
|
|
52
|
+
return f"'{escaped}'"
|
|
53
|
+
raise TypeError(f"Unsupported Cypher property value {value!r}")
|
|
54
|
+
|
|
55
|
+
def _node_pattern(self, var: str, label: str, props=None) -> str:
|
|
56
|
+
if not props:
|
|
57
|
+
return f"({var}:{label})"
|
|
58
|
+
prop_list = ", ".join(
|
|
59
|
+
f"{name}: {self._cypher_literal(value)}" for name, value in props.items()
|
|
60
|
+
)
|
|
61
|
+
return f"({var}:{label} {{{prop_list}}})"
|
|
62
|
+
|
|
63
|
+
def _get_next_node_var(self):
|
|
64
|
+
self.node_counter += 1
|
|
65
|
+
return f"n{self.node_counter}"
|
|
66
|
+
|
|
67
|
+
async def visit_leaf(self, query):
|
|
68
|
+
if not self.root_label:
|
|
69
|
+
self.root_label = self.table_from_query(query)
|
|
70
|
+
self.match_parts = [
|
|
71
|
+
self._node_pattern(
|
|
72
|
+
self.current_node,
|
|
73
|
+
self.root_label,
|
|
74
|
+
getattr(query, "_match_props", None),
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if query in self.visited:
|
|
79
|
+
# Prevent infinite recursion
|
|
80
|
+
return
|
|
81
|
+
else:
|
|
82
|
+
self.visited.add(query)
|
|
83
|
+
for q in query.edges:
|
|
84
|
+
await self.visit(q)
|
|
85
|
+
|
|
86
|
+
async def visit_project(self, query):
|
|
87
|
+
await self.visit(query.child)
|
|
88
|
+
proj = ", ".join(
|
|
89
|
+
[
|
|
90
|
+
f"{self.current_node}.{x}" if x != ":id" else f"{self.current_node}.id"
|
|
91
|
+
for x in query.projector
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
self.return_clause = f"RETURN {proj}"
|
|
95
|
+
|
|
96
|
+
async def visit_take(self, query):
|
|
97
|
+
await self.visit(query.child)
|
|
98
|
+
self.limit_clause = f"LIMIT {query._count}"
|
|
99
|
+
|
|
100
|
+
async def visit_where(self, query):
|
|
101
|
+
await self.visit(query.child)
|
|
102
|
+
# TODO: more general lazy expression evaluator
|
|
103
|
+
left, op, right = query._expr.value.split()
|
|
104
|
+
right = ast.literal_eval(right)
|
|
105
|
+
table, field = left.split(".") if "." in left else (self.cypher, left)
|
|
106
|
+
self.where_clauses.append(f"{self.current_node}.{field} {op} {right}")
|
|
107
|
+
|
|
108
|
+
async def visit_order_by(self, query):
|
|
109
|
+
await self.visit(query.child)
|
|
110
|
+
key = query._expr.value
|
|
111
|
+
table, field = key.split(".") if "." in key else (self.cypher, key)
|
|
112
|
+
self.order_by_clause = f"ORDER BY {self.current_node}.{field}"
|
|
113
|
+
|
|
114
|
+
async def visit_edge(self, query):
|
|
115
|
+
# Ensure we have the root label and initial match part
|
|
116
|
+
if not self.root_label:
|
|
117
|
+
# Find the root query
|
|
118
|
+
root_query = query
|
|
119
|
+
while hasattr(root_query, "child") and root_query.child:
|
|
120
|
+
root_query = root_query.child
|
|
121
|
+
if hasattr(root_query, "__class__"):
|
|
122
|
+
self.root_label = self.table_from_query(root_query)
|
|
123
|
+
self.match_parts = [
|
|
124
|
+
self._node_pattern(
|
|
125
|
+
self.current_node,
|
|
126
|
+
self.root_label,
|
|
127
|
+
getattr(root_query, "_match_props", None),
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
edge_name = query.edge_name
|
|
132
|
+
|
|
133
|
+
# Check if this is part of a multi-hop pattern of the same edge type
|
|
134
|
+
# Look ahead to see if the child has another edge of the same type
|
|
135
|
+
has_same_edge_child = (
|
|
136
|
+
hasattr(query, "child")
|
|
137
|
+
and hasattr(query.child, "OP")
|
|
138
|
+
and query.child.OP.name == "EDGE"
|
|
139
|
+
and query.child.edge_name == edge_name
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if has_same_edge_child:
|
|
143
|
+
root_query = query
|
|
144
|
+
while hasattr(root_query, "child") and root_query.child:
|
|
145
|
+
root_query = root_query.child
|
|
146
|
+
# Count the total number of consecutive edges of the same type
|
|
147
|
+
hops = 1 # current edge
|
|
148
|
+
current_query = query.child
|
|
149
|
+
while (
|
|
150
|
+
hasattr(current_query, "OP")
|
|
151
|
+
and current_query.OP.name == "EDGE"
|
|
152
|
+
and current_query.edge_name == edge_name
|
|
153
|
+
):
|
|
154
|
+
hops += 1
|
|
155
|
+
current_query = current_query.child
|
|
156
|
+
|
|
157
|
+
# This is the start of a multi-hop pattern (e.g., friend-of-friend, etc.)
|
|
158
|
+
self.match_parts = [
|
|
159
|
+
self._node_pattern(
|
|
160
|
+
"a",
|
|
161
|
+
self.root_label,
|
|
162
|
+
getattr(root_query, "_match_props", None),
|
|
163
|
+
),
|
|
164
|
+
f"[e:{edge_name.upper()}*{hops}..{hops}]",
|
|
165
|
+
f"(b:{self.root_label})",
|
|
166
|
+
]
|
|
167
|
+
self.current_node = "b"
|
|
168
|
+
# Skip visiting the intermediate edges and visit the query after the chain
|
|
169
|
+
await self.visit(current_query)
|
|
170
|
+
else:
|
|
171
|
+
# Regular edge traversal
|
|
172
|
+
child_label = self.table_from_query(query._unbound)
|
|
173
|
+
next_node = self._get_next_node_var()
|
|
174
|
+
relationship = f"[:{edge_name.upper()}]"
|
|
175
|
+
self.match_parts.append(f"{relationship}->({next_node}:{child_label})")
|
|
176
|
+
self.current_node = next_node
|
|
177
|
+
await self.visit(query.child)
|
|
178
|
+
|
|
179
|
+
async def visit_union(self, query):
|
|
180
|
+
# UNION in Cypher - this is complex, would need to handle multiple queries
|
|
181
|
+
# For now, just visit the child
|
|
182
|
+
await self.visit(query.child)
|
|
183
|
+
|
|
184
|
+
async def visit_count(self, query):
|
|
185
|
+
await self.visit(query.child)
|
|
186
|
+
self.return_clause = "RETURN count(*)"
|
|
187
|
+
|
|
188
|
+
async def visit_nest(self, query):
|
|
189
|
+
# Nesting - not directly supported in Cypher
|
|
190
|
+
await self.visit(query.child)
|
|
191
|
+
|
|
192
|
+
async def visit_let(self, query):
|
|
193
|
+
# LET - for renaming, could be handled with AS in RETURN
|
|
194
|
+
await self.visit(query.child)
|