pytrilogy 0.0.2.5__tar.gz → 0.0.2.7__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.
Potentially problematic release.
This version of pytrilogy might be problematic. Click here for more details.
- {pytrilogy-0.0.2.5/pytrilogy.egg-info → pytrilogy-0.0.2.7}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7/pytrilogy.egg-info}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_environment.py +10 -3
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_models.py +55 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_parsing.py +45 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/constants.py +1 -2
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/enums.py +1 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/models.py +76 -19
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/inline_datasource.py +11 -7
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/concept_strategies_v3.py +12 -2
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/common.py +1 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/filter_node.py +19 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/group_node.py +1 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/group_to_node.py +0 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/node_merge_node.py +4 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/rowset_node.py +3 -2
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/base_node.py +1 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/filter_node.py +1 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/merge_node.py +28 -23
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/query_processor.py +24 -31
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/base.py +6 -3
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/common.py +2 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/duckdb.py +5 -3
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/presto.py +2 -1
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/common.py +6 -2
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/parse_engine.py +6 -3
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/trilogy.lark +3 -2
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/README.md +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pyproject.toml +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/SOURCES.txt +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/setup.cfg +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/setup.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_select.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/environment_helpers.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/functions.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/graph_models.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/inline_constant.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/utility.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/executor.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/render.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/utility.py +0 -0
|
@@ -59,6 +59,13 @@ key order_id int;
|
|
|
59
59
|
|
|
60
60
|
assert env1.concepts["order_id"] == env1.concepts["replacements.order_id"]
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
found = False
|
|
63
|
+
for x in env1.datasources["replacements.replacements"].columns:
|
|
64
|
+
if (
|
|
65
|
+
x.alias == "order_id"
|
|
66
|
+
and x.concept.address == env1.concepts["order_id"].address
|
|
67
|
+
):
|
|
68
|
+
assert x.concept == env1.concepts["order_id"]
|
|
69
|
+
assert x.modifiers == [Modifier.PARTIAL]
|
|
70
|
+
found = True
|
|
71
|
+
assert found
|
|
@@ -10,6 +10,8 @@ from trilogy.core.models import (
|
|
|
10
10
|
UndefinedConcept,
|
|
11
11
|
BaseJoin,
|
|
12
12
|
Comparison,
|
|
13
|
+
Join,
|
|
14
|
+
JoinKey,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -54,6 +56,8 @@ def test_cte_merge(test_environment, test_environment_graph):
|
|
|
54
56
|
merged = a + b
|
|
55
57
|
assert merged.output_columns == outputs
|
|
56
58
|
|
|
59
|
+
assert "Target: Grain<Abstract>." in merged.comment
|
|
60
|
+
|
|
57
61
|
|
|
58
62
|
def test_concept(test_environment, test_environment_graph):
|
|
59
63
|
test_concept = list(test_environment.concepts.values())[0]
|
|
@@ -191,3 +195,54 @@ def test_comparison():
|
|
|
191
195
|
Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
|
|
192
196
|
except Exception as exc:
|
|
193
197
|
assert isinstance(exc, SyntaxError)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_join(test_environment: Environment):
|
|
201
|
+
datasource = list(test_environment.datasources.values())[0]
|
|
202
|
+
outputs = [c.concept for c in datasource.columns]
|
|
203
|
+
output_map = {
|
|
204
|
+
c.address: {
|
|
205
|
+
datasource,
|
|
206
|
+
}
|
|
207
|
+
for c in outputs
|
|
208
|
+
}
|
|
209
|
+
a = CTE(
|
|
210
|
+
name="test",
|
|
211
|
+
output_columns=[outputs[0]],
|
|
212
|
+
grain=Grain(),
|
|
213
|
+
source=QueryDatasource(
|
|
214
|
+
input_concepts=[outputs[0]],
|
|
215
|
+
output_concepts=[outputs[0]],
|
|
216
|
+
datasources=[datasource],
|
|
217
|
+
grain=Grain(),
|
|
218
|
+
joins=[],
|
|
219
|
+
source_map={outputs[0].address: {datasource}},
|
|
220
|
+
),
|
|
221
|
+
source_map={c.address: [datasource.identifier] for c in outputs},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
b = CTE(
|
|
225
|
+
name="testb",
|
|
226
|
+
output_columns=outputs,
|
|
227
|
+
grain=Grain(),
|
|
228
|
+
source=QueryDatasource(
|
|
229
|
+
input_concepts=outputs,
|
|
230
|
+
output_concepts=outputs,
|
|
231
|
+
datasources=[datasource],
|
|
232
|
+
grain=Grain(),
|
|
233
|
+
joins=[],
|
|
234
|
+
source_map=output_map,
|
|
235
|
+
),
|
|
236
|
+
source_map={c.address: [datasource.identifier] for c in outputs},
|
|
237
|
+
)
|
|
238
|
+
test = Join(
|
|
239
|
+
left_cte=a,
|
|
240
|
+
right_cte=b,
|
|
241
|
+
joinkeys=[JoinKey(concept=x) for x in outputs],
|
|
242
|
+
jointype=JoinType.RIGHT_OUTER,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert (
|
|
246
|
+
str(test)
|
|
247
|
+
== "right outer JOIN test and testb on local.product_id<local.product_id>,local.category_id<local.category_id>"
|
|
248
|
+
), str(test)
|
|
@@ -160,7 +160,8 @@ address `preqldata.analytics_411641820.events_*`
|
|
|
160
160
|
;"""
|
|
161
161
|
)
|
|
162
162
|
query = parsed[-1]
|
|
163
|
-
assert query.address.
|
|
163
|
+
assert query.address.quoted is True
|
|
164
|
+
assert query.address.location == "preqldata.analytics_411641820.events_*"
|
|
164
165
|
|
|
165
166
|
|
|
166
167
|
def test_purpose_and_keys():
|
|
@@ -448,3 +449,46 @@ select
|
|
|
448
449
|
{"a": 1, "b": 2, "c": 3},
|
|
449
450
|
1,
|
|
450
451
|
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_datasource_colon():
|
|
455
|
+
|
|
456
|
+
text = """
|
|
457
|
+
key x int;
|
|
458
|
+
key y int;
|
|
459
|
+
|
|
460
|
+
datasource test (
|
|
461
|
+
x:x,
|
|
462
|
+
y:y)
|
|
463
|
+
grain(x)
|
|
464
|
+
address `abc:def`
|
|
465
|
+
;
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
select x;
|
|
469
|
+
"""
|
|
470
|
+
env, parsed = parse_text(text)
|
|
471
|
+
|
|
472
|
+
results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
|
|
473
|
+
|
|
474
|
+
assert '"abc:def" as test' in results
|
|
475
|
+
|
|
476
|
+
text = """
|
|
477
|
+
key x int;
|
|
478
|
+
key y int;
|
|
479
|
+
|
|
480
|
+
datasource test (
|
|
481
|
+
x:x,
|
|
482
|
+
y:y)
|
|
483
|
+
grain(x)
|
|
484
|
+
address abcdef
|
|
485
|
+
;
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
select x;
|
|
489
|
+
"""
|
|
490
|
+
env, parsed = parse_text(text)
|
|
491
|
+
|
|
492
|
+
results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
|
|
493
|
+
|
|
494
|
+
assert "abcdef as test" in results, results
|
|
@@ -33,6 +33,7 @@ class Config:
|
|
|
33
33
|
strict_mode: bool = True
|
|
34
34
|
human_identifiers: bool = True
|
|
35
35
|
validate_missing: bool = True
|
|
36
|
+
show_comments: bool = False
|
|
36
37
|
optimizations: Optimizations = field(default_factory=Optimizations)
|
|
37
38
|
|
|
38
39
|
def set_random_seed(self, seed: int):
|
|
@@ -42,5 +43,3 @@ class Config:
|
|
|
42
43
|
CONFIG = Config()
|
|
43
44
|
|
|
44
45
|
CONFIG.set_random_seed(42)
|
|
45
|
-
|
|
46
|
-
CONFIG.strict_mode = True
|
|
@@ -945,12 +945,14 @@ class ColumnAssignment(BaseModel):
|
|
|
945
945
|
)
|
|
946
946
|
|
|
947
947
|
def with_merge(
|
|
948
|
-
self,
|
|
948
|
+
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
949
949
|
) -> "ColumnAssignment":
|
|
950
950
|
return ColumnAssignment(
|
|
951
951
|
alias=self.alias,
|
|
952
|
-
concept=concept,
|
|
953
|
-
modifiers=
|
|
952
|
+
concept=self.concept.with_merge(source, target, modifiers),
|
|
953
|
+
modifiers=(
|
|
954
|
+
modifiers if self.concept.address == source.address else self.modifiers
|
|
955
|
+
),
|
|
954
956
|
)
|
|
955
957
|
|
|
956
958
|
|
|
@@ -1817,6 +1819,7 @@ class MultiSelectStatement(SelectTypeMixin, Mergeable, Namespaced, BaseModel):
|
|
|
1817
1819
|
class Address(BaseModel):
|
|
1818
1820
|
location: str
|
|
1819
1821
|
is_query: bool = False
|
|
1822
|
+
quoted: bool = False
|
|
1820
1823
|
|
|
1821
1824
|
|
|
1822
1825
|
class Query(BaseModel):
|
|
@@ -1889,20 +1892,22 @@ class Datasource(Namespaced, BaseModel):
|
|
|
1889
1892
|
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
1890
1893
|
):
|
|
1891
1894
|
original = [c for c in self.columns if c.concept.address == source.address]
|
|
1895
|
+
if len(original) != 1:
|
|
1896
|
+
raise ValueError(
|
|
1897
|
+
f"Expected exactly one column to merge, got {len(original)} for {source.address}, {[x.alias for x in original]}"
|
|
1898
|
+
)
|
|
1892
1899
|
# map to the alias with the modifier, and the original
|
|
1893
1900
|
self.columns = [
|
|
1894
|
-
(
|
|
1895
|
-
c.with_merge(target, modifiers)
|
|
1896
|
-
if c.concept.address == source.address
|
|
1897
|
-
else c
|
|
1898
|
-
)
|
|
1901
|
+
c.with_merge(source, target, modifiers)
|
|
1899
1902
|
for c in self.columns
|
|
1903
|
+
if c.concept.address != source.address
|
|
1900
1904
|
] + original
|
|
1901
1905
|
self.grain = self.grain.with_merge(source, target, modifiers)
|
|
1902
1906
|
self.where = (
|
|
1903
1907
|
self.where.with_merge(source, target, modifiers) if self.where else None
|
|
1904
1908
|
)
|
|
1905
|
-
|
|
1909
|
+
|
|
1910
|
+
self.add_column(target, original[0].alias, modifiers)
|
|
1906
1911
|
|
|
1907
1912
|
@property
|
|
1908
1913
|
def env_label(self) -> str:
|
|
@@ -1914,7 +1919,7 @@ class Datasource(Namespaced, BaseModel):
|
|
|
1914
1919
|
def condition(self):
|
|
1915
1920
|
return None
|
|
1916
1921
|
|
|
1917
|
-
@
|
|
1922
|
+
@property
|
|
1918
1923
|
def output_lcl(self) -> LooseConceptList:
|
|
1919
1924
|
return LooseConceptList(concepts=self.output_concepts)
|
|
1920
1925
|
|
|
@@ -1922,9 +1927,9 @@ class Datasource(Namespaced, BaseModel):
|
|
|
1922
1927
|
def can_be_inlined(self) -> bool:
|
|
1923
1928
|
if isinstance(self.address, Address) and self.address.is_query:
|
|
1924
1929
|
return False
|
|
1925
|
-
for x in self.columns:
|
|
1926
|
-
|
|
1927
|
-
|
|
1930
|
+
# for x in self.columns:
|
|
1931
|
+
# if not isinstance(x.alias, str):
|
|
1932
|
+
# return False
|
|
1928
1933
|
return True
|
|
1929
1934
|
|
|
1930
1935
|
@property
|
|
@@ -1959,12 +1964,15 @@ class Datasource(Namespaced, BaseModel):
|
|
|
1959
1964
|
)
|
|
1960
1965
|
return grain
|
|
1961
1966
|
|
|
1962
|
-
def add_column(
|
|
1967
|
+
def add_column(
|
|
1968
|
+
self,
|
|
1969
|
+
concept: Concept,
|
|
1970
|
+
alias: str | RawColumnExpr | Function,
|
|
1971
|
+
modifiers: List[Modifier] | None = None,
|
|
1972
|
+
):
|
|
1963
1973
|
self.columns.append(
|
|
1964
|
-
ColumnAssignment(alias=alias, concept=concept, modifiers=modifiers)
|
|
1974
|
+
ColumnAssignment(alias=alias, concept=concept, modifiers=modifiers or [])
|
|
1965
1975
|
)
|
|
1966
|
-
# force refresh
|
|
1967
|
-
del self.output_lcl
|
|
1968
1976
|
|
|
1969
1977
|
def __add__(self, other):
|
|
1970
1978
|
if not other == self:
|
|
@@ -1997,7 +2005,7 @@ class Datasource(Namespaced, BaseModel):
|
|
|
1997
2005
|
where=self.where.with_namespace(namespace) if self.where else None,
|
|
1998
2006
|
)
|
|
1999
2007
|
|
|
2000
|
-
@
|
|
2008
|
+
@property
|
|
2001
2009
|
def concepts(self) -> List[Concept]:
|
|
2002
2010
|
return [c.concept for c in self.columns]
|
|
2003
2011
|
|
|
@@ -2148,6 +2156,12 @@ class BaseJoin(BaseModel):
|
|
|
2148
2156
|
)
|
|
2149
2157
|
|
|
2150
2158
|
def __str__(self):
|
|
2159
|
+
if self.concept_pairs:
|
|
2160
|
+
return (
|
|
2161
|
+
f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
|
|
2162
|
+
f" {self.right_datasource.identifier} on"
|
|
2163
|
+
f" {','.join([str(k[0])+'='+str(k[1]) for k in self.concept_pairs])}"
|
|
2164
|
+
)
|
|
2151
2165
|
return (
|
|
2152
2166
|
f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
|
|
2153
2167
|
f" {self.right_datasource.identifier} on"
|
|
@@ -2459,6 +2473,19 @@ class CTE(BaseModel):
|
|
|
2459
2473
|
self.base_alias_override = candidates[0] if candidates else None
|
|
2460
2474
|
return True
|
|
2461
2475
|
|
|
2476
|
+
@property
|
|
2477
|
+
def comment(self) -> str:
|
|
2478
|
+
base = f"Target: {str(self.grain)}."
|
|
2479
|
+
if self.parent_ctes:
|
|
2480
|
+
base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
|
|
2481
|
+
if self.joins:
|
|
2482
|
+
base += f"\n-- Joins: {', '.join([str(x) for x in self.joins])}."
|
|
2483
|
+
if self.partial_concepts:
|
|
2484
|
+
base += (
|
|
2485
|
+
f"\n-- Partials: {', '.join([str(x) for x in self.partial_concepts])}."
|
|
2486
|
+
)
|
|
2487
|
+
return base
|
|
2488
|
+
|
|
2462
2489
|
def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
|
|
2463
2490
|
qds_being_inlined = parent.source
|
|
2464
2491
|
ds_being_inlined = qds_being_inlined.datasources[0]
|
|
@@ -2549,6 +2576,10 @@ class CTE(BaseModel):
|
|
|
2549
2576
|
self.hidden_concepts = unique(
|
|
2550
2577
|
self.hidden_concepts + other.hidden_concepts, "address"
|
|
2551
2578
|
)
|
|
2579
|
+
self.existence_source_map = {
|
|
2580
|
+
**self.existence_source_map,
|
|
2581
|
+
**other.existence_source_map,
|
|
2582
|
+
}
|
|
2552
2583
|
return self
|
|
2553
2584
|
|
|
2554
2585
|
@property
|
|
@@ -2579,6 +2610,16 @@ class CTE(BaseModel):
|
|
|
2579
2610
|
return self.relevant_base_ctes[0].name
|
|
2580
2611
|
return self.source.name
|
|
2581
2612
|
|
|
2613
|
+
@property
|
|
2614
|
+
def quote_address(self) -> bool:
|
|
2615
|
+
if self.is_root_datasource:
|
|
2616
|
+
candidate = self.source.datasources[0]
|
|
2617
|
+
if isinstance(candidate, Datasource) and isinstance(
|
|
2618
|
+
candidate.address, Address
|
|
2619
|
+
):
|
|
2620
|
+
return candidate.address.quoted
|
|
2621
|
+
return False
|
|
2622
|
+
|
|
2582
2623
|
@property
|
|
2583
2624
|
def base_alias(self) -> str:
|
|
2584
2625
|
if self.base_alias_override:
|
|
@@ -2730,6 +2771,12 @@ class Join(BaseModel):
|
|
|
2730
2771
|
return self.left_name + self.right_name + self.jointype.value
|
|
2731
2772
|
|
|
2732
2773
|
def __str__(self):
|
|
2774
|
+
if self.joinkey_pairs:
|
|
2775
|
+
return (
|
|
2776
|
+
f"{self.jointype.value} JOIN {self.left_name} and"
|
|
2777
|
+
f" {self.right_name} on"
|
|
2778
|
+
f" {','.join([str(k[0])+'='+str(k[1]) for k in self.joinkey_pairs])}"
|
|
2779
|
+
)
|
|
2733
2780
|
return (
|
|
2734
2781
|
f"{self.jointype.value} JOIN {self.left_name} and"
|
|
2735
2782
|
f" {self.right_name} on {','.join([str(k) for k in self.joinkeys])}"
|
|
@@ -2991,6 +3038,7 @@ class Environment(BaseModel):
|
|
|
2991
3038
|
|
|
2992
3039
|
materialized_concepts: List[Concept] = Field(default_factory=list)
|
|
2993
3040
|
alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
|
|
3041
|
+
canonical_map: Dict[str, str] = Field(default_factory=dict)
|
|
2994
3042
|
_parse_count: int = 0
|
|
2995
3043
|
|
|
2996
3044
|
@classmethod
|
|
@@ -3039,7 +3087,7 @@ class Environment(BaseModel):
|
|
|
3039
3087
|
if x.address not in current_mat
|
|
3040
3088
|
]
|
|
3041
3089
|
if new:
|
|
3042
|
-
logger.
|
|
3090
|
+
logger.debug(f"Environment added new materialized concepts {new}")
|
|
3043
3091
|
|
|
3044
3092
|
def validate_concept(self, lookup: str, meta: Meta | None = None):
|
|
3045
3093
|
existing: Concept = self.concepts.get(lookup) # type: ignore
|
|
@@ -3202,13 +3250,22 @@ class Environment(BaseModel):
|
|
|
3202
3250
|
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
3203
3251
|
):
|
|
3204
3252
|
replacements = {}
|
|
3253
|
+
# exit early if we've run this
|
|
3254
|
+
if source.address in self.alias_origin_lookup:
|
|
3255
|
+
if self.concepts[source.address] == target:
|
|
3256
|
+
return
|
|
3205
3257
|
self.alias_origin_lookup[source.address] = source
|
|
3206
3258
|
for k, v in self.concepts.items():
|
|
3259
|
+
|
|
3207
3260
|
if v.address == target.address:
|
|
3208
3261
|
v.pseudonyms[source.address] = source
|
|
3209
3262
|
if v.address == source.address:
|
|
3210
3263
|
replacements[k] = target
|
|
3264
|
+
self.canonical_map[k] = target.address
|
|
3211
3265
|
v.pseudonyms[target.address] = target
|
|
3266
|
+
# we need to update keys and grains of all concepts
|
|
3267
|
+
else:
|
|
3268
|
+
replacements[k] = v.with_merge(source, target, modifiers)
|
|
3212
3269
|
self.concepts.update(replacements)
|
|
3213
3270
|
|
|
3214
3271
|
for k, ds in self.datasources.items():
|
|
@@ -42,19 +42,18 @@ class InlineDatasource(OptimizationRule):
|
|
|
42
42
|
self.log(f"parent {parent_cte.name} datasource is not inlineable")
|
|
43
43
|
continue
|
|
44
44
|
root_outputs = {x.address for x in root.output_concepts}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
grain_components = {x.address for x in root.grain.components}
|
|
45
|
+
inherited = {
|
|
46
|
+
x for x, v in cte.source_map.items() if v and parent_cte.name in v
|
|
47
|
+
}
|
|
49
48
|
if not inherited.issubset(root_outputs):
|
|
50
49
|
cte_missing = inherited - root_outputs
|
|
51
50
|
self.log(
|
|
52
51
|
f"Not all {parent_cte.name} require inputs are found on datasource, missing {cte_missing}"
|
|
53
52
|
)
|
|
54
53
|
continue
|
|
55
|
-
if not
|
|
56
|
-
self.log("Not all
|
|
57
|
-
|
|
54
|
+
if not root.grain.issubset(parent_cte.grain):
|
|
55
|
+
self.log(f"Not all {parent_cte.name} is at wrong grain to inline")
|
|
56
|
+
continue
|
|
58
57
|
to_inline.append(parent_cte)
|
|
59
58
|
|
|
60
59
|
optimized = False
|
|
@@ -68,6 +67,11 @@ class InlineDatasource(OptimizationRule):
|
|
|
68
67
|
f"Skipping inlining raw datasource {replaceable.source.name} ({replaceable.name}) due to multiple references"
|
|
69
68
|
)
|
|
70
69
|
continue
|
|
70
|
+
if not replaceable.source.datasources[0].grain.issubset(replaceable.grain):
|
|
71
|
+
self.log(
|
|
72
|
+
f"Forcing group ({parent_cte.grain} being replaced by inlined source {root.grain})"
|
|
73
|
+
)
|
|
74
|
+
force_group = True
|
|
71
75
|
result = cte.inline_parent_datasource(replaceable, force_group=force_group)
|
|
72
76
|
if result:
|
|
73
77
|
self.log(
|
|
@@ -612,9 +612,19 @@ def _search_concepts(
|
|
|
612
612
|
)
|
|
613
613
|
|
|
614
614
|
if expanded:
|
|
615
|
-
|
|
615
|
+
# we don't need to return the entire list; just the ones we needed pre-expansion
|
|
616
|
+
ex_resolve = expanded.resolve()
|
|
617
|
+
extra = [
|
|
618
|
+
x
|
|
619
|
+
for x in ex_resolve.output_concepts
|
|
620
|
+
if x.address not in [y.address for y in mandatory_list]
|
|
621
|
+
and x not in ex_resolve.grain.components
|
|
622
|
+
]
|
|
623
|
+
expanded.output_concepts = mandatory_list
|
|
624
|
+
expanded.rebuild_cache()
|
|
625
|
+
|
|
616
626
|
logger.info(
|
|
617
|
-
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in mandatory_list]} via concept addition;"
|
|
627
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in mandatory_list]} via concept addition; removing extra {[c.address for c in extra]}"
|
|
618
628
|
)
|
|
619
629
|
return expanded
|
|
620
630
|
# if we can't find it after expanding to a merge, then
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/filter_node.py
RENAMED
|
@@ -105,10 +105,29 @@ def gen_filter_node(
|
|
|
105
105
|
environment=environment,
|
|
106
106
|
g=g,
|
|
107
107
|
parents=core_parents,
|
|
108
|
+
grain=Grain(
|
|
109
|
+
components=[immediate_parent] + parent_row_concepts,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert filter_node.resolve().grain == Grain(
|
|
114
|
+
components=[immediate_parent] + parent_row_concepts,
|
|
108
115
|
)
|
|
109
116
|
if not local_optional or all(
|
|
110
117
|
[x.address in [y.address for y in parent_row_concepts] for x in local_optional]
|
|
111
118
|
):
|
|
119
|
+
outputs = [
|
|
120
|
+
x
|
|
121
|
+
for x in filter_node.output_concepts
|
|
122
|
+
if x.address in [y.address for y in local_optional]
|
|
123
|
+
]
|
|
124
|
+
logger.info(
|
|
125
|
+
f"{padding(depth)}{LOGGER_PREFIX} no extra enrichment needed for filter node"
|
|
126
|
+
)
|
|
127
|
+
filter_node.output_concepts = [
|
|
128
|
+
concept,
|
|
129
|
+
] + outputs
|
|
130
|
+
filter_node.rebuild_cache()
|
|
112
131
|
return filter_node
|
|
113
132
|
enrich_node = source_concepts( # this fetches the parent + join keys
|
|
114
133
|
# to then connect to the rest of the query
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/group_node.py
RENAMED
|
@@ -29,7 +29,7 @@ def gen_group_node(
|
|
|
29
29
|
resolve_function_parent_concepts(concept), "address"
|
|
30
30
|
)
|
|
31
31
|
logger.info(
|
|
32
|
-
f"{padding(depth)}{LOGGER_PREFIX}
|
|
32
|
+
f"{padding(depth)}{LOGGER_PREFIX} parent concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
# if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/node_merge_node.py
RENAMED
|
@@ -260,6 +260,7 @@ def subgraphs_to_merge_node(
|
|
|
260
260
|
source_concepts,
|
|
261
261
|
history,
|
|
262
262
|
conditions,
|
|
263
|
+
enable_early_exit: bool = True,
|
|
263
264
|
):
|
|
264
265
|
parents: List[StrategyNode] = []
|
|
265
266
|
logger.info(
|
|
@@ -290,6 +291,8 @@ def subgraphs_to_merge_node(
|
|
|
290
291
|
for x in parents:
|
|
291
292
|
for y in x.output_concepts:
|
|
292
293
|
input_c.append(y)
|
|
294
|
+
if len(parents) == 1 and enable_early_exit:
|
|
295
|
+
return parents[0]
|
|
293
296
|
|
|
294
297
|
return MergeNode(
|
|
295
298
|
input_concepts=unique(input_c, "address"),
|
|
@@ -350,6 +353,7 @@ def gen_merge_node(
|
|
|
350
353
|
source_concepts=source_concepts,
|
|
351
354
|
history=history,
|
|
352
355
|
conditions=conditions,
|
|
356
|
+
enable_early_exit=False,
|
|
353
357
|
)
|
|
354
358
|
if test:
|
|
355
359
|
return test
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/rowset_node.py
RENAMED
|
@@ -74,8 +74,9 @@ def gen_rowset_node(
|
|
|
74
74
|
if existence_parents:
|
|
75
75
|
node.parents += existence_parents
|
|
76
76
|
# we don't need to join to any existence parents
|
|
77
|
-
if isinstance(node, MergeNode):
|
|
78
|
-
|
|
77
|
+
# if isinstance(node, MergeNode) and node.node_joins is None:
|
|
78
|
+
# # set it explicitly to empty to avoid inference
|
|
79
|
+
# node.node_joins = []
|
|
79
80
|
for parent in existence_parents:
|
|
80
81
|
for x in parent.output_concepts:
|
|
81
82
|
if x.address not in node.output_lcl:
|
|
@@ -192,7 +192,7 @@ class StrategyNode:
|
|
|
192
192
|
p.resolve() for p in self.parents
|
|
193
193
|
]
|
|
194
194
|
|
|
195
|
-
grain = Grain(components=self.output_concepts)
|
|
195
|
+
grain = self.grain if self.grain else Grain(components=self.output_concepts)
|
|
196
196
|
source_map = resolve_concept_map(
|
|
197
197
|
parent_sources,
|
|
198
198
|
self.output_concepts,
|
|
@@ -78,7 +78,7 @@ def deduplicate_nodes_and_joins(
|
|
|
78
78
|
duplicates = False
|
|
79
79
|
duplicates, merged, removed = deduplicate_nodes(merged, logging_prefix)
|
|
80
80
|
# filter out any removed joins
|
|
81
|
-
if joins:
|
|
81
|
+
if joins is not None:
|
|
82
82
|
joins = [
|
|
83
83
|
j
|
|
84
84
|
for j in joins
|
|
@@ -138,6 +138,16 @@ class MergeNode(StrategyNode):
|
|
|
138
138
|
continue
|
|
139
139
|
final_joins.append(join)
|
|
140
140
|
self.node_joins = final_joins
|
|
141
|
+
partial_lookup: list[Concept] = []
|
|
142
|
+
non_partial: List[Concept] = []
|
|
143
|
+
for node in parents or []:
|
|
144
|
+
partial_lookup += node.partial_concepts
|
|
145
|
+
non_partial += [
|
|
146
|
+
x for x in node.output_concepts if x not in node.partial_concepts
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
final_partial = [x for x in partial_lookup if x not in non_partial]
|
|
150
|
+
self.partial_concepts = final_partial
|
|
141
151
|
|
|
142
152
|
def translate_node_joins(self, node_joins: List[NodeJoin]) -> List[BaseJoin]:
|
|
143
153
|
joins = []
|
|
@@ -219,12 +229,13 @@ class MergeNode(StrategyNode):
|
|
|
219
229
|
)
|
|
220
230
|
joins = self.translate_node_joins(final_joins)
|
|
221
231
|
else:
|
|
232
|
+
logger.info(
|
|
233
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Final joins is not null {final_joins} but is empty, skipping join generation"
|
|
234
|
+
)
|
|
222
235
|
return []
|
|
223
236
|
|
|
224
237
|
for join in joins:
|
|
225
|
-
logger.info(
|
|
226
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} final join {join.join_type} {[str(c) for c in join.concepts]}"
|
|
227
|
-
)
|
|
238
|
+
logger.info(f"{self.logging_prefix}{LOGGER_PREFIX} final join {str(join)}")
|
|
228
239
|
return joins
|
|
229
240
|
|
|
230
241
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -249,6 +260,12 @@ class MergeNode(StrategyNode):
|
|
|
249
260
|
# early exit if we can just return the parent
|
|
250
261
|
final_datasets: List[QueryDatasource | Datasource] = list(merged.values())
|
|
251
262
|
|
|
263
|
+
existence_final = [
|
|
264
|
+
x
|
|
265
|
+
for x in final_datasets
|
|
266
|
+
if all([y in self.existence_concepts for y in x.output_concepts])
|
|
267
|
+
]
|
|
268
|
+
|
|
252
269
|
if len(merged.keys()) == 1:
|
|
253
270
|
final: QueryDatasource | Datasource = list(merged.values())[0]
|
|
254
271
|
if (
|
|
@@ -288,34 +305,25 @@ class MergeNode(StrategyNode):
|
|
|
288
305
|
for source in final_datasets:
|
|
289
306
|
pregrain += source.grain
|
|
290
307
|
|
|
291
|
-
grain =
|
|
292
|
-
self.grain
|
|
293
|
-
if self.grain
|
|
294
|
-
else Grain(
|
|
295
|
-
components=[
|
|
296
|
-
c
|
|
297
|
-
for c in pregrain.components
|
|
298
|
-
if c.address in [x.address for x in self.output_concepts]
|
|
299
|
-
]
|
|
300
|
-
)
|
|
301
|
-
)
|
|
308
|
+
grain = self.grain if self.grain else pregrain
|
|
302
309
|
|
|
303
310
|
logger.info(
|
|
304
311
|
f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
|
|
305
312
|
)
|
|
306
|
-
|
|
307
|
-
if len(
|
|
313
|
+
join_candidates = [x for x in final_datasets if x not in existence_final]
|
|
314
|
+
if len(join_candidates) > 1:
|
|
308
315
|
joins = self.generate_joins(
|
|
309
|
-
|
|
316
|
+
join_candidates, final_joins, pregrain, grain, self.environment
|
|
310
317
|
)
|
|
311
318
|
else:
|
|
312
319
|
joins = []
|
|
313
|
-
|
|
320
|
+
logger.info(
|
|
321
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Final join count for CTE parent count {len(join_candidates)} is {len(joins)}"
|
|
322
|
+
)
|
|
314
323
|
full_join_concepts = []
|
|
315
324
|
for join in joins:
|
|
316
325
|
if join.join_type == JoinType.FULL:
|
|
317
326
|
full_join_concepts += join.concepts
|
|
318
|
-
|
|
319
327
|
if self.whole_grain:
|
|
320
328
|
force_group = False
|
|
321
329
|
elif self.force_group is False:
|
|
@@ -337,9 +345,6 @@ class MergeNode(StrategyNode):
|
|
|
337
345
|
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
338
346
|
full_joins=full_join_concepts,
|
|
339
347
|
)
|
|
340
|
-
logger.info(
|
|
341
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} source_map {str(source_map)}"
|
|
342
|
-
)
|
|
343
348
|
qds = QueryDatasource(
|
|
344
349
|
input_concepts=unique(self.input_concepts, "address"),
|
|
345
350
|
output_concepts=unique(self.output_concepts, "address"),
|
|
@@ -183,49 +183,42 @@ def generate_cte_name(full_name: str, name_map: dict[str, str]) -> str:
|
|
|
183
183
|
return full_name.replace("<", "").replace(">", "").replace(",", "_")
|
|
184
184
|
|
|
185
185
|
|
|
186
|
-
def
|
|
186
|
+
def resolve_cte_base_name_and_alias_v2(
|
|
187
187
|
name: str,
|
|
188
188
|
source: QueryDatasource,
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
source_map: Dict[str, list[str]],
|
|
190
|
+
raw_joins: List[Join | InstantiatedUnnestJoin],
|
|
191
191
|
) -> Tuple[str | None, str | None]:
|
|
192
|
-
|
|
193
|
-
valid_joins: List[Join] = [join for join in joins if isinstance(join, Join)]
|
|
194
|
-
relevant_parent_sources = set()
|
|
195
|
-
for k, v in source.source_map.items():
|
|
196
|
-
if v:
|
|
197
|
-
relevant_parent_sources.update(v)
|
|
198
|
-
eligible = [x for x in source.datasources if x in relevant_parent_sources]
|
|
192
|
+
joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
|
|
199
193
|
if (
|
|
200
|
-
len(
|
|
201
|
-
and isinstance(
|
|
202
|
-
and not
|
|
194
|
+
len(source.datasources) == 1
|
|
195
|
+
and isinstance(source.datasources[0], Datasource)
|
|
196
|
+
and not source.datasources[0].name == CONSTANT_DATASET
|
|
203
197
|
):
|
|
204
|
-
ds =
|
|
198
|
+
ds = source.datasources[0]
|
|
205
199
|
return ds.safe_location, ds.identifier
|
|
206
200
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return parents[0].name, parents[0].name
|
|
211
|
-
elif valid_joins and len(valid_joins) > 0:
|
|
212
|
-
candidates = [x.left_cte.name for x in valid_joins]
|
|
213
|
-
disallowed = [x.right_cte.name for x in valid_joins]
|
|
201
|
+
if joins and len(joins) > 0:
|
|
202
|
+
candidates = [x.left_cte.name for x in joins]
|
|
203
|
+
disallowed = [x.right_cte.name for x in joins]
|
|
214
204
|
try:
|
|
215
205
|
cte = [y for y in candidates if y not in disallowed][0]
|
|
216
206
|
return cte, cte
|
|
217
207
|
except IndexError:
|
|
218
208
|
raise SyntaxError(
|
|
219
|
-
f"Invalid join configuration {candidates} {disallowed}
|
|
209
|
+
f"Invalid join configuration {candidates} {disallowed} for {name}",
|
|
220
210
|
)
|
|
221
|
-
elif eligible:
|
|
222
|
-
matched = [x for x in parents if x.source.name == eligible[0].name]
|
|
223
|
-
if matched:
|
|
224
|
-
return matched[0].name, matched[0].name
|
|
225
211
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
212
|
+
counts: dict[str, int] = defaultdict(lambda: 0)
|
|
213
|
+
output_addresses = [x.address for x in source.output_concepts]
|
|
214
|
+
for k, v in source_map.items():
|
|
215
|
+
for vx in v:
|
|
216
|
+
if k in output_addresses:
|
|
217
|
+
counts[vx] = counts[vx] + 1
|
|
218
|
+
else:
|
|
219
|
+
counts[vx] = counts[vx]
|
|
220
|
+
if counts:
|
|
221
|
+
return max(counts, key=counts.get), max(counts, key=counts.get) # type: ignore
|
|
229
222
|
return None, None
|
|
230
223
|
|
|
231
224
|
|
|
@@ -274,8 +267,8 @@ def datasource_to_ctes(
|
|
|
274
267
|
for x in [base_join_to_join(join, parents) for join in query_datasource.joins]
|
|
275
268
|
if x
|
|
276
269
|
]
|
|
277
|
-
base_name, base_alias =
|
|
278
|
-
human_id, query_datasource,
|
|
270
|
+
base_name, base_alias = resolve_cte_base_name_and_alias_v2(
|
|
271
|
+
human_id, query_datasource, source_map, final_joins
|
|
279
272
|
)
|
|
280
273
|
cte = CTE(
|
|
281
274
|
name=human_id,
|
|
@@ -499,10 +499,12 @@ class BaseDialect:
|
|
|
499
499
|
for c in cte.output_columns
|
|
500
500
|
if c.address not in [y.address for y in cte.hidden_concepts]
|
|
501
501
|
]
|
|
502
|
-
if cte.
|
|
503
|
-
source = cte.base_name
|
|
502
|
+
if cte.quote_address:
|
|
503
|
+
source = f"{self.QUOTE_CHARACTER}{cte.base_name}{self.QUOTE_CHARACTER}"
|
|
504
504
|
else:
|
|
505
|
-
source =
|
|
505
|
+
source = cte.base_name
|
|
506
|
+
if cte.base_name != cte.base_alias:
|
|
507
|
+
source = f"{source} as {cte.base_alias}"
|
|
506
508
|
return CompiledCTE(
|
|
507
509
|
name=cte.name,
|
|
508
510
|
statement=self.SQL_TEMPLATE.render(
|
|
@@ -511,6 +513,7 @@ class BaseDialect:
|
|
|
511
513
|
grain=cte.grain,
|
|
512
514
|
limit=cte.limit,
|
|
513
515
|
# some joins may not need to be rendered
|
|
516
|
+
comment=cte.comment if CONFIG.show_comments else None,
|
|
514
517
|
joins=[
|
|
515
518
|
j
|
|
516
519
|
for j in [
|
|
@@ -26,7 +26,8 @@ def render_join(
|
|
|
26
26
|
raise ValueError("must provide a cte to build an unnest joins")
|
|
27
27
|
if unnest_mode == UnnestMode.CROSS_JOIN:
|
|
28
28
|
return f"CROSS JOIN {render_func(join.concept, cte, False)} as {quote_character}{join.concept.safe_address}{quote_character}"
|
|
29
|
-
|
|
29
|
+
if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
|
|
30
|
+
return f"CROSS JOIN {render_func(join.concept, cte, False)} as array_unnest ({quote_character}{join.concept.safe_address}{quote_character})"
|
|
30
31
|
return f"FULL JOIN {render_func(join.concept, cte, False)} as unnest_wrapper({quote_character}{join.concept.safe_address}{quote_character})"
|
|
31
32
|
left_name = join.left_name
|
|
32
33
|
right_name = join.right_name
|
|
@@ -47,8 +47,9 @@ CREATE OR REPLACE TABLE {{ output.address.location }} AS
|
|
|
47
47
|
{% endif %}{%- if ctes %}
|
|
48
48
|
WITH {% for cte in ctes %}
|
|
49
49
|
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
|
|
50
|
-
{
|
|
51
|
-
{
|
|
50
|
+
{%- if full_select -%}{{full_select}}
|
|
51
|
+
{%- else -%}{%- if comment %}
|
|
52
|
+
-- {{ comment }}{% endif %}
|
|
52
53
|
SELECT
|
|
53
54
|
{%- for select in select_columns %}
|
|
54
55
|
{{ select }}{% if not loop.last %},{% endif %}{% endfor %}
|
|
@@ -56,7 +57,8 @@ SELECT
|
|
|
56
57
|
{{ base }}{% endif %}{% if joins %}
|
|
57
58
|
{%- for join in joins %}
|
|
58
59
|
{{ join }}{% endfor %}{% endif %}
|
|
59
|
-
{
|
|
60
|
+
{%- if where %}
|
|
61
|
+
WHERE
|
|
60
62
|
{{ where }}
|
|
61
63
|
{% endif -%}{%- if group_by %}
|
|
62
64
|
GROUP BY {% for group in group_by %}
|
|
@@ -5,7 +5,7 @@ from jinja2 import Template
|
|
|
5
5
|
from trilogy.core.enums import FunctionType, WindowType
|
|
6
6
|
from trilogy.dialect.base import BaseDialect
|
|
7
7
|
from trilogy.core.models import DataType
|
|
8
|
-
|
|
8
|
+
from trilogy.core.enums import UnnestMode
|
|
9
9
|
|
|
10
10
|
WINDOW_FUNCTION_MAP: Mapping[WindowType, Callable[[Any, Any, Any], str]] = {}
|
|
11
11
|
|
|
@@ -86,6 +86,7 @@ class PrestoDialect(BaseDialect):
|
|
|
86
86
|
QUOTE_CHARACTER = '"'
|
|
87
87
|
SQL_TEMPLATE = SQL_TEMPLATE
|
|
88
88
|
DATATYPE_MAP = {**BaseDialect.DATATYPE_MAP, DataType.NUMERIC: "DECIMAL"}
|
|
89
|
+
UNNEST_MODE = UnnestMode.CROSS_JOIN
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
class TrinoDialect(PrestoDialect):
|
|
@@ -112,12 +112,16 @@ def filter_item_to_concept(
|
|
|
112
112
|
return Concept(
|
|
113
113
|
name=name,
|
|
114
114
|
datatype=parent.content.datatype,
|
|
115
|
-
purpose=
|
|
115
|
+
purpose=Purpose.PROPERTY,
|
|
116
116
|
lineage=parent,
|
|
117
117
|
metadata=fmetadata,
|
|
118
118
|
namespace=namespace,
|
|
119
119
|
# filtered copies cannot inherit keys
|
|
120
|
-
keys=
|
|
120
|
+
keys=(
|
|
121
|
+
parent.content.keys
|
|
122
|
+
if parent.content.purpose == Purpose.PROPERTY
|
|
123
|
+
else (parent.content,)
|
|
124
|
+
),
|
|
121
125
|
grain=(
|
|
122
126
|
parent.content.grain
|
|
123
127
|
if parent.content.purpose == Purpose.PROPERTY
|
|
@@ -297,8 +297,11 @@ class ParseToObjects(Transformer):
|
|
|
297
297
|
def concept_lit(self, args) -> Concept:
|
|
298
298
|
return self.environment.concepts.__getitem__(args[0])
|
|
299
299
|
|
|
300
|
-
def ADDRESS(self, args) ->
|
|
301
|
-
return args.value
|
|
300
|
+
def ADDRESS(self, args) -> Address:
|
|
301
|
+
return Address(location=args.value, quoted=False)
|
|
302
|
+
|
|
303
|
+
def QUOTED_ADDRESS(self, args) -> Address:
|
|
304
|
+
return Address(location=args.value[1:-1], quoted=True)
|
|
302
305
|
|
|
303
306
|
def STRING_CHARS(self, args) -> str:
|
|
304
307
|
return args.value
|
|
@@ -1010,7 +1013,7 @@ class ParseToObjects(Transformer):
|
|
|
1010
1013
|
|
|
1011
1014
|
@v_args(meta=True)
|
|
1012
1015
|
def address(self, meta: Meta, args):
|
|
1013
|
-
return
|
|
1016
|
+
return args[0]
|
|
1014
1017
|
|
|
1015
1018
|
@v_args(meta=True)
|
|
1016
1019
|
def query(self, meta: Meta, args):
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
grain_clause: "grain" "(" column_list ")"
|
|
41
41
|
|
|
42
|
-
address: "address" ADDRESS
|
|
42
|
+
address: "address" (QUOTED_ADDRESS | ADDRESS)
|
|
43
43
|
|
|
44
44
|
query: "query" MULTILINE_STRING
|
|
45
45
|
|
|
@@ -258,7 +258,8 @@
|
|
|
258
258
|
// base language constructs
|
|
259
259
|
concept_lit: IDENTIFIER
|
|
260
260
|
IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.\-]*/
|
|
261
|
-
|
|
261
|
+
QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_\-\.\-\*\:]*`/
|
|
262
|
+
ADDRESS: IDENTIFIER
|
|
262
263
|
|
|
263
264
|
MULTILINE_STRING: /\'{3}(.*?)\'{3}/s
|
|
264
265
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/basic_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/multiselect_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/select_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/unnest_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/window_node.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|