pytrilogy 0.0.2.54__tar.gz → 0.0.2.56__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.54/pytrilogy.egg-info → pytrilogy-0.0.2.56}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56/pytrilogy.egg-info}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_select.py +33 -1
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/environment_helpers.py +16 -5
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/models.py +99 -3
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/concept_strategies_v3.py +1 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/filter_node.py +5 -1
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/base_node.py +2 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/merge_node.py +0 -3
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/utility.py +45 -14
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/query_processor.py +1 -1
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/parse_engine.py +3 -71
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/render.py +21 -2
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/README.md +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/pyproject.toml +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/SOURCES.txt +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/setup.cfg +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/setup.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_enums.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_executor.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_models.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_parse_engine.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_parsing.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_show.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/constants.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/enums.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/functions.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/graph_models.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/inline_constant.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/inline_datasource.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/common.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/union_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/union_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/base.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/common.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/duckdb.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/presto.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/executor.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/common.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/parsing/trilogy.lark +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/utility.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# from trilogy.compiler import compile
|
|
2
|
-
from trilogy import Dialects
|
|
2
|
+
from trilogy import Dialects, Environment
|
|
3
3
|
from trilogy.core.models import Grain, SelectStatement
|
|
4
4
|
from trilogy.core.query_processor import process_query
|
|
5
5
|
from trilogy.dialect.bigquery import BigqueryDialect
|
|
@@ -176,3 +176,35 @@ select id + 2 as three;
|
|
|
176
176
|
|
|
177
177
|
result = Dialects.DUCK_DB.default_executor(environment=env).execute_text(q1)[-1]
|
|
178
178
|
assert result.fetchone().three == 3
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_select_from_components():
|
|
182
|
+
env = Environment()
|
|
183
|
+
q1 = """
|
|
184
|
+
|
|
185
|
+
key id int;
|
|
186
|
+
property id.class int;
|
|
187
|
+
property id.name string;
|
|
188
|
+
|
|
189
|
+
select
|
|
190
|
+
class,
|
|
191
|
+
upper(id.name)-> upper_name,
|
|
192
|
+
count(id) ->class_id_count,
|
|
193
|
+
;
|
|
194
|
+
"""
|
|
195
|
+
env, statements = env.parse(q1)
|
|
196
|
+
|
|
197
|
+
select: SelectStatement = statements[-1]
|
|
198
|
+
|
|
199
|
+
assert select.grain.components == {"local.class", "local.upper_name"}
|
|
200
|
+
assert select.local_concepts["local.class_id_count"].grain.components == {
|
|
201
|
+
"local.class",
|
|
202
|
+
"local.upper_name",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# SelectStatement.from_inputs(
|
|
206
|
+
# environment=env,
|
|
207
|
+
# selection=[SelectItem(concept=env.concepts["id"]),
|
|
208
|
+
# SelectItem(concept=env.concepts["id.class"])],
|
|
209
|
+
# input_components=[],
|
|
210
|
+
# )
|
|
@@ -11,12 +11,23 @@ from trilogy.core.models import (
|
|
|
11
11
|
)
|
|
12
12
|
from trilogy.parsing.common import Meta, arg_to_datatype, process_function_args
|
|
13
13
|
|
|
14
|
+
FUNCTION_DESCRIPTION_MAPS = {
|
|
15
|
+
FunctionType.DATE: "The date part of a timestamp/date. Integer, 0-31 depending on month.",
|
|
16
|
+
FunctionType.MONTH: "The month part of a timestamp/date. Integer, 1-12.",
|
|
17
|
+
FunctionType.YEAR: "The year part of a timestamp/date. Integer.",
|
|
18
|
+
FunctionType.QUARTER: "The quarter part of a timestamp/date. Integer, 1-4.",
|
|
19
|
+
FunctionType.DAY_OF_WEEK: "The day of the week part of a timestamp/date. Integer, 0-6.",
|
|
20
|
+
FunctionType.HOUR: "The hour part of a timestamp. Integer, 0-23.",
|
|
21
|
+
FunctionType.MINUTE: "The minute part of a timestamp. Integer, 0-59.",
|
|
22
|
+
FunctionType.SECOND: "The second part of a timestamp. Integer, 0-59.",
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
|
|
15
26
|
def generate_date_concepts(concept: Concept, environment: Environment):
|
|
16
27
|
if concept.metadata and concept.metadata.description:
|
|
17
28
|
base_description = concept.metadata.description
|
|
18
29
|
else:
|
|
19
|
-
base_description = f"a {concept.
|
|
30
|
+
base_description = f"a {concept.address}"
|
|
20
31
|
if concept.metadata and concept.metadata.line_number:
|
|
21
32
|
base_line_number = concept.metadata.line_number
|
|
22
33
|
else:
|
|
@@ -54,7 +65,7 @@ def generate_date_concepts(concept: Concept, environment: Environment):
|
|
|
54
65
|
concept.address,
|
|
55
66
|
),
|
|
56
67
|
metadata=Metadata(
|
|
57
|
-
description=f"Auto-derived
|
|
68
|
+
description=f"Auto-derived from {base_description}. {FUNCTION_DESCRIPTION_MAPS.get(ftype, ftype.value)}. ",
|
|
58
69
|
line_number=base_line_number,
|
|
59
70
|
concept_source=ConceptSource.AUTO_DERIVED,
|
|
60
71
|
),
|
|
@@ -68,7 +79,7 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
|
|
|
68
79
|
if concept.metadata and concept.metadata.description:
|
|
69
80
|
base_description = concept.metadata.description
|
|
70
81
|
else:
|
|
71
|
-
base_description =
|
|
82
|
+
base_description = concept.address
|
|
72
83
|
if concept.metadata and concept.metadata.line_number:
|
|
73
84
|
base_line_number = concept.metadata.line_number
|
|
74
85
|
else:
|
|
@@ -105,7 +116,7 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
|
|
|
105
116
|
concept.address,
|
|
106
117
|
),
|
|
107
118
|
metadata=Metadata(
|
|
108
|
-
description=f"Auto-derived
|
|
119
|
+
description=f"Auto-derived from {base_description}. {FUNCTION_DESCRIPTION_MAPS.get(ftype, ftype.value)}.",
|
|
109
120
|
line_number=base_line_number,
|
|
110
121
|
concept_source=ConceptSource.AUTO_DERIVED,
|
|
111
122
|
),
|
|
@@ -147,7 +158,7 @@ def generate_key_concepts(concept: Concept, environment: Environment):
|
|
|
147
158
|
concept.address,
|
|
148
159
|
},
|
|
149
160
|
metadata=Metadata(
|
|
150
|
-
description=f"Auto-derived
|
|
161
|
+
description=f"Auto-derived integer. The {ftype.value} of {concept.address}, {base_description}",
|
|
151
162
|
line_number=base_line_number,
|
|
152
163
|
concept_source=ConceptSource.AUTO_DERIVED,
|
|
153
164
|
),
|
|
@@ -620,7 +620,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
620
620
|
)
|
|
621
621
|
final_grain = self.grain or grain
|
|
622
622
|
keys = self.keys if self.keys else None
|
|
623
|
-
if self.is_aggregate and isinstance(new_lineage, Function):
|
|
623
|
+
if self.is_aggregate and isinstance(new_lineage, Function) and grain.components:
|
|
624
624
|
grain_components = [environment.concepts[c] for c in grain.components]
|
|
625
625
|
new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
|
|
626
626
|
final_grain = grain
|
|
@@ -852,7 +852,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
852
852
|
|
|
853
853
|
class ConceptRef(BaseModel):
|
|
854
854
|
address: str
|
|
855
|
-
line_no: int
|
|
855
|
+
line_no: int | None = None
|
|
856
856
|
|
|
857
857
|
def hydrate(self, environment: Environment) -> Concept:
|
|
858
858
|
return environment.concepts.__getitem__(self.address, self.line_no)
|
|
@@ -1015,6 +1015,7 @@ class EnvironmentConceptDict(dict):
|
|
|
1015
1015
|
def raise_undefined(
|
|
1016
1016
|
self, key: str, line_no: int | None = None, file: Path | str | None = None
|
|
1017
1017
|
) -> Never:
|
|
1018
|
+
|
|
1018
1019
|
matches = self._find_similar_concepts(key)
|
|
1019
1020
|
message = f"Undefined concept: {key}."
|
|
1020
1021
|
if matches:
|
|
@@ -1660,6 +1661,96 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1660
1661
|
] = Field(default_factory=EnvironmentConceptDict)
|
|
1661
1662
|
grain: Grain = Field(default_factory=Grain)
|
|
1662
1663
|
|
|
1664
|
+
@classmethod
|
|
1665
|
+
def from_inputs(
|
|
1666
|
+
cls,
|
|
1667
|
+
environment: Environment,
|
|
1668
|
+
selection: List[SelectItem],
|
|
1669
|
+
order_by: OrderBy | None = None,
|
|
1670
|
+
limit: int | None = None,
|
|
1671
|
+
meta: Metadata | None = None,
|
|
1672
|
+
where_clause: WhereClause | None = None,
|
|
1673
|
+
having_clause: HavingClause | None = None,
|
|
1674
|
+
) -> "SelectStatement":
|
|
1675
|
+
|
|
1676
|
+
output = SelectStatement(
|
|
1677
|
+
selection=selection,
|
|
1678
|
+
where_clause=where_clause,
|
|
1679
|
+
having_clause=having_clause,
|
|
1680
|
+
limit=limit,
|
|
1681
|
+
order_by=order_by,
|
|
1682
|
+
meta=meta or Metadata(),
|
|
1683
|
+
)
|
|
1684
|
+
for parse_pass in [
|
|
1685
|
+
1,
|
|
1686
|
+
2,
|
|
1687
|
+
]:
|
|
1688
|
+
# the first pass will result in all concepts being defined
|
|
1689
|
+
# the second will get grains appropriately
|
|
1690
|
+
# eg if someone does sum(x)->a, b+c -> z - we don't know if Z is a key to group by or an aggregate
|
|
1691
|
+
# until after the first pass, and so don't know the grain of a
|
|
1692
|
+
|
|
1693
|
+
if parse_pass == 1:
|
|
1694
|
+
grain = Grain.from_concepts(
|
|
1695
|
+
[
|
|
1696
|
+
x.content
|
|
1697
|
+
for x in output.selection
|
|
1698
|
+
if isinstance(x.content, Concept)
|
|
1699
|
+
],
|
|
1700
|
+
where_clause=output.where_clause,
|
|
1701
|
+
)
|
|
1702
|
+
if parse_pass == 2:
|
|
1703
|
+
grain = Grain.from_concepts(
|
|
1704
|
+
output.output_components, where_clause=output.where_clause
|
|
1705
|
+
)
|
|
1706
|
+
output.grain = grain
|
|
1707
|
+
pass_grain = Grain() if parse_pass == 1 else grain
|
|
1708
|
+
for item in selection:
|
|
1709
|
+
# we don't know the grain of an aggregate at assignment time
|
|
1710
|
+
# so rebuild at this point in the tree
|
|
1711
|
+
# TODO: simplify
|
|
1712
|
+
if isinstance(item.content, ConceptTransform):
|
|
1713
|
+
new_concept = item.content.output.with_select_context(
|
|
1714
|
+
output.local_concepts,
|
|
1715
|
+
# the first pass grain will be incorrect
|
|
1716
|
+
pass_grain,
|
|
1717
|
+
environment=environment,
|
|
1718
|
+
)
|
|
1719
|
+
output.local_concepts[new_concept.address] = new_concept
|
|
1720
|
+
item.content.output = new_concept
|
|
1721
|
+
if parse_pass == 2 and CONFIG.select_as_definition:
|
|
1722
|
+
environment.add_concept(new_concept)
|
|
1723
|
+
elif isinstance(item.content, UndefinedConcept):
|
|
1724
|
+
environment.concepts.raise_undefined(
|
|
1725
|
+
item.content.address,
|
|
1726
|
+
line_no=item.content.metadata.line_number,
|
|
1727
|
+
file=environment.env_file_path,
|
|
1728
|
+
)
|
|
1729
|
+
elif isinstance(item.content, Concept):
|
|
1730
|
+
# Sometimes cached values here don't have the latest info
|
|
1731
|
+
# but we can't just use environment, as it might not have the right grain.
|
|
1732
|
+
item.content = item.content.with_select_context(
|
|
1733
|
+
output.local_concepts,
|
|
1734
|
+
pass_grain,
|
|
1735
|
+
environment=environment,
|
|
1736
|
+
)
|
|
1737
|
+
output.local_concepts[item.content.address] = item.content
|
|
1738
|
+
|
|
1739
|
+
if order_by:
|
|
1740
|
+
output.order_by = order_by.with_select_context(
|
|
1741
|
+
local_concepts=output.local_concepts,
|
|
1742
|
+
grain=output.grain,
|
|
1743
|
+
environment=environment,
|
|
1744
|
+
)
|
|
1745
|
+
if output.having_clause:
|
|
1746
|
+
output.having_clause = output.having_clause.with_select_context(
|
|
1747
|
+
local_concepts=output.local_concepts,
|
|
1748
|
+
grain=output.grain,
|
|
1749
|
+
environment=environment,
|
|
1750
|
+
)
|
|
1751
|
+
output.validate_syntax(environment)
|
|
1752
|
+
return output
|
|
1753
|
+
|
|
1663
1754
|
def validate_syntax(self, environment: Environment):
|
|
1664
1755
|
if self.where_clause:
|
|
1665
1756
|
for x in self.where_clause.concept_arguments:
|
|
@@ -3264,6 +3355,7 @@ class Environment(BaseModel):
|
|
|
3264
3355
|
alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
|
|
3265
3356
|
# TODO: support freezing environments to avoid mutation
|
|
3266
3357
|
frozen: bool = False
|
|
3358
|
+
env_file_path: Path | None = None
|
|
3267
3359
|
|
|
3268
3360
|
def freeze(self):
|
|
3269
3361
|
self.frozen = True
|
|
@@ -3317,7 +3409,11 @@ class Environment(BaseModel):
|
|
|
3317
3409
|
path = Path(path)
|
|
3318
3410
|
with open(path, "r") as f:
|
|
3319
3411
|
read = f.read()
|
|
3320
|
-
return Environment(working_path=
|
|
3412
|
+
return Environment(working_path=path.parent, env_file_path=path).parse(read)[0]
|
|
3413
|
+
|
|
3414
|
+
@classmethod
|
|
3415
|
+
def from_string(cls, input: str) -> "Environment":
|
|
3416
|
+
return Environment().parse(input)[0]
|
|
3321
3417
|
|
|
3322
3418
|
@classmethod
|
|
3323
3419
|
def from_cache(cls, path) -> Optional["Environment"]:
|
|
@@ -866,6 +866,7 @@ def _search_concepts(
|
|
|
866
866
|
)
|
|
867
867
|
if complete == ValidationResult.INCOMPLETE_CONDITION:
|
|
868
868
|
cond_dict = {str(node): node.preexisting_conditions for node in stack}
|
|
869
|
+
logger.error(f"Have {cond_dict} and need {str(conditions)}")
|
|
869
870
|
raise SyntaxError(f"Have {cond_dict} and need {str(conditions)}")
|
|
870
871
|
# early exit if we have a complete stack with one node
|
|
871
872
|
# we can only early exit if we have a complete stack
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/filter_node.py
RENAMED
|
@@ -60,7 +60,7 @@ def gen_filter_node(
|
|
|
60
60
|
g=g,
|
|
61
61
|
depth=depth + 1,
|
|
62
62
|
history=history,
|
|
63
|
-
|
|
63
|
+
conditions=conditions,
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
flattened_existence = [x for y in parent_existence_concepts for x in y]
|
|
@@ -194,6 +194,9 @@ def gen_filter_node(
|
|
|
194
194
|
history=history,
|
|
195
195
|
conditions=conditions,
|
|
196
196
|
)
|
|
197
|
+
logger.info(
|
|
198
|
+
f"{padding(depth)}{LOGGER_PREFIX} returning filter node and enrich node with {enrich_node.output_concepts} and {enrich_node.input_concepts}"
|
|
199
|
+
)
|
|
197
200
|
return MergeNode(
|
|
198
201
|
input_concepts=[concept, immediate_parent] + local_optional,
|
|
199
202
|
output_concepts=[
|
|
@@ -206,4 +209,5 @@ def gen_filter_node(
|
|
|
206
209
|
filter_node,
|
|
207
210
|
enrich_node,
|
|
208
211
|
],
|
|
212
|
+
preexisting_conditions=conditions.conditional if conditions else None,
|
|
209
213
|
)
|
|
@@ -210,6 +210,8 @@ class StrategyNode:
|
|
|
210
210
|
return self
|
|
211
211
|
|
|
212
212
|
def add_condition(self, condition: Conditional | Comparison | Parenthetical):
|
|
213
|
+
if self.conditions and condition == self.conditions:
|
|
214
|
+
return self
|
|
213
215
|
if self.conditions:
|
|
214
216
|
self.conditions = Conditional(
|
|
215
217
|
left=self.conditions, right=condition, operator=BooleanOperator.AND
|
|
@@ -224,9 +224,6 @@ class MergeNode(StrategyNode):
|
|
|
224
224
|
f"{self.logging_prefix}{LOGGER_PREFIX} Final joins is not null {final_joins} but is empty, skipping join generation"
|
|
225
225
|
)
|
|
226
226
|
return []
|
|
227
|
-
|
|
228
|
-
for join in joins:
|
|
229
|
-
logger.info(f"{self.logging_prefix}{LOGGER_PREFIX} final join {str(join)}")
|
|
230
227
|
return joins
|
|
231
228
|
|
|
232
229
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Set, Tuple
|
|
|
6
6
|
|
|
7
7
|
import networkx as nx
|
|
8
8
|
|
|
9
|
+
from trilogy.constants import logger
|
|
9
10
|
from trilogy.core.enums import BooleanOperator, FunctionClass, Granularity, Purpose
|
|
10
11
|
from trilogy.core.models import (
|
|
11
12
|
CTE,
|
|
@@ -160,6 +161,8 @@ def resolve_join_order_v2(
|
|
|
160
161
|
final_join_type = JoinType.LEFT_OUTER
|
|
161
162
|
elif any([x == JoinType.FULL for x in join_types]):
|
|
162
163
|
final_join_type = JoinType.FULL
|
|
164
|
+
logger.info("JOIN DEBUG")
|
|
165
|
+
logger.info(joinkeys)
|
|
163
166
|
output.append(
|
|
164
167
|
JoinOrderOutput(
|
|
165
168
|
# left=left_candidate,
|
|
@@ -306,11 +309,37 @@ def resolve_instantiated_concept(
|
|
|
306
309
|
)
|
|
307
310
|
|
|
308
311
|
|
|
312
|
+
def reduce_concept_pairs(input: list[ConceptPair]) -> list[ConceptPair]:
|
|
313
|
+
left_keys = set()
|
|
314
|
+
right_keys = set()
|
|
315
|
+
for pair in input:
|
|
316
|
+
if pair.left.purpose == Purpose.KEY:
|
|
317
|
+
left_keys.add(pair.left.address)
|
|
318
|
+
if pair.right.purpose == Purpose.KEY:
|
|
319
|
+
right_keys.add(pair.right.address)
|
|
320
|
+
final: list[ConceptPair] = []
|
|
321
|
+
for pair in input:
|
|
322
|
+
if (
|
|
323
|
+
pair.left.purpose == Purpose.PROPERTY
|
|
324
|
+
and pair.left.keys
|
|
325
|
+
and pair.left.keys.issubset(left_keys)
|
|
326
|
+
):
|
|
327
|
+
continue
|
|
328
|
+
if (
|
|
329
|
+
pair.right.purpose == Purpose.PROPERTY
|
|
330
|
+
and pair.right.keys
|
|
331
|
+
and pair.right.keys.issubset(right_keys)
|
|
332
|
+
):
|
|
333
|
+
continue
|
|
334
|
+
final.append(pair)
|
|
335
|
+
return final
|
|
336
|
+
|
|
337
|
+
|
|
309
338
|
def get_node_joins(
|
|
310
339
|
datasources: List[QueryDatasource | Datasource],
|
|
311
340
|
environment: Environment,
|
|
312
341
|
# concepts:List[Concept],
|
|
313
|
-
):
|
|
342
|
+
) -> List[BaseJoin]:
|
|
314
343
|
graph = nx.Graph()
|
|
315
344
|
partials: dict[str, list[str]] = {}
|
|
316
345
|
ds_node_map: dict[str, QueryDatasource | Datasource] = {}
|
|
@@ -337,19 +366,21 @@ def get_node_joins(
|
|
|
337
366
|
join_type=j.type,
|
|
338
367
|
# preserve empty field for maps
|
|
339
368
|
concepts=[] if not j.keys else None,
|
|
340
|
-
concept_pairs=
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
369
|
+
concept_pairs=reduce_concept_pairs(
|
|
370
|
+
[
|
|
371
|
+
ConceptPair(
|
|
372
|
+
left=resolve_instantiated_concept(
|
|
373
|
+
concept_map[concept], ds_node_map[k]
|
|
374
|
+
),
|
|
375
|
+
right=resolve_instantiated_concept(
|
|
376
|
+
concept_map[concept], ds_node_map[j.right]
|
|
377
|
+
),
|
|
378
|
+
existing_datasource=ds_node_map[k],
|
|
379
|
+
)
|
|
380
|
+
for k, v in j.keys.items()
|
|
381
|
+
for concept in v
|
|
382
|
+
]
|
|
383
|
+
),
|
|
353
384
|
)
|
|
354
385
|
for j in joins
|
|
355
386
|
]
|
|
@@ -359,7 +359,7 @@ def get_query_node(
|
|
|
359
359
|
environment.concepts[k] = v
|
|
360
360
|
graph = generate_graph(environment)
|
|
361
361
|
logger.info(
|
|
362
|
-
f"{LOGGER_PREFIX} getting source datasource for query with filtering {statement.where_clause_category} and
|
|
362
|
+
f"{LOGGER_PREFIX} getting source datasource for query with filtering {statement.where_clause_category} and grain {statement.grain}"
|
|
363
363
|
)
|
|
364
364
|
if not statement.output_components:
|
|
365
365
|
raise ValueError(f"Statement has no output components {statement}")
|
|
@@ -17,7 +17,6 @@ from lark.tree import Meta
|
|
|
17
17
|
from pydantic import ValidationError
|
|
18
18
|
|
|
19
19
|
from trilogy.constants import (
|
|
20
|
-
CONFIG,
|
|
21
20
|
DEFAULT_NAMESPACE,
|
|
22
21
|
NULL_VALUE,
|
|
23
22
|
MagicConstants,
|
|
@@ -108,7 +107,6 @@ from trilogy.core.models import (
|
|
|
108
107
|
StructType,
|
|
109
108
|
SubselectComparison,
|
|
110
109
|
TupleWrapper,
|
|
111
|
-
UndefinedConcept,
|
|
112
110
|
WhereClause,
|
|
113
111
|
Window,
|
|
114
112
|
WindowItem,
|
|
@@ -1030,81 +1028,15 @@ class ParseToObjects(Transformer):
|
|
|
1030
1028
|
if not select_items:
|
|
1031
1029
|
raise ValueError("Malformed select, missing select items")
|
|
1032
1030
|
|
|
1033
|
-
|
|
1031
|
+
return SelectStatement.from_inputs(
|
|
1032
|
+
environment=self.environment,
|
|
1034
1033
|
selection=select_items,
|
|
1034
|
+
order_by=order_by,
|
|
1035
1035
|
where_clause=where,
|
|
1036
1036
|
having_clause=having,
|
|
1037
1037
|
limit=limit,
|
|
1038
|
-
order_by=order_by,
|
|
1039
1038
|
meta=Metadata(line_number=meta.line),
|
|
1040
1039
|
)
|
|
1041
|
-
for parse_pass in [
|
|
1042
|
-
1,
|
|
1043
|
-
2,
|
|
1044
|
-
]:
|
|
1045
|
-
# the first pass will result in all concepts being defined
|
|
1046
|
-
# the second will get grains appropriately
|
|
1047
|
-
# eg if someone does sum(x)->a, b+c -> z - we don't know if Z is a key to group by or an aggregate
|
|
1048
|
-
# until after the first pass, and so don't know the grain of a
|
|
1049
|
-
|
|
1050
|
-
if parse_pass == 1:
|
|
1051
|
-
grain = Grain.from_concepts(
|
|
1052
|
-
[
|
|
1053
|
-
x.content
|
|
1054
|
-
for x in output.selection
|
|
1055
|
-
if isinstance(x.content, Concept)
|
|
1056
|
-
],
|
|
1057
|
-
where_clause=output.where_clause,
|
|
1058
|
-
)
|
|
1059
|
-
if parse_pass == 2:
|
|
1060
|
-
grain = Grain.from_concepts(
|
|
1061
|
-
output.output_components, where_clause=output.where_clause
|
|
1062
|
-
)
|
|
1063
|
-
output.grain = grain
|
|
1064
|
-
for item in select_items:
|
|
1065
|
-
# we don't know the grain of an aggregate at assignment time
|
|
1066
|
-
# so rebuild at this point in the tree
|
|
1067
|
-
# TODO: simplify
|
|
1068
|
-
if isinstance(item.content, ConceptTransform):
|
|
1069
|
-
new_concept = item.content.output.with_select_context(
|
|
1070
|
-
output.local_concepts,
|
|
1071
|
-
output.grain,
|
|
1072
|
-
environment=self.environment,
|
|
1073
|
-
)
|
|
1074
|
-
output.local_concepts[new_concept.address] = new_concept
|
|
1075
|
-
item.content.output = new_concept
|
|
1076
|
-
if parse_pass == 2 and CONFIG.select_as_definition:
|
|
1077
|
-
self.environment.add_concept(new_concept)
|
|
1078
|
-
elif isinstance(item.content, UndefinedConcept):
|
|
1079
|
-
self.environment.concepts.raise_undefined(
|
|
1080
|
-
item.content.address,
|
|
1081
|
-
line_no=item.content.metadata.line_number,
|
|
1082
|
-
file=self.token_address,
|
|
1083
|
-
)
|
|
1084
|
-
elif isinstance(item.content, Concept):
|
|
1085
|
-
# Sometimes cached values here don't have the latest info
|
|
1086
|
-
# but we can't just use environment, as it might not have the right grain.
|
|
1087
|
-
item.content = item.content.with_select_context(
|
|
1088
|
-
output.local_concepts,
|
|
1089
|
-
output.grain,
|
|
1090
|
-
environment=self.environment,
|
|
1091
|
-
)
|
|
1092
|
-
output.local_concepts[item.content.address] = item.content
|
|
1093
|
-
|
|
1094
|
-
if order_by:
|
|
1095
|
-
output.order_by = order_by.with_select_context(
|
|
1096
|
-
local_concepts=output.local_concepts,
|
|
1097
|
-
grain=output.grain,
|
|
1098
|
-
environment=self.environment,
|
|
1099
|
-
)
|
|
1100
|
-
if output.having_clause:
|
|
1101
|
-
output.having_clause = output.having_clause.with_select_context(
|
|
1102
|
-
local_concepts=output.local_concepts,
|
|
1103
|
-
grain=output.grain,
|
|
1104
|
-
environment=self.environment,
|
|
1105
|
-
)
|
|
1106
|
-
output.validate_syntax(self.environment)
|
|
1107
|
-
return output
|
|
1108
1040
|
|
|
1109
1041
|
@v_args(meta=True)
|
|
1110
1042
|
def address(self, meta: Meta, args):
|
|
@@ -18,6 +18,7 @@ from trilogy.core.models import (
|
|
|
18
18
|
Concept,
|
|
19
19
|
ConceptDeclarationStatement,
|
|
20
20
|
ConceptDerivation,
|
|
21
|
+
ConceptRef,
|
|
21
22
|
ConceptTransform,
|
|
22
23
|
Conditional,
|
|
23
24
|
CopyStatement,
|
|
@@ -256,8 +257,18 @@ class Renderer:
|
|
|
256
257
|
namespace = ""
|
|
257
258
|
if not concept.lineage:
|
|
258
259
|
if concept.purpose == Purpose.PROPERTY and concept.keys:
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
if len(concept.keys) == 1:
|
|
261
|
+
output = f"{concept.purpose.value} {self.to_string(ConceptRef(address=list(concept.keys)[0]))}.{namespace}{concept.name} {self.to_string(concept.datatype)};"
|
|
262
|
+
else:
|
|
263
|
+
keys = ",".join(
|
|
264
|
+
sorted(
|
|
265
|
+
list(
|
|
266
|
+
self.to_string(ConceptRef(address=x))
|
|
267
|
+
for x in concept.keys
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
|
|
261
272
|
else:
|
|
262
273
|
output = f"{concept.purpose.value} {namespace}{concept.name} {self.to_string(concept.datatype)};"
|
|
263
274
|
else:
|
|
@@ -377,8 +388,16 @@ class Renderer:
|
|
|
377
388
|
|
|
378
389
|
@to_string.register
|
|
379
390
|
def _(self, arg: "FilterItem"):
|
|
391
|
+
|
|
380
392
|
return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
|
|
381
393
|
|
|
394
|
+
@to_string.register
|
|
395
|
+
def _(self, arg: "ConceptRef"):
|
|
396
|
+
ns, base = arg.address.rsplit(".", 1)
|
|
397
|
+
if ns == DEFAULT_NAMESPACE:
|
|
398
|
+
return base
|
|
399
|
+
return arg.address
|
|
400
|
+
|
|
382
401
|
@to_string.register
|
|
383
402
|
def _(self, arg: "ImportStatement"):
|
|
384
403
|
path: str = str(arg.path).replace("\\", ".")
|
|
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
|
|
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.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/__init__.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/basic_node.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/group_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/group_to_node.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/node_merge_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/rowset_node.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/union_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/unnest_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.2.54 → pytrilogy-0.0.2.56}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|