pytrilogy 0.0.2.25__py3-none-any.whl → 0.0.2.27__py3-none-any.whl
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.
Potentially problematic release.
This version of pytrilogy might be problematic. Click here for more details.
- {pytrilogy-0.0.2.25.dist-info → pytrilogy-0.0.2.27.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.25.dist-info → pytrilogy-0.0.2.27.dist-info}/RECORD +26 -26
- {pytrilogy-0.0.2.25.dist-info → pytrilogy-0.0.2.27.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -1
- trilogy/core/graph_models.py +2 -2
- trilogy/core/models.py +205 -140
- trilogy/core/optimizations/inline_datasource.py +4 -4
- trilogy/core/processing/node_generators/common.py +0 -1
- trilogy/core/processing/node_generators/select_merge_node.py +56 -23
- trilogy/core/processing/nodes/base_node.py +3 -0
- trilogy/core/processing/nodes/merge_node.py +12 -12
- trilogy/core/processing/nodes/select_node_v2.py +6 -2
- trilogy/core/processing/utility.py +237 -258
- trilogy/core/query_processor.py +65 -53
- trilogy/dialect/base.py +1 -0
- trilogy/dialect/common.py +4 -25
- trilogy/executor.py +12 -3
- trilogy/hooks/query_debugger.py +5 -1
- trilogy/parsing/common.py +4 -6
- trilogy/parsing/parse_engine.py +20 -16
- trilogy/parsing/render.py +63 -21
- trilogy/parsing/trilogy.lark +6 -4
- {pytrilogy-0.0.2.25.dist-info → pytrilogy-0.0.2.27.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.25.dist-info → pytrilogy-0.0.2.27.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.25.dist-info → pytrilogy-0.0.2.27.dist-info}/top_level.txt +0 -0
trilogy/core/query_processor.py
CHANGED
|
@@ -5,7 +5,7 @@ from trilogy.core.graph_models import ReferenceGraph
|
|
|
5
5
|
from trilogy.core.constants import CONSTANT_DATASET
|
|
6
6
|
from trilogy.core.processing.concept_strategies_v3 import source_query_concepts
|
|
7
7
|
from trilogy.core.enums import BooleanOperator
|
|
8
|
-
from trilogy.constants import CONFIG
|
|
8
|
+
from trilogy.constants import CONFIG
|
|
9
9
|
from trilogy.core.processing.nodes import SelectNode, StrategyNode, History
|
|
10
10
|
from trilogy.core.models import (
|
|
11
11
|
Concept,
|
|
@@ -17,7 +17,6 @@ from trilogy.core.models import (
|
|
|
17
17
|
CTE,
|
|
18
18
|
Join,
|
|
19
19
|
UnnestJoin,
|
|
20
|
-
JoinKey,
|
|
21
20
|
MaterializedDataset,
|
|
22
21
|
ProcessedQuery,
|
|
23
22
|
ProcessedQueryPersist,
|
|
@@ -28,6 +27,7 @@ from trilogy.core.models import (
|
|
|
28
27
|
Conditional,
|
|
29
28
|
ProcessedCopyStatement,
|
|
30
29
|
CopyStatement,
|
|
30
|
+
CTEConceptPair,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
from trilogy.utility import unique
|
|
@@ -52,44 +52,53 @@ def base_join_to_join(
|
|
|
52
52
|
concept_to_unnest=base_join.parent.concept_arguments[0],
|
|
53
53
|
alias=base_join.alias,
|
|
54
54
|
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
|
|
56
|
+
def get_datasource_cte(datasource: Datasource | QueryDatasource) -> CTE:
|
|
57
|
+
for cte in ctes:
|
|
58
|
+
if cte.source.identifier == datasource.identifier:
|
|
59
|
+
return cte
|
|
60
|
+
for cte in ctes:
|
|
61
|
+
if cte.source.datasources[0].identifier == datasource.identifier:
|
|
62
|
+
return cte
|
|
63
|
+
raise ValueError(f"Could not find CTE for datasource {datasource.identifier}")
|
|
64
|
+
|
|
65
|
+
if base_join.left_datasource is not None:
|
|
66
|
+
left_cte = get_datasource_cte(base_join.left_datasource)
|
|
67
|
+
else:
|
|
68
|
+
# multiple left ctes
|
|
69
|
+
left_cte = None
|
|
70
|
+
right_cte = get_datasource_cte(base_join.right_datasource)
|
|
71
|
+
if base_join.concept_pairs:
|
|
72
|
+
final_pairs = [
|
|
73
|
+
CTEConceptPair(
|
|
74
|
+
left=pair.left,
|
|
75
|
+
right=pair.right,
|
|
76
|
+
existing_datasource=pair.existing_datasource,
|
|
77
|
+
modifiers=pair.modifiers,
|
|
78
|
+
cte=get_datasource_cte(pair.existing_datasource),
|
|
69
79
|
)
|
|
80
|
+
for pair in base_join.concept_pairs
|
|
70
81
|
]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
cte.source.datasources[0].full_name
|
|
83
|
-
== base_join.right_datasource.full_name
|
|
82
|
+
elif base_join.concepts and base_join.left_datasource:
|
|
83
|
+
final_pairs = [
|
|
84
|
+
CTEConceptPair(
|
|
85
|
+
left=concept,
|
|
86
|
+
right=concept,
|
|
87
|
+
existing_datasource=base_join.left_datasource,
|
|
88
|
+
modifiers=[],
|
|
89
|
+
cte=get_datasource_cte(
|
|
90
|
+
base_join.left_datasource,
|
|
91
|
+
),
|
|
84
92
|
)
|
|
93
|
+
for concept in base_join.concepts
|
|
85
94
|
]
|
|
86
|
-
|
|
95
|
+
else:
|
|
96
|
+
final_pairs = []
|
|
87
97
|
return Join(
|
|
88
98
|
left_cte=left_cte,
|
|
89
99
|
right_cte=right_cte,
|
|
90
|
-
joinkeys=[JoinKey(concept=concept) for concept in base_join.concepts],
|
|
91
100
|
jointype=base_join.join_type,
|
|
92
|
-
joinkey_pairs=
|
|
101
|
+
joinkey_pairs=final_pairs,
|
|
93
102
|
)
|
|
94
103
|
|
|
95
104
|
|
|
@@ -100,7 +109,7 @@ def generate_source_map(
|
|
|
100
109
|
# now populate anything derived in this level
|
|
101
110
|
for qdk, qdv in query_datasource.source_map.items():
|
|
102
111
|
unnest = [x for x in qdv if isinstance(x, UnnestJoin)]
|
|
103
|
-
for
|
|
112
|
+
for _ in unnest:
|
|
104
113
|
source_map[qdk] = []
|
|
105
114
|
if (
|
|
106
115
|
qdk not in source_map
|
|
@@ -110,16 +119,18 @@ def generate_source_map(
|
|
|
110
119
|
source_map[qdk] = []
|
|
111
120
|
basic = [x for x in qdv if isinstance(x, Datasource)]
|
|
112
121
|
for base in basic:
|
|
113
|
-
source_map[qdk].append(base.
|
|
122
|
+
source_map[qdk].append(base.safe_identifier)
|
|
114
123
|
|
|
115
124
|
ctes = [x for x in qdv if isinstance(x, QueryDatasource)]
|
|
116
125
|
if ctes:
|
|
117
|
-
names = set([x.
|
|
118
|
-
matches = [
|
|
126
|
+
names = set([x.safe_identifier for x in ctes])
|
|
127
|
+
matches = [
|
|
128
|
+
cte for cte in all_new_ctes if cte.source.safe_identifier in names
|
|
129
|
+
]
|
|
119
130
|
|
|
120
131
|
if not matches and names:
|
|
121
132
|
raise SyntaxError(
|
|
122
|
-
f"Missing parent CTEs for source map; expecting {names}, have {[cte.source.
|
|
133
|
+
f"Missing parent CTEs for source map; expecting {names}, have {[cte.source.safe_identifier for cte in all_new_ctes]}"
|
|
123
134
|
)
|
|
124
135
|
for cte in matches:
|
|
125
136
|
output_address = [
|
|
@@ -128,11 +139,11 @@ def generate_source_map(
|
|
|
128
139
|
if x.address not in [z.address for z in cte.partial_concepts]
|
|
129
140
|
]
|
|
130
141
|
if qdk in output_address:
|
|
131
|
-
source_map[qdk].append(cte.
|
|
142
|
+
source_map[qdk].append(cte.safe_identifier)
|
|
132
143
|
# now do a pass that accepts partials
|
|
133
144
|
for cte in matches:
|
|
134
145
|
if qdk not in source_map:
|
|
135
|
-
source_map[qdk] = [cte.
|
|
146
|
+
source_map[qdk] = [cte.safe_identifier]
|
|
136
147
|
if qdk not in source_map:
|
|
137
148
|
if not qdv:
|
|
138
149
|
source_map[qdk] = []
|
|
@@ -145,8 +156,10 @@ def generate_source_map(
|
|
|
145
156
|
# as they cannot be referenced in row resolution
|
|
146
157
|
existence_source_map: Dict[str, list[str]] = defaultdict(list)
|
|
147
158
|
for ek, ev in query_datasource.existence_source_map.items():
|
|
148
|
-
|
|
149
|
-
ematches = [
|
|
159
|
+
ids = set([x.safe_identifier for x in ev])
|
|
160
|
+
ematches = [
|
|
161
|
+
cte.name for cte in all_new_ctes if cte.source.safe_identifier in ids
|
|
162
|
+
]
|
|
150
163
|
existence_source_map[ek] = ematches
|
|
151
164
|
return {
|
|
152
165
|
k: [] if not v else list(set(v)) for k, v in source_map.items()
|
|
@@ -195,16 +208,19 @@ def resolve_cte_base_name_and_alias_v2(
|
|
|
195
208
|
source_map: Dict[str, list[str]],
|
|
196
209
|
raw_joins: List[Join | InstantiatedUnnestJoin],
|
|
197
210
|
) -> Tuple[str | None, str | None]:
|
|
198
|
-
joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
|
|
199
211
|
if (
|
|
200
212
|
isinstance(source.datasources[0], Datasource)
|
|
201
213
|
and not source.datasources[0].name == CONSTANT_DATASET
|
|
202
214
|
):
|
|
203
215
|
ds = source.datasources[0]
|
|
204
|
-
return ds.safe_location, ds.
|
|
216
|
+
return ds.safe_location, ds.safe_identifier
|
|
205
217
|
|
|
218
|
+
joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
|
|
206
219
|
if joins and len(joins) > 0:
|
|
207
|
-
candidates = [x.left_cte.name for x in joins]
|
|
220
|
+
candidates = [x.left_cte.name for x in joins if x.left_cte]
|
|
221
|
+
for join in joins:
|
|
222
|
+
if join.joinkey_pairs:
|
|
223
|
+
candidates += [x.cte.name for x in join.joinkey_pairs if x.cte]
|
|
208
224
|
disallowed = [x.right_cte.name for x in joins]
|
|
209
225
|
try:
|
|
210
226
|
cte = [y for y in candidates if y not in disallowed][0]
|
|
@@ -213,7 +229,6 @@ def resolve_cte_base_name_and_alias_v2(
|
|
|
213
229
|
raise SyntaxError(
|
|
214
230
|
f"Invalid join configuration {candidates} {disallowed} for {name}",
|
|
215
231
|
)
|
|
216
|
-
|
|
217
232
|
counts: dict[str, int] = defaultdict(lambda: 0)
|
|
218
233
|
output_addresses = [x.address for x in source.output_concepts]
|
|
219
234
|
input_address = [x.address for x in source.input_concepts]
|
|
@@ -257,23 +272,20 @@ def datasource_to_ctes(
|
|
|
257
272
|
# this is required to ensure that constant datasets
|
|
258
273
|
# render properly on initial access; since they have
|
|
259
274
|
# no actual source
|
|
260
|
-
if source.
|
|
275
|
+
if source.name == CONSTANT_DATASET:
|
|
261
276
|
source_map = {k: [] for k in query_datasource.source_map}
|
|
262
277
|
existence_map = source_map
|
|
263
278
|
else:
|
|
264
279
|
source_map = {
|
|
265
|
-
k: [] if not v else [source.
|
|
280
|
+
k: [] if not v else [source.safe_identifier]
|
|
266
281
|
for k, v in query_datasource.source_map.items()
|
|
267
282
|
}
|
|
268
283
|
existence_map = source_map
|
|
269
284
|
|
|
270
|
-
human_id = generate_cte_name(query_datasource.
|
|
285
|
+
human_id = generate_cte_name(query_datasource.identifier, name_map)
|
|
286
|
+
|
|
287
|
+
final_joins = [base_join_to_join(join, parents) for join in query_datasource.joins]
|
|
271
288
|
|
|
272
|
-
final_joins = [
|
|
273
|
-
x
|
|
274
|
-
for x in [base_join_to_join(join, parents) for join in query_datasource.joins]
|
|
275
|
-
if x
|
|
276
|
-
]
|
|
277
289
|
base_name, base_alias = resolve_cte_base_name_and_alias_v2(
|
|
278
290
|
human_id, query_datasource, source_map, final_joins
|
|
279
291
|
)
|
trilogy/dialect/base.py
CHANGED
trilogy/dialect/common.py
CHANGED
|
@@ -68,39 +68,18 @@ def render_join(
|
|
|
68
68
|
if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
|
|
69
69
|
return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
|
|
70
70
|
return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
|
|
71
|
-
left_name = join.left_name
|
|
71
|
+
# left_name = join.left_name
|
|
72
72
|
right_name = join.right_name
|
|
73
73
|
right_base = join.right_ref
|
|
74
|
-
base_joinkeys = [
|
|
75
|
-
null_wrapper(
|
|
76
|
-
render_join_concept(
|
|
77
|
-
left_name,
|
|
78
|
-
quote_character,
|
|
79
|
-
join.left_cte,
|
|
80
|
-
key.concept,
|
|
81
|
-
render_expr_func,
|
|
82
|
-
join.inlined_ctes,
|
|
83
|
-
),
|
|
84
|
-
render_join_concept(
|
|
85
|
-
right_name,
|
|
86
|
-
quote_character,
|
|
87
|
-
join.right_cte,
|
|
88
|
-
key.concept,
|
|
89
|
-
render_expr_func,
|
|
90
|
-
join.inlined_ctes,
|
|
91
|
-
),
|
|
92
|
-
modifiers=key.concept.modifiers or [],
|
|
93
|
-
)
|
|
94
|
-
for key in join.joinkeys
|
|
95
|
-
]
|
|
74
|
+
base_joinkeys = []
|
|
96
75
|
if join.joinkey_pairs:
|
|
97
76
|
base_joinkeys.extend(
|
|
98
77
|
[
|
|
99
78
|
null_wrapper(
|
|
100
79
|
render_join_concept(
|
|
101
|
-
|
|
80
|
+
join.get_name(pair.cte),
|
|
102
81
|
quote_character,
|
|
103
|
-
|
|
82
|
+
pair.cte,
|
|
104
83
|
pair.left,
|
|
105
84
|
render_expr_func,
|
|
106
85
|
join.inlined_ctes,
|
trilogy/executor.py
CHANGED
|
@@ -334,7 +334,9 @@ class Executor(object):
|
|
|
334
334
|
text(command),
|
|
335
335
|
)
|
|
336
336
|
|
|
337
|
-
def execute_text(
|
|
337
|
+
def execute_text(
|
|
338
|
+
self, command: str, non_interactive: bool = False
|
|
339
|
+
) -> List[CursorResult]:
|
|
338
340
|
"""Run a preql text command"""
|
|
339
341
|
output = []
|
|
340
342
|
# connection = self.engine.connect()
|
|
@@ -351,11 +353,18 @@ class Executor(object):
|
|
|
351
353
|
)
|
|
352
354
|
)
|
|
353
355
|
continue
|
|
356
|
+
if non_interactive:
|
|
357
|
+
if not isinstance(
|
|
358
|
+
statement, (ProcessedCopyStatement, ProcessedQueryPersist)
|
|
359
|
+
):
|
|
360
|
+
continue
|
|
354
361
|
output.append(self.execute_query(statement))
|
|
355
362
|
return output
|
|
356
363
|
|
|
357
|
-
def execute_file(
|
|
364
|
+
def execute_file(
|
|
365
|
+
self, file: str | Path, non_interactive: bool = False
|
|
366
|
+
) -> List[CursorResult]:
|
|
358
367
|
file = Path(file)
|
|
359
368
|
with open(file, "r") as f:
|
|
360
369
|
command = f.read()
|
|
361
|
-
return self.execute_text(command)
|
|
370
|
+
return self.execute_text(command, non_interactive=non_interactive)
|
trilogy/hooks/query_debugger.py
CHANGED
trilogy/parsing/common.py
CHANGED
|
@@ -292,21 +292,19 @@ def arbitrary_to_concept(
|
|
|
292
292
|
|
|
293
293
|
if isinstance(parent, AggregateWrapper):
|
|
294
294
|
if not name:
|
|
295
|
-
name = (
|
|
296
|
-
f"_agg_{parent.function.operator.value}_{string_to_hash(str(parent))}"
|
|
297
|
-
)
|
|
295
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_agg_{parent.function.operator.value}_{string_to_hash(str(parent))}"
|
|
298
296
|
return agg_wrapper_to_concept(parent, namespace, name, metadata, purpose)
|
|
299
297
|
elif isinstance(parent, WindowItem):
|
|
300
298
|
if not name:
|
|
301
|
-
name = f"_window_{parent.type.value}_{string_to_hash(str(parent))}"
|
|
299
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_window_{parent.type.value}_{string_to_hash(str(parent))}"
|
|
302
300
|
return window_item_to_concept(parent, name, namespace, purpose, metadata)
|
|
303
301
|
elif isinstance(parent, FilterItem):
|
|
304
302
|
if not name:
|
|
305
|
-
name = f"_filter_{parent.content.name}_{string_to_hash(str(parent))}"
|
|
303
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{parent.content.name}_{string_to_hash(str(parent))}"
|
|
306
304
|
return filter_item_to_concept(parent, name, namespace, purpose, metadata)
|
|
307
305
|
elif isinstance(parent, Function):
|
|
308
306
|
if not name:
|
|
309
|
-
name = f"_func_{parent.operator.value}_{string_to_hash(str(parent))}"
|
|
307
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_func_{parent.operator.value}_{string_to_hash(str(parent))}"
|
|
310
308
|
return function_to_concept(parent, name, namespace)
|
|
311
309
|
elif isinstance(parent, ListWrapper):
|
|
312
310
|
if not name:
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -123,6 +123,13 @@ from trilogy.parsing.common import (
|
|
|
123
123
|
arbitrary_to_concept,
|
|
124
124
|
process_function_args,
|
|
125
125
|
)
|
|
126
|
+
from dataclasses import dataclass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class WholeGrainWrapper:
|
|
131
|
+
where: WhereClause
|
|
132
|
+
|
|
126
133
|
|
|
127
134
|
CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
|
|
128
135
|
|
|
@@ -254,8 +261,9 @@ class ParseToObjects(Transformer):
|
|
|
254
261
|
def IDENTIFIER(self, args) -> str:
|
|
255
262
|
return args.value
|
|
256
263
|
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
@v_args(meta=True)
|
|
265
|
+
def concept_lit(self, meta: Meta, args) -> Concept:
|
|
266
|
+
return self.environment.concepts.__getitem__(args[0], meta.line)
|
|
259
267
|
|
|
260
268
|
def ADDRESS(self, args) -> Address:
|
|
261
269
|
return Address(location=args.value, quoted=False)
|
|
@@ -565,9 +573,11 @@ class ParseToObjects(Transformer):
|
|
|
565
573
|
return args
|
|
566
574
|
|
|
567
575
|
def grain_clause(self, args) -> Grain:
|
|
568
|
-
# namespace=self.environment.namespace,
|
|
569
576
|
return Grain(components=[self.environment.concepts[a] for a in args[0]])
|
|
570
577
|
|
|
578
|
+
def whole_grain_clause(self, args) -> WholeGrainWrapper:
|
|
579
|
+
return WholeGrainWrapper(where=args[0])
|
|
580
|
+
|
|
571
581
|
def MULTILINE_STRING(self, args) -> str:
|
|
572
582
|
return args[3:-3]
|
|
573
583
|
|
|
@@ -581,11 +591,14 @@ class ParseToObjects(Transformer):
|
|
|
581
591
|
grain: Optional[Grain] = None
|
|
582
592
|
address: Optional[Address] = None
|
|
583
593
|
where: Optional[WhereClause] = None
|
|
594
|
+
non_partial_for: Optional[WhereClause] = None
|
|
584
595
|
for val in args[1:]:
|
|
585
596
|
if isinstance(val, Address):
|
|
586
597
|
address = val
|
|
587
598
|
elif isinstance(val, Grain):
|
|
588
599
|
grain = val
|
|
600
|
+
elif isinstance(val, WholeGrainWrapper):
|
|
601
|
+
non_partial_for = val.where
|
|
589
602
|
elif isinstance(val, Query):
|
|
590
603
|
address = Address(location=f"({val.text})", is_query=True)
|
|
591
604
|
elif isinstance(val, WhereClause):
|
|
@@ -595,7 +608,7 @@ class ParseToObjects(Transformer):
|
|
|
595
608
|
"Malformed datasource, missing address or query declaration"
|
|
596
609
|
)
|
|
597
610
|
datasource = Datasource(
|
|
598
|
-
|
|
611
|
+
name=name,
|
|
599
612
|
columns=columns,
|
|
600
613
|
# grain will be set by default from args
|
|
601
614
|
# TODO: move to factory
|
|
@@ -603,6 +616,7 @@ class ParseToObjects(Transformer):
|
|
|
603
616
|
address=address,
|
|
604
617
|
namespace=self.environment.namespace,
|
|
605
618
|
where=where,
|
|
619
|
+
non_partial_for=non_partial_for,
|
|
606
620
|
)
|
|
607
621
|
for column in columns:
|
|
608
622
|
column.concept = column.concept.with_grain(datasource.grain)
|
|
@@ -800,20 +814,10 @@ class ParseToObjects(Transformer):
|
|
|
800
814
|
except Exception as e:
|
|
801
815
|
raise ImportError(f"Unable to import file {target}, parsing error: {e}")
|
|
802
816
|
|
|
803
|
-
for _, concept in nparser.environment.concepts.items():
|
|
804
|
-
self.environment.add_concept(
|
|
805
|
-
concept.with_namespace(alias), _ignore_cache=True
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
for _, datasource in nparser.environment.datasources.items():
|
|
809
|
-
self.environment.add_datasource(
|
|
810
|
-
datasource.with_namespace(alias), _ignore_cache=True
|
|
811
|
-
)
|
|
812
817
|
imps = ImportStatement(
|
|
813
818
|
alias=alias, path=Path(args[0]), environment=nparser.environment
|
|
814
819
|
)
|
|
815
|
-
self.environment.
|
|
816
|
-
self.environment.gen_concept_list_caches()
|
|
820
|
+
self.environment.add_import(alias, nparser.environment, imps)
|
|
817
821
|
return imps
|
|
818
822
|
|
|
819
823
|
@v_args(meta=True)
|
|
@@ -840,7 +844,7 @@ class ParseToObjects(Transformer):
|
|
|
840
844
|
if self.environment.namespace
|
|
841
845
|
else DEFAULT_NAMESPACE
|
|
842
846
|
),
|
|
843
|
-
|
|
847
|
+
name=identifier,
|
|
844
848
|
address=Address(location=address),
|
|
845
849
|
grain=grain,
|
|
846
850
|
)
|
trilogy/parsing/render.py
CHANGED
|
@@ -17,6 +17,7 @@ from trilogy.core.models import (
|
|
|
17
17
|
SelectItem,
|
|
18
18
|
WhereClause,
|
|
19
19
|
Conditional,
|
|
20
|
+
SubselectComparison,
|
|
20
21
|
Comparison,
|
|
21
22
|
Environment,
|
|
22
23
|
ConceptDeclarationStatement,
|
|
@@ -25,6 +26,7 @@ from trilogy.core.models import (
|
|
|
25
26
|
WindowItem,
|
|
26
27
|
FilterItem,
|
|
27
28
|
ColumnAssignment,
|
|
29
|
+
RawColumnExpr,
|
|
28
30
|
CaseElse,
|
|
29
31
|
CaseWhen,
|
|
30
32
|
ImportStatement,
|
|
@@ -50,13 +52,16 @@ from collections import defaultdict
|
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
QUERY_TEMPLATE = Template(
|
|
53
|
-
"""
|
|
54
|
-
{{
|
|
55
|
-
|
|
56
|
-
{{
|
|
55
|
+
"""{% if where %}WHERE
|
|
56
|
+
{{ where }}
|
|
57
|
+
{% endif %}SELECT{%- for select in select_columns %}
|
|
58
|
+
{{ select }},{% endfor %}{% if having %}
|
|
59
|
+
HAVING
|
|
60
|
+
{{ having }}
|
|
61
|
+
{% endif %}{%- if order_by %}
|
|
57
62
|
ORDER BY{% for order in order_by %}
|
|
58
|
-
{{ order }}{% if not loop.last %},{% endif %}
|
|
59
|
-
{%
|
|
63
|
+
{{ order }}{% if not loop.last %},{% endif %}{% endfor %}
|
|
64
|
+
{% endif %}{%- if limit is not none %}
|
|
60
65
|
LIMIT {{ limit }}{% endif %};"""
|
|
61
66
|
)
|
|
62
67
|
|
|
@@ -75,6 +80,9 @@ class Renderer:
|
|
|
75
80
|
metrics = []
|
|
76
81
|
# first, keys
|
|
77
82
|
for concept in arg.concepts.values():
|
|
83
|
+
if "__preql_internal" in concept.address:
|
|
84
|
+
continue
|
|
85
|
+
|
|
78
86
|
# don't render anything that came from an import
|
|
79
87
|
if concept.namespace in arg.imports:
|
|
80
88
|
continue
|
|
@@ -117,10 +125,10 @@ class Renderer:
|
|
|
117
125
|
for datasource in arg.datasources.values()
|
|
118
126
|
if datasource.namespace == DEFAULT_NAMESPACE
|
|
119
127
|
]
|
|
120
|
-
rendered_imports = [
|
|
121
|
-
|
|
122
|
-
for import_statement in
|
|
123
|
-
|
|
128
|
+
rendered_imports = []
|
|
129
|
+
for _, imports in arg.imports.items():
|
|
130
|
+
for import_statement in imports:
|
|
131
|
+
rendered_imports.append(self.to_string(import_statement))
|
|
124
132
|
components = []
|
|
125
133
|
if rendered_imports:
|
|
126
134
|
components.append(rendered_imports)
|
|
@@ -128,19 +136,26 @@ class Renderer:
|
|
|
128
136
|
components.append(rendered_concepts)
|
|
129
137
|
if rendered_datasources:
|
|
130
138
|
components.append(rendered_datasources)
|
|
139
|
+
|
|
131
140
|
final = "\n\n".join("\n".join(x) for x in components)
|
|
132
141
|
return final
|
|
133
142
|
|
|
134
143
|
@to_string.register
|
|
135
144
|
def _(self, arg: Datasource):
|
|
136
|
-
assignments = ",\n
|
|
145
|
+
assignments = ",\n ".join([self.to_string(x) for x in arg.columns])
|
|
146
|
+
if arg.non_partial_for:
|
|
147
|
+
non_partial = f"\ncomplete where {self.to_string(arg.non_partial_for)}"
|
|
148
|
+
else:
|
|
149
|
+
non_partial = ""
|
|
137
150
|
base = f"""datasource {arg.name} (
|
|
138
151
|
{assignments}
|
|
139
|
-
)
|
|
140
|
-
{self.to_string(arg.grain)}
|
|
152
|
+
)
|
|
153
|
+
{self.to_string(arg.grain)}{non_partial}
|
|
141
154
|
{self.to_string(arg.address)}"""
|
|
155
|
+
|
|
142
156
|
if arg.where:
|
|
143
|
-
base += f"\
|
|
157
|
+
base += f"\nwhere {self.to_string(arg.where)}"
|
|
158
|
+
|
|
144
159
|
base += ";"
|
|
145
160
|
return base
|
|
146
161
|
|
|
@@ -209,7 +224,19 @@ class Renderer:
|
|
|
209
224
|
|
|
210
225
|
@to_string.register
|
|
211
226
|
def _(self, arg: "ColumnAssignment"):
|
|
212
|
-
|
|
227
|
+
if arg.modifiers:
|
|
228
|
+
modifiers = "".join(
|
|
229
|
+
[self.to_string(modifier) for modifier in arg.modifiers]
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
modifiers = ""
|
|
233
|
+
if isinstance(arg.alias, str):
|
|
234
|
+
return f"{arg.alias}: {modifiers}{self.to_string(arg.concept)}"
|
|
235
|
+
return f"{self.to_string(arg.alias)}: {modifiers}{self.to_string(arg.concept)}"
|
|
236
|
+
|
|
237
|
+
@to_string.register
|
|
238
|
+
def _(self, arg: "RawColumnExpr"):
|
|
239
|
+
return f"raw('''{arg.text}''')"
|
|
213
240
|
|
|
214
241
|
@to_string.register
|
|
215
242
|
def _(self, arg: "ConceptDeclarationStatement"):
|
|
@@ -266,6 +293,7 @@ class Renderer:
|
|
|
266
293
|
return QUERY_TEMPLATE.render(
|
|
267
294
|
select_columns=[self.to_string(c) for c in arg.selection],
|
|
268
295
|
where=self.to_string(arg.where_clause) if arg.where_clause else None,
|
|
296
|
+
having=self.to_string(arg.having_clause) if arg.having_clause else None,
|
|
269
297
|
order_by=(
|
|
270
298
|
[self.to_string(c) for c in arg.order_by.items]
|
|
271
299
|
if arg.order_by
|
|
@@ -301,16 +329,23 @@ class Renderer:
|
|
|
301
329
|
|
|
302
330
|
@to_string.register
|
|
303
331
|
def _(self, arg: OrderBy):
|
|
304
|
-
return ",\
|
|
332
|
+
return ",\n".join([self.to_string(c) for c in arg.items])
|
|
305
333
|
|
|
306
334
|
@to_string.register
|
|
307
335
|
def _(self, arg: "WhereClause"):
|
|
308
|
-
|
|
336
|
+
base = f"{self.to_string(arg.conditional)}"
|
|
337
|
+
if base[0] == "(" and base[-1] == ")":
|
|
338
|
+
return base[1:-1]
|
|
339
|
+
return base
|
|
309
340
|
|
|
310
341
|
@to_string.register
|
|
311
342
|
def _(self, arg: "Conditional"):
|
|
312
343
|
return f"({self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)})"
|
|
313
344
|
|
|
345
|
+
@to_string.register
|
|
346
|
+
def _(self, arg: "SubselectComparison"):
|
|
347
|
+
return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
|
|
348
|
+
|
|
314
349
|
@to_string.register
|
|
315
350
|
def _(self, arg: "Comparison"):
|
|
316
351
|
return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
|
|
@@ -319,10 +354,12 @@ class Renderer:
|
|
|
319
354
|
def _(self, arg: "WindowItem"):
|
|
320
355
|
over = ",".join(self.to_string(c) for c in arg.over)
|
|
321
356
|
order = ",".join(self.to_string(c) for c in arg.order_by)
|
|
322
|
-
if over:
|
|
357
|
+
if over and order:
|
|
323
358
|
return (
|
|
324
|
-
f"{arg.type.value} {self.to_string(arg.content)} by {order}
|
|
359
|
+
f"{arg.type.value} {self.to_string(arg.content)} by {order} over {over}"
|
|
325
360
|
)
|
|
361
|
+
elif over:
|
|
362
|
+
return f"{arg.type.value} {self.to_string(arg.content)} over {over}"
|
|
326
363
|
return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
|
|
327
364
|
|
|
328
365
|
@to_string.register
|
|
@@ -331,6 +368,8 @@ class Renderer:
|
|
|
331
368
|
|
|
332
369
|
@to_string.register
|
|
333
370
|
def _(self, arg: "ImportStatement"):
|
|
371
|
+
if arg.alias == DEFAULT_NAMESPACE:
|
|
372
|
+
return f"import {arg.path};"
|
|
334
373
|
return f"import {arg.path} as {arg.alias};"
|
|
335
374
|
|
|
336
375
|
@to_string.register
|
|
@@ -343,13 +382,15 @@ class Renderer:
|
|
|
343
382
|
|
|
344
383
|
@to_string.register
|
|
345
384
|
def _(self, arg: "ConceptTransform"):
|
|
346
|
-
return f"{self.to_string(arg.function)}->{arg.output.name}"
|
|
385
|
+
return f"{self.to_string(arg.function)} -> {arg.output.name}"
|
|
347
386
|
|
|
348
387
|
@to_string.register
|
|
349
388
|
def _(self, arg: "Function"):
|
|
350
389
|
inputs = ",".join(self.to_string(c) for c in arg.arguments)
|
|
351
390
|
if arg.operator == FunctionType.CONSTANT:
|
|
352
391
|
return f"{inputs}"
|
|
392
|
+
if arg.operator == FunctionType.CAST:
|
|
393
|
+
return f"CAST({self.to_string(arg.arguments[0])} AS {self.to_string(arg.arguments[1])})"
|
|
353
394
|
if arg.operator == FunctionType.INDEX_ACCESS:
|
|
354
395
|
return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
|
|
355
396
|
return f"{arg.operator.value}({inputs})"
|
|
@@ -361,7 +402,8 @@ class Renderer:
|
|
|
361
402
|
@to_string.register
|
|
362
403
|
def _(self, arg: AggregateWrapper):
|
|
363
404
|
if arg.by:
|
|
364
|
-
|
|
405
|
+
by = ", ".join([self.to_string(x) for x in arg.by])
|
|
406
|
+
return f"{self.to_string(arg.function)} by {by}"
|
|
365
407
|
return f"{self.to_string(arg.function)}"
|
|
366
408
|
|
|
367
409
|
@to_string.register
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -35,8 +35,10 @@
|
|
|
35
35
|
prop_ident: "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "." IDENTIFIER
|
|
36
36
|
|
|
37
37
|
// datasource concepts
|
|
38
|
-
datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? (address | query) where?
|
|
39
|
-
|
|
38
|
+
datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? whole_grain_clause? (address | query) where?
|
|
39
|
+
|
|
40
|
+
whole_grain_clause: "complete" where
|
|
41
|
+
|
|
40
42
|
grain_clause: "grain" "(" column_list ")"
|
|
41
43
|
|
|
42
44
|
address: "address" (QUOTED_ADDRESS | ADDRESS)
|
|
@@ -289,8 +291,8 @@
|
|
|
289
291
|
|
|
290
292
|
// base language constructs
|
|
291
293
|
concept_lit: IDENTIFIER
|
|
292
|
-
IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_
|
|
293
|
-
QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_
|
|
294
|
+
IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.]*/
|
|
295
|
+
QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:]*`/
|
|
294
296
|
ADDRESS: IDENTIFIER
|
|
295
297
|
|
|
296
298
|
MULTILINE_STRING: /\'{3}(.*?)\'{3}/s
|
|
File without changes
|
|
File without changes
|