pytrilogy 0.0.2.26__tar.gz → 0.0.2.27__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.26/pytrilogy.egg-info → pytrilogy-0.0.2.27}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27/pytrilogy.egg-info}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_parsing.py +22 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_partial_handling.py +1 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_query_processing.py +1 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/graph_models.py +2 -2
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/models.py +111 -85
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/inline_datasource.py +4 -4
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/select_merge_node.py +7 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/base_node.py +3 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/merge_node.py +10 -10
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/select_node_v2.py +6 -2
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/utility.py +3 -3
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/query_processor.py +21 -17
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/query_debugger.py +5 -1
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/parse_engine.py +17 -14
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/render.py +25 -7
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/trilogy.lark +4 -2
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/README.md +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pyproject.toml +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/SOURCES.txt +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/setup.cfg +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/setup.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_models.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_select.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_show.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/constants.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/enums.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/environment_helpers.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/functions.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/inline_constant.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/concept_strategies_v3.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/common.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/filter_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/base.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/common.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/duckdb.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/presto.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/executor.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/common.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/utility.py +0 -0
|
@@ -491,6 +491,28 @@ select x;
|
|
|
491
491
|
assert "abcdef as test" in results, results
|
|
492
492
|
|
|
493
493
|
|
|
494
|
+
def test_datasource_where_equivalent():
|
|
495
|
+
|
|
496
|
+
text = """
|
|
497
|
+
key x int;
|
|
498
|
+
key y int;
|
|
499
|
+
|
|
500
|
+
datasource test (
|
|
501
|
+
x:x,
|
|
502
|
+
y:~y)
|
|
503
|
+
grain(x)
|
|
504
|
+
complete where y > 10
|
|
505
|
+
address `abc:def`
|
|
506
|
+
;
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
env, parsed = parse_text(text)
|
|
511
|
+
|
|
512
|
+
ds = parsed[-1]
|
|
513
|
+
assert ds.non_partial_for.conditional.right == 10
|
|
514
|
+
|
|
515
|
+
|
|
494
516
|
def test_filter_concise():
|
|
495
517
|
|
|
496
518
|
text = """
|
|
@@ -134,7 +134,7 @@ def test_query_aggregation(test_environment, test_environment_graph):
|
|
|
134
134
|
environment=test_environment, graph=test_environment_graph, statement=select
|
|
135
135
|
)
|
|
136
136
|
|
|
137
|
-
assert {datasource.identifier} == {"
|
|
137
|
+
assert {datasource.identifier} == {"revenue_at_local_order_id_at_abstract"}
|
|
138
138
|
check = datasource
|
|
139
139
|
assert len(check.input_concepts) == 2
|
|
140
140
|
assert check.input_concepts[0].name == "revenue"
|
|
@@ -6,7 +6,7 @@ from trilogy.core.models import Concept, Datasource
|
|
|
6
6
|
def concept_to_node(input: Concept) -> str:
|
|
7
7
|
# if input.purpose == Purpose.METRIC:
|
|
8
8
|
# return f"c~{input.namespace}.{input.name}@{input.grain}"
|
|
9
|
-
return f"c~{input.
|
|
9
|
+
return f"c~{input.address}@{input.grain}"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def datasource_to_node(input: Datasource) -> str:
|
|
@@ -14,7 +14,7 @@ def datasource_to_node(input: Datasource) -> str:
|
|
|
14
14
|
# return "ds~join~" + ",".join(
|
|
15
15
|
# [datasource_to_node(sub) for sub in input.datasources]
|
|
16
16
|
# )
|
|
17
|
-
return f"ds~{input.
|
|
17
|
+
return f"ds~{input.identifier}"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class ReferenceGraph(nx.DiGraph):
|
|
@@ -1719,7 +1719,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1719
1719
|
def to_datasource(
|
|
1720
1720
|
self,
|
|
1721
1721
|
namespace: str,
|
|
1722
|
-
|
|
1722
|
+
name: str,
|
|
1723
1723
|
address: Address,
|
|
1724
1724
|
grain: Grain | None = None,
|
|
1725
1725
|
) -> Datasource:
|
|
@@ -1753,7 +1753,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1753
1753
|
condition = self.having_clause.conditional
|
|
1754
1754
|
|
|
1755
1755
|
new_datasource = Datasource(
|
|
1756
|
-
|
|
1756
|
+
name=name,
|
|
1757
1757
|
address=address,
|
|
1758
1758
|
grain=grain or self.grain,
|
|
1759
1759
|
columns=columns,
|
|
@@ -2059,7 +2059,7 @@ class MergeStatementV2(HasUUID, Namespaced, BaseModel):
|
|
|
2059
2059
|
|
|
2060
2060
|
|
|
2061
2061
|
class Datasource(HasUUID, Namespaced, BaseModel):
|
|
2062
|
-
|
|
2062
|
+
name: str
|
|
2063
2063
|
columns: List[ColumnAssignment]
|
|
2064
2064
|
address: Union[Address, str]
|
|
2065
2065
|
grain: Grain = Field(
|
|
@@ -2094,10 +2094,14 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2094
2094
|
self.add_column(target, original[0].alias, modifiers)
|
|
2095
2095
|
|
|
2096
2096
|
@property
|
|
2097
|
-
def
|
|
2097
|
+
def identifier(self) -> str:
|
|
2098
2098
|
if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
|
|
2099
|
-
return self.
|
|
2100
|
-
return f"{self.namespace}.{self.
|
|
2099
|
+
return self.name
|
|
2100
|
+
return f"{self.namespace}.{self.name}"
|
|
2101
|
+
|
|
2102
|
+
@property
|
|
2103
|
+
def safe_identifier(self) -> str:
|
|
2104
|
+
return self.identifier.replace(".", "_")
|
|
2101
2105
|
|
|
2102
2106
|
@property
|
|
2103
2107
|
def condition(self):
|
|
@@ -2166,13 +2170,13 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2166
2170
|
return self
|
|
2167
2171
|
|
|
2168
2172
|
def __repr__(self):
|
|
2169
|
-
return f"Datasource<{self.
|
|
2173
|
+
return f"Datasource<{self.identifier}@<{self.grain}>"
|
|
2170
2174
|
|
|
2171
2175
|
def __str__(self):
|
|
2172
2176
|
return self.__repr__()
|
|
2173
2177
|
|
|
2174
2178
|
def __hash__(self):
|
|
2175
|
-
return self.
|
|
2179
|
+
return self.identifier.__hash__()
|
|
2176
2180
|
|
|
2177
2181
|
def with_namespace(self, namespace: str):
|
|
2178
2182
|
new_namespace = (
|
|
@@ -2181,7 +2185,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2181
2185
|
else namespace
|
|
2182
2186
|
)
|
|
2183
2187
|
return Datasource(
|
|
2184
|
-
|
|
2188
|
+
name=self.name,
|
|
2185
2189
|
namespace=new_namespace,
|
|
2186
2190
|
grain=self.grain.with_namespace(namespace),
|
|
2187
2191
|
address=self.address,
|
|
@@ -2231,19 +2235,6 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2231
2235
|
f" {existing}."
|
|
2232
2236
|
)
|
|
2233
2237
|
|
|
2234
|
-
@property
|
|
2235
|
-
def name(self) -> str:
|
|
2236
|
-
return self.identifier
|
|
2237
|
-
# TODO: namespace all references
|
|
2238
|
-
# return f'{self.namespace}_{self.identifier}'
|
|
2239
|
-
|
|
2240
|
-
@property
|
|
2241
|
-
def full_name(self) -> str:
|
|
2242
|
-
if not self.namespace:
|
|
2243
|
-
return self.identifier
|
|
2244
|
-
namespace = self.namespace.replace(".", "_") if self.namespace else ""
|
|
2245
|
-
return f"{namespace}_{self.identifier}"
|
|
2246
|
-
|
|
2247
2238
|
@property
|
|
2248
2239
|
def safe_location(self) -> str:
|
|
2249
2240
|
if isinstance(self.address, Address):
|
|
@@ -2298,7 +2289,7 @@ class BaseJoin(BaseModel):
|
|
|
2298
2289
|
super().__init__(**data)
|
|
2299
2290
|
if (
|
|
2300
2291
|
self.left_datasource
|
|
2301
|
-
and self.left_datasource.
|
|
2292
|
+
and self.left_datasource.identifier == self.right_datasource.identifier
|
|
2302
2293
|
):
|
|
2303
2294
|
raise SyntaxError(
|
|
2304
2295
|
f"Cannot join a dataself to itself, joining {self.left_datasource} and"
|
|
@@ -2410,6 +2401,10 @@ class QueryDatasource(BaseModel):
|
|
|
2410
2401
|
def __repr__(self):
|
|
2411
2402
|
return f"{self.identifier}@<{self.grain}>"
|
|
2412
2403
|
|
|
2404
|
+
@property
|
|
2405
|
+
def safe_identifier(self):
|
|
2406
|
+
return self.identifier.replace(".", "_")
|
|
2407
|
+
|
|
2413
2408
|
@property
|
|
2414
2409
|
def non_partial_concept_addresses(self) -> List[str]:
|
|
2415
2410
|
return [
|
|
@@ -2474,10 +2469,6 @@ class QueryDatasource(BaseModel):
|
|
|
2474
2469
|
def name(self):
|
|
2475
2470
|
return self.identifier
|
|
2476
2471
|
|
|
2477
|
-
@property
|
|
2478
|
-
def full_name(self):
|
|
2479
|
-
return self.identifier
|
|
2480
|
-
|
|
2481
2472
|
@property
|
|
2482
2473
|
def group_required(self) -> bool:
|
|
2483
2474
|
if self.force_group is True:
|
|
@@ -2524,10 +2515,12 @@ class QueryDatasource(BaseModel):
|
|
|
2524
2515
|
merged_datasources = {}
|
|
2525
2516
|
|
|
2526
2517
|
for ds in [*self.datasources, *other.datasources]:
|
|
2527
|
-
if ds.
|
|
2528
|
-
merged_datasources[ds.
|
|
2518
|
+
if ds.safe_identifier in merged_datasources:
|
|
2519
|
+
merged_datasources[ds.safe_identifier] = (
|
|
2520
|
+
merged_datasources[ds.safe_identifier] + ds
|
|
2521
|
+
)
|
|
2529
2522
|
else:
|
|
2530
|
-
merged_datasources[ds.
|
|
2523
|
+
merged_datasources[ds.safe_identifier] = ds
|
|
2531
2524
|
|
|
2532
2525
|
final_source_map = defaultdict(set)
|
|
2533
2526
|
for key in self.source_map:
|
|
@@ -2538,7 +2531,9 @@ class QueryDatasource(BaseModel):
|
|
|
2538
2531
|
if key not in final_source_map:
|
|
2539
2532
|
final_source_map[key] = other.source_map[key]
|
|
2540
2533
|
for k, v in final_source_map.items():
|
|
2541
|
-
final_source_map[k] = set(
|
|
2534
|
+
final_source_map[k] = set(
|
|
2535
|
+
merged_datasources[x.safe_identifier] for x in list(v)
|
|
2536
|
+
)
|
|
2542
2537
|
self_hidden = self.hidden_concepts or []
|
|
2543
2538
|
other_hidden = other.hidden_concepts or []
|
|
2544
2539
|
hidden = [x for x in self_hidden if x.address in other_hidden]
|
|
@@ -2578,7 +2573,7 @@ class QueryDatasource(BaseModel):
|
|
|
2578
2573
|
)
|
|
2579
2574
|
# partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
|
|
2580
2575
|
return (
|
|
2581
|
-
"_join_".join([d.
|
|
2576
|
+
"_join_".join([d.identifier for d in self.datasources])
|
|
2582
2577
|
+ (f"_at_{grain}" if grain else "_at_abstract")
|
|
2583
2578
|
+ (f"_filtered_by_{filters}" if filters else "")
|
|
2584
2579
|
# + (f"_partial_{partial}" if partial else "")
|
|
@@ -2594,8 +2589,9 @@ class QueryDatasource(BaseModel):
|
|
|
2594
2589
|
for x in self.datasources:
|
|
2595
2590
|
# query datasources should be referenced by their alias, always
|
|
2596
2591
|
force_alias = isinstance(x, QueryDatasource)
|
|
2592
|
+
#
|
|
2597
2593
|
use_raw_name = isinstance(x, Datasource) and not force_alias
|
|
2598
|
-
if source and x.
|
|
2594
|
+
if source and x.safe_identifier != source:
|
|
2599
2595
|
continue
|
|
2600
2596
|
try:
|
|
2601
2597
|
return x.get_alias(
|
|
@@ -2649,6 +2645,14 @@ class CTE(BaseModel):
|
|
|
2649
2645
|
base_name_override: Optional[str] = None
|
|
2650
2646
|
base_alias_override: Optional[str] = None
|
|
2651
2647
|
|
|
2648
|
+
@property
|
|
2649
|
+
def identifier(self):
|
|
2650
|
+
return self.name
|
|
2651
|
+
|
|
2652
|
+
@property
|
|
2653
|
+
def safe_identifier(self):
|
|
2654
|
+
return self.name
|
|
2655
|
+
|
|
2652
2656
|
@computed_field # type: ignore
|
|
2653
2657
|
@property
|
|
2654
2658
|
def output_lcl(self) -> LooseConceptList:
|
|
@@ -2746,7 +2750,7 @@ class CTE(BaseModel):
|
|
|
2746
2750
|
return False
|
|
2747
2751
|
if any(
|
|
2748
2752
|
[
|
|
2749
|
-
x.
|
|
2753
|
+
x.safe_identifier == ds_being_inlined.safe_identifier
|
|
2750
2754
|
for x in self.source.datasources
|
|
2751
2755
|
]
|
|
2752
2756
|
):
|
|
@@ -2757,39 +2761,49 @@ class CTE(BaseModel):
|
|
|
2757
2761
|
*[
|
|
2758
2762
|
x
|
|
2759
2763
|
for x in self.source.datasources
|
|
2760
|
-
if x.
|
|
2764
|
+
if x.safe_identifier != qds_being_inlined.safe_identifier
|
|
2761
2765
|
],
|
|
2762
2766
|
]
|
|
2763
2767
|
# need to identify this before updating joins
|
|
2764
2768
|
if self.base_name == parent.name:
|
|
2765
2769
|
self.base_name_override = ds_being_inlined.safe_location
|
|
2766
|
-
self.base_alias_override = ds_being_inlined.
|
|
2770
|
+
self.base_alias_override = ds_being_inlined.safe_identifier
|
|
2767
2771
|
|
|
2768
2772
|
for join in self.joins:
|
|
2769
2773
|
if isinstance(join, InstantiatedUnnestJoin):
|
|
2770
2774
|
continue
|
|
2771
|
-
if
|
|
2775
|
+
if (
|
|
2776
|
+
join.left_cte
|
|
2777
|
+
and join.left_cte.safe_identifier == parent.safe_identifier
|
|
2778
|
+
):
|
|
2772
2779
|
join.inline_cte(parent)
|
|
2773
2780
|
if join.joinkey_pairs:
|
|
2774
2781
|
for pair in join.joinkey_pairs:
|
|
2775
|
-
if pair.cte and pair.cte.
|
|
2782
|
+
if pair.cte and pair.cte.safe_identifier == parent.safe_identifier:
|
|
2776
2783
|
join.inline_cte(parent)
|
|
2777
|
-
if join.right_cte.
|
|
2784
|
+
if join.right_cte.safe_identifier == parent.safe_identifier:
|
|
2778
2785
|
join.inline_cte(parent)
|
|
2779
2786
|
for k, v in self.source_map.items():
|
|
2780
2787
|
if isinstance(v, list):
|
|
2781
2788
|
self.source_map[k] = [
|
|
2782
|
-
|
|
2789
|
+
(
|
|
2790
|
+
ds_being_inlined.safe_identifier
|
|
2791
|
+
if x == parent.safe_identifier
|
|
2792
|
+
else x
|
|
2793
|
+
)
|
|
2794
|
+
for x in v
|
|
2783
2795
|
]
|
|
2784
|
-
elif v == parent.
|
|
2785
|
-
self.source_map[k] = [ds_being_inlined.
|
|
2796
|
+
elif v == parent.safe_identifier:
|
|
2797
|
+
self.source_map[k] = [ds_being_inlined.safe_identifier]
|
|
2786
2798
|
|
|
2787
2799
|
# zip in any required values for lookups
|
|
2788
2800
|
for k in ds_being_inlined.output_lcl.addresses:
|
|
2789
2801
|
if k in self.source_map and self.source_map[k]:
|
|
2790
2802
|
continue
|
|
2791
|
-
self.source_map[k] = [ds_being_inlined.
|
|
2792
|
-
self.parent_ctes = [
|
|
2803
|
+
self.source_map[k] = [ds_being_inlined.safe_identifier]
|
|
2804
|
+
self.parent_ctes = [
|
|
2805
|
+
x for x in self.parent_ctes if x.safe_identifier != parent.safe_identifier
|
|
2806
|
+
]
|
|
2793
2807
|
if force_group:
|
|
2794
2808
|
self.group_to_grain = True
|
|
2795
2809
|
return True
|
|
@@ -3006,28 +3020,22 @@ class Join(BaseModel):
|
|
|
3006
3020
|
def inline_cte(self, cte: CTE):
|
|
3007
3021
|
self.inlined_ctes.add(cte.name)
|
|
3008
3022
|
|
|
3009
|
-
# @property
|
|
3010
|
-
# def left_name(self) -> str:
|
|
3011
|
-
# if self.left_cte.name in self.inlined_ctes:
|
|
3012
|
-
# return self.left_cte.source.datasources[0].identifier
|
|
3013
|
-
# return self.left_cte.name
|
|
3014
|
-
|
|
3015
3023
|
def get_name(self, cte: CTE):
|
|
3016
|
-
if cte.
|
|
3017
|
-
return cte.source.datasources[0].
|
|
3018
|
-
return cte.
|
|
3024
|
+
if cte.identifier in self.inlined_ctes:
|
|
3025
|
+
return cte.source.datasources[0].safe_identifier
|
|
3026
|
+
return cte.safe_identifier
|
|
3019
3027
|
|
|
3020
3028
|
@property
|
|
3021
3029
|
def right_name(self) -> str:
|
|
3022
|
-
if self.right_cte.
|
|
3023
|
-
return self.right_cte.source.datasources[0].
|
|
3024
|
-
return self.right_cte.
|
|
3030
|
+
if self.right_cte.identifier in self.inlined_ctes:
|
|
3031
|
+
return self.right_cte.source.datasources[0].safe_identifier
|
|
3032
|
+
return self.right_cte.safe_identifier
|
|
3025
3033
|
|
|
3026
3034
|
@property
|
|
3027
3035
|
def right_ref(self) -> str:
|
|
3028
|
-
if self.right_cte.
|
|
3029
|
-
return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].
|
|
3030
|
-
return self.right_cte.
|
|
3036
|
+
if self.right_cte.identifier in self.inlined_ctes:
|
|
3037
|
+
return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].safe_identifier}"
|
|
3038
|
+
return self.right_cte.safe_identifier
|
|
3031
3039
|
|
|
3032
3040
|
@property
|
|
3033
3041
|
def unique_id(self) -> str:
|
|
@@ -3306,7 +3314,9 @@ class Environment(BaseModel):
|
|
|
3306
3314
|
] = Field(default_factory=EnvironmentDatasourceDict)
|
|
3307
3315
|
functions: Dict[str, Function] = Field(default_factory=dict)
|
|
3308
3316
|
data_types: Dict[str, DataType] = Field(default_factory=dict)
|
|
3309
|
-
imports: Dict[str, ImportStatement] = Field(
|
|
3317
|
+
imports: Dict[str, list[ImportStatement]] = Field(
|
|
3318
|
+
default_factory=lambda: defaultdict(list)
|
|
3319
|
+
)
|
|
3310
3320
|
namespace: str = DEFAULT_NAMESPACE
|
|
3311
3321
|
working_path: str | Path = Field(default_factory=lambda: os.getcwd())
|
|
3312
3322
|
environment_config: EnvironmentOptions = Field(default_factory=EnvironmentOptions)
|
|
@@ -3420,14 +3430,28 @@ class Environment(BaseModel):
|
|
|
3420
3430
|
f"Assignment to concept '{lookup}' is a duplicate declaration;"
|
|
3421
3431
|
)
|
|
3422
3432
|
|
|
3423
|
-
def add_import(
|
|
3424
|
-
self
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3433
|
+
def add_import(
|
|
3434
|
+
self, alias: str, source: Environment, imp_stm: ImportStatement | None = None
|
|
3435
|
+
):
|
|
3436
|
+
exists = False
|
|
3437
|
+
existing = self.imports[alias]
|
|
3438
|
+
if imp_stm:
|
|
3439
|
+
if any([x.path == imp_stm.path for x in existing]):
|
|
3440
|
+
exists = True
|
|
3441
|
+
|
|
3442
|
+
else:
|
|
3443
|
+
if any([x.path == source.working_path for x in existing]):
|
|
3444
|
+
exists = True
|
|
3445
|
+
imp_stm = ImportStatement(alias=alias, path=Path(source.working_path))
|
|
3446
|
+
|
|
3447
|
+
if not exists:
|
|
3448
|
+
self.imports[alias].append(imp_stm)
|
|
3449
|
+
|
|
3450
|
+
for _, concept in source.concepts.items():
|
|
3451
|
+
self.add_concept(concept.with_namespace(alias), _ignore_cache=True)
|
|
3452
|
+
|
|
3453
|
+
for _, datasource in source.datasources.items():
|
|
3454
|
+
self.add_datasource(datasource.with_namespace(alias), _ignore_cache=True)
|
|
3431
3455
|
self.gen_concept_list_caches()
|
|
3432
3456
|
return self
|
|
3433
3457
|
|
|
@@ -3438,18 +3462,15 @@ class Environment(BaseModel):
|
|
|
3438
3462
|
apath[-1] = apath[-1] + ".preql"
|
|
3439
3463
|
|
|
3440
3464
|
target: Path = Path(self.working_path, *apath)
|
|
3465
|
+
if alias in self.imports:
|
|
3466
|
+
imports = self.imports[alias]
|
|
3467
|
+
for x in imports:
|
|
3468
|
+
if x.path == target:
|
|
3469
|
+
return imports
|
|
3441
3470
|
if env:
|
|
3442
|
-
self.imports[alias]
|
|
3443
|
-
alias=alias, path=target, environment=env
|
|
3471
|
+
self.imports[alias].append(
|
|
3472
|
+
ImportStatement(alias=alias, path=target, environment=env)
|
|
3444
3473
|
)
|
|
3445
|
-
|
|
3446
|
-
elif alias in self.imports:
|
|
3447
|
-
current = self.imports[alias]
|
|
3448
|
-
env = self.imports[alias].environment
|
|
3449
|
-
if current.path != target:
|
|
3450
|
-
raise ImportError(
|
|
3451
|
-
f"Attempted to import {target} with alias {alias} but {alias} is already imported from {current.path}"
|
|
3452
|
-
)
|
|
3453
3474
|
else:
|
|
3454
3475
|
try:
|
|
3455
3476
|
with open(target, "r", encoding="utf-8") as f:
|
|
@@ -3468,14 +3489,13 @@ class Environment(BaseModel):
|
|
|
3468
3489
|
f"Unable to import file {target.parent}, parsing error: {e}"
|
|
3469
3490
|
)
|
|
3470
3491
|
env = nparser.environment
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
self.add_concept(concept.with_namespace(alias))
|
|
3492
|
+
for _, concept in env.concepts.items():
|
|
3493
|
+
self.add_concept(concept.with_namespace(alias))
|
|
3474
3494
|
|
|
3475
|
-
|
|
3476
|
-
|
|
3495
|
+
for _, datasource in env.datasources.items():
|
|
3496
|
+
self.add_datasource(datasource.with_namespace(alias))
|
|
3477
3497
|
imps = ImportStatement(alias=alias, path=target, environment=env)
|
|
3478
|
-
self.imports[alias]
|
|
3498
|
+
self.imports[alias].append(imps)
|
|
3479
3499
|
return imps
|
|
3480
3500
|
|
|
3481
3501
|
def parse(
|
|
@@ -3538,8 +3558,14 @@ class Environment(BaseModel):
|
|
|
3538
3558
|
meta: Meta | None = None,
|
|
3539
3559
|
_ignore_cache: bool = False,
|
|
3540
3560
|
):
|
|
3541
|
-
self.datasources[datasource.
|
|
3561
|
+
self.datasources[datasource.identifier] = datasource
|
|
3562
|
+
|
|
3563
|
+
eligible_to_promote_roots = datasource.non_partial_for is None
|
|
3564
|
+
# mark this as canonical source
|
|
3542
3565
|
for current_concept in datasource.output_concepts:
|
|
3566
|
+
if not eligible_to_promote_roots:
|
|
3567
|
+
continue
|
|
3568
|
+
|
|
3543
3569
|
current_derivation = current_concept.derivation
|
|
3544
3570
|
# TODO: refine this section;
|
|
3545
3571
|
# too hacky for maintainability
|
|
@@ -63,14 +63,14 @@ class InlineDatasource(OptimizationRule):
|
|
|
63
63
|
for replaceable in to_inline:
|
|
64
64
|
if replaceable.name not in self.candidates[cte.name]:
|
|
65
65
|
self.candidates[cte.name].add(replaceable.name)
|
|
66
|
-
self.count[replaceable.source.
|
|
66
|
+
self.count[replaceable.source.identifier] += 1
|
|
67
67
|
return True
|
|
68
68
|
if (
|
|
69
|
-
self.count[replaceable.source.
|
|
69
|
+
self.count[replaceable.source.identifier]
|
|
70
70
|
> CONFIG.optimizations.constant_inline_cutoff
|
|
71
71
|
):
|
|
72
72
|
self.log(
|
|
73
|
-
f"Skipping inlining raw datasource {replaceable.source.
|
|
73
|
+
f"Skipping inlining raw datasource {replaceable.source.identifier} ({replaceable.name}) due to multiple references"
|
|
74
74
|
)
|
|
75
75
|
continue
|
|
76
76
|
if not replaceable.source.datasources[0].grain.issubset(replaceable.grain):
|
|
@@ -81,7 +81,7 @@ class InlineDatasource(OptimizationRule):
|
|
|
81
81
|
result = cte.inline_parent_datasource(replaceable, force_group=force_group)
|
|
82
82
|
if result:
|
|
83
83
|
self.log(
|
|
84
|
-
f"Inlined parent {replaceable.name} with {replaceable.source.
|
|
84
|
+
f"Inlined parent {replaceable.name} with {replaceable.source.identifier}"
|
|
85
85
|
)
|
|
86
86
|
optimized = True
|
|
87
87
|
else:
|
|
@@ -193,6 +193,7 @@ def create_select_node(
|
|
|
193
193
|
g,
|
|
194
194
|
environment: Environment,
|
|
195
195
|
depth: int,
|
|
196
|
+
conditions: WhereClause | None = None,
|
|
196
197
|
) -> StrategyNode:
|
|
197
198
|
ds_name = ds_name.split("~")[1]
|
|
198
199
|
all_concepts = [
|
|
@@ -231,6 +232,7 @@ def create_select_node(
|
|
|
231
232
|
c.concept for c in datasource.columns if c.is_nullable and c.concept in all_lcl
|
|
232
233
|
]
|
|
233
234
|
nullable_lcl = LooseConceptList(concepts=nullable_concepts)
|
|
235
|
+
partial_is_full = conditions and (conditions == datasource.non_partial_for)
|
|
234
236
|
|
|
235
237
|
bcandidate: StrategyNode = SelectNode(
|
|
236
238
|
input_concepts=[c.concept for c in datasource.columns],
|
|
@@ -239,12 +241,15 @@ def create_select_node(
|
|
|
239
241
|
g=g,
|
|
240
242
|
parents=[],
|
|
241
243
|
depth=depth,
|
|
242
|
-
partial_concepts=
|
|
244
|
+
partial_concepts=(
|
|
245
|
+
[] if partial_is_full else [c for c in all_concepts if c in partial_lcl]
|
|
246
|
+
),
|
|
243
247
|
nullable_concepts=[c for c in all_concepts if c in nullable_lcl],
|
|
244
248
|
accept_partial=accept_partial,
|
|
245
249
|
datasource=datasource,
|
|
246
250
|
grain=Grain(components=all_concepts),
|
|
247
251
|
conditions=datasource.where.conditional if datasource.where else None,
|
|
252
|
+
render_condition=not partial_is_full,
|
|
248
253
|
)
|
|
249
254
|
|
|
250
255
|
# we need to nest the group node one further
|
|
@@ -312,6 +317,7 @@ def gen_select_merge_node(
|
|
|
312
317
|
accept_partial=accept_partial,
|
|
313
318
|
environment=environment,
|
|
314
319
|
depth=depth,
|
|
320
|
+
conditions=conditions,
|
|
315
321
|
)
|
|
316
322
|
for k, subgraph in sub_nodes.items()
|
|
317
323
|
]
|
|
@@ -165,6 +165,7 @@ class StrategyNode:
|
|
|
165
165
|
hidden_concepts: List[Concept] | None = None,
|
|
166
166
|
existence_concepts: List[Concept] | None = None,
|
|
167
167
|
virtual_output_concepts: List[Concept] | None = None,
|
|
168
|
+
render_condition: bool = True,
|
|
168
169
|
):
|
|
169
170
|
self.input_concepts: List[Concept] = (
|
|
170
171
|
unique(input_concepts, "address") if input_concepts else []
|
|
@@ -208,6 +209,7 @@ class StrategyNode:
|
|
|
208
209
|
)
|
|
209
210
|
self.validate_parents()
|
|
210
211
|
self.log = True
|
|
212
|
+
self.render_condition = render_condition
|
|
211
213
|
|
|
212
214
|
def add_parents(self, parents: list["StrategyNode"]):
|
|
213
215
|
self.parents += parents
|
|
@@ -380,6 +382,7 @@ class StrategyNode:
|
|
|
380
382
|
hidden_concepts=list(self.hidden_concepts),
|
|
381
383
|
existence_concepts=list(self.existence_concepts),
|
|
382
384
|
virtual_output_concepts=list(self.virtual_output_concepts),
|
|
385
|
+
render_condition=self.render_condition,
|
|
383
386
|
)
|
|
384
387
|
|
|
385
388
|
|
|
@@ -89,8 +89,8 @@ def deduplicate_nodes_and_joins(
|
|
|
89
89
|
joins = [
|
|
90
90
|
j
|
|
91
91
|
for j in joins
|
|
92
|
-
if j.left_node.resolve().
|
|
93
|
-
and j.right_node.resolve().
|
|
92
|
+
if j.left_node.resolve().identifier not in removed
|
|
93
|
+
and j.right_node.resolve().identifier not in removed
|
|
94
94
|
]
|
|
95
95
|
return joins, merged
|
|
96
96
|
|
|
@@ -155,8 +155,8 @@ class MergeNode(StrategyNode):
|
|
|
155
155
|
for join in node_joins:
|
|
156
156
|
left = join.left_node.resolve()
|
|
157
157
|
right = join.right_node.resolve()
|
|
158
|
-
if left.
|
|
159
|
-
raise SyntaxError(f"Cannot join node {left.
|
|
158
|
+
if left.identifier == right.identifier:
|
|
159
|
+
raise SyntaxError(f"Cannot join node {left.identifier} to itself")
|
|
160
160
|
joins.append(
|
|
161
161
|
BaseJoin(
|
|
162
162
|
left_datasource=left,
|
|
@@ -168,7 +168,7 @@ class MergeNode(StrategyNode):
|
|
|
168
168
|
)
|
|
169
169
|
return joins
|
|
170
170
|
|
|
171
|
-
def create_full_joins(self, dataset_list: List[QueryDatasource]):
|
|
171
|
+
def create_full_joins(self, dataset_list: List[QueryDatasource | Datasource]):
|
|
172
172
|
joins = []
|
|
173
173
|
seen = set()
|
|
174
174
|
for left_value in dataset_list:
|
|
@@ -198,7 +198,7 @@ class MergeNode(StrategyNode):
|
|
|
198
198
|
environment: Environment,
|
|
199
199
|
) -> List[BaseJoin | UnnestJoin]:
|
|
200
200
|
# only finally, join between them for unique values
|
|
201
|
-
dataset_list: List[QueryDatasource] = sorted(
|
|
201
|
+
dataset_list: List[QueryDatasource | Datasource] = sorted(
|
|
202
202
|
final_datasets, key=lambda x: -len(x.grain.components_copy)
|
|
203
203
|
)
|
|
204
204
|
|
|
@@ -238,13 +238,13 @@ class MergeNode(StrategyNode):
|
|
|
238
238
|
merged: dict[str, QueryDatasource | Datasource] = {}
|
|
239
239
|
final_joins: List[NodeJoin] | None = self.node_joins
|
|
240
240
|
for source in parent_sources:
|
|
241
|
-
if source.
|
|
241
|
+
if source.identifier in merged:
|
|
242
242
|
logger.info(
|
|
243
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.
|
|
243
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.identifier} into existing"
|
|
244
244
|
)
|
|
245
|
-
merged[source.
|
|
245
|
+
merged[source.identifier] = merged[source.identifier] + source
|
|
246
246
|
else:
|
|
247
|
-
merged[source.
|
|
247
|
+
merged[source.identifier] = source
|
|
248
248
|
|
|
249
249
|
# it's possible that we have more sources than we need
|
|
250
250
|
final_joins, merged = deduplicate_nodes_and_joins(
|