pytrilogy 0.0.3.70__tar.gz → 0.0.3.71__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.3.70/pytrilogy.egg-info → pytrilogy-0.0.3.71}/PKG-INFO +1 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71/pytrilogy.egg-info}/PKG-INFO +1 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/SOURCES.txt +1 -0
- pytrilogy-0.0.3.71/tests/test_execute_models.py +61 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/graph_models.py +6 -2
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/datasource.py +5 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/execute.py +38 -39
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/concept_strategies_v3.py +6 -4
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/node_merge_node.py +3 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/rowset_node.py +0 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +69 -20
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_merge_node.py +70 -33
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/base_node.py +16 -12
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/union_node.py +9 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/query_processor.py +5 -3
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/base.py +4 -1
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/LICENSE.md +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/README.md +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/pyproject.toml +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/setup.cfg +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/setup.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_enums.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_executor.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_failure.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_models.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_parse_engine.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_parsing.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_parsing_failures.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_query_render.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_select.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_show.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_typing.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_user_functions.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/authoring/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/constants.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/enums.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/environment_helpers.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/functions.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/author.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/build.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/build_environment.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/core.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/models/environment.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/inline_datasource.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_loop.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_node_factory.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_utility.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_validation.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/common.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/filter_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/union_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/merge_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/recursive_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/utility.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/statements/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/statements/author.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/statements/build.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/statements/common.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/statements/execute.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/utility.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/common.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/dataframe.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/duckdb.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/presto.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/executor.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/common.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/parse_engine.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/render.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/parsing/trilogy.lark +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/render.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/__init__.py +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/date.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/display.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/geography.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/money.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/net.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/ranking.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/std/report.preql +0 -0
- {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/utility.py +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from pytest import raises
|
|
2
|
+
|
|
3
|
+
from trilogy.core.exceptions import InvalidSyntaxException
|
|
4
|
+
from trilogy.core.models.execute import (
|
|
5
|
+
BuildConcept,
|
|
6
|
+
BuildDatasource,
|
|
7
|
+
BuildGrain,
|
|
8
|
+
DataType,
|
|
9
|
+
Purpose,
|
|
10
|
+
raise_helpful_join_validation_error,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_raise_helpful_join_validation_error():
|
|
15
|
+
|
|
16
|
+
with raises(InvalidSyntaxException):
|
|
17
|
+
raise_helpful_join_validation_error(
|
|
18
|
+
concepts=[
|
|
19
|
+
BuildConcept(
|
|
20
|
+
name="test_concept",
|
|
21
|
+
datatype=DataType.INTEGER,
|
|
22
|
+
purpose=Purpose.KEY,
|
|
23
|
+
build_is_aggregate=False,
|
|
24
|
+
grain=BuildGrain(),
|
|
25
|
+
)
|
|
26
|
+
],
|
|
27
|
+
left_datasource=BuildDatasource(name="left_ds", columns=[], address="agsg"),
|
|
28
|
+
right_datasource=BuildDatasource(
|
|
29
|
+
name="right_ds", columns=[], address="agsg"
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
with raises(InvalidSyntaxException):
|
|
33
|
+
raise_helpful_join_validation_error(
|
|
34
|
+
concepts=[
|
|
35
|
+
BuildConcept(
|
|
36
|
+
name="test_concept",
|
|
37
|
+
datatype=DataType.INTEGER,
|
|
38
|
+
purpose=Purpose.KEY,
|
|
39
|
+
build_is_aggregate=False,
|
|
40
|
+
grain=BuildGrain(),
|
|
41
|
+
)
|
|
42
|
+
],
|
|
43
|
+
left_datasource=None,
|
|
44
|
+
right_datasource=BuildDatasource(
|
|
45
|
+
name="right_ds", columns=[], address="agsg"
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
with raises(InvalidSyntaxException):
|
|
49
|
+
raise_helpful_join_validation_error(
|
|
50
|
+
concepts=[
|
|
51
|
+
BuildConcept(
|
|
52
|
+
name="test_concept",
|
|
53
|
+
datatype=DataType.INTEGER,
|
|
54
|
+
purpose=Purpose.KEY,
|
|
55
|
+
build_is_aggregate=False,
|
|
56
|
+
grain=BuildGrain(),
|
|
57
|
+
)
|
|
58
|
+
],
|
|
59
|
+
left_datasource=BuildDatasource(name="left_ds", columns=[], address="agsg"),
|
|
60
|
+
right_datasource=None,
|
|
61
|
+
)
|
|
@@ -4,7 +4,7 @@ from trilogy.core.models.build import BuildConcept, BuildDatasource, BuildWhereC
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def get_graph_exact_match(
|
|
7
|
-
g: nx.DiGraph, conditions: BuildWhereClause | None
|
|
7
|
+
g: nx.DiGraph, accept_partial: bool, conditions: BuildWhereClause | None
|
|
8
8
|
) -> set[str]:
|
|
9
9
|
datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
|
|
10
10
|
nx.get_node_attributes(g, "datasource")
|
|
@@ -20,6 +20,9 @@ def get_graph_exact_match(
|
|
|
20
20
|
if not conditions and not ds.non_partial_for:
|
|
21
21
|
exact.add(node)
|
|
22
22
|
continue
|
|
23
|
+
elif not conditions and accept_partial and ds.non_partial_for:
|
|
24
|
+
exact.add(node)
|
|
25
|
+
continue
|
|
23
26
|
elif conditions:
|
|
24
27
|
if not ds.non_partial_for:
|
|
25
28
|
continue
|
|
@@ -34,10 +37,11 @@ def get_graph_exact_match(
|
|
|
34
37
|
|
|
35
38
|
def prune_sources_for_conditions(
|
|
36
39
|
g: nx.DiGraph,
|
|
40
|
+
accept_partial: bool,
|
|
37
41
|
conditions: BuildWhereClause | None,
|
|
38
42
|
):
|
|
39
43
|
|
|
40
|
-
complete = get_graph_exact_match(g, conditions)
|
|
44
|
+
complete = get_graph_exact_match(g, accept_partial, conditions)
|
|
41
45
|
to_remove = []
|
|
42
46
|
for node in g.nodes:
|
|
43
47
|
if node.startswith("ds~") and node not in complete:
|
|
@@ -249,6 +249,11 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
249
249
|
address=self.address,
|
|
250
250
|
columns=[c.with_namespace(namespace) for c in self.columns],
|
|
251
251
|
where=self.where.with_namespace(namespace) if self.where else None,
|
|
252
|
+
non_partial_for=(
|
|
253
|
+
self.non_partial_for.with_namespace(namespace)
|
|
254
|
+
if self.non_partial_for
|
|
255
|
+
else None
|
|
256
|
+
),
|
|
252
257
|
)
|
|
253
258
|
return new
|
|
254
259
|
|
|
@@ -24,12 +24,12 @@ from trilogy.core.enums import (
|
|
|
24
24
|
ComparisonOperator,
|
|
25
25
|
Derivation,
|
|
26
26
|
FunctionType,
|
|
27
|
-
Granularity,
|
|
28
27
|
JoinType,
|
|
29
28
|
Modifier,
|
|
30
29
|
Purpose,
|
|
31
30
|
SourceType,
|
|
32
31
|
)
|
|
32
|
+
from trilogy.core.exceptions import InvalidSyntaxException
|
|
33
33
|
from trilogy.core.models.build import (
|
|
34
34
|
BuildCaseElse,
|
|
35
35
|
BuildCaseWhen,
|
|
@@ -438,7 +438,7 @@ class ConceptPair(BaseModel):
|
|
|
438
438
|
|
|
439
439
|
|
|
440
440
|
class CTEConceptPair(ConceptPair):
|
|
441
|
-
cte: CTE
|
|
441
|
+
cte: CTE | UnionCTE
|
|
442
442
|
|
|
443
443
|
|
|
444
444
|
class InstantiatedUnnestJoin(BaseModel):
|
|
@@ -460,6 +460,30 @@ class UnnestJoin(BaseModel):
|
|
|
460
460
|
return self.alias + "".join([str(s.address) for s in self.concepts])
|
|
461
461
|
|
|
462
462
|
|
|
463
|
+
def raise_helpful_join_validation_error(
|
|
464
|
+
concepts: List[BuildConcept],
|
|
465
|
+
left_datasource: BuildDatasource | QueryDatasource | None,
|
|
466
|
+
right_datasource: BuildDatasource | QueryDatasource | None,
|
|
467
|
+
):
|
|
468
|
+
|
|
469
|
+
if not left_datasource or not right_datasource:
|
|
470
|
+
raise InvalidSyntaxException(
|
|
471
|
+
"No mutual keys found, and not two valid datasources"
|
|
472
|
+
)
|
|
473
|
+
left_keys = [c.address for c in left_datasource.output_concepts]
|
|
474
|
+
right_keys = [c.address for c in right_datasource.output_concepts]
|
|
475
|
+
match_concepts = [c.address for c in concepts]
|
|
476
|
+
assert left_datasource
|
|
477
|
+
assert right_datasource
|
|
478
|
+
raise InvalidSyntaxException(
|
|
479
|
+
"No mutual join keys found between"
|
|
480
|
+
f" {left_datasource.identifier} and"
|
|
481
|
+
f" {right_datasource.identifier}, left_keys {left_keys},"
|
|
482
|
+
f" right_keys {right_keys},"
|
|
483
|
+
f" provided join concepts {match_concepts}"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
463
487
|
class BaseJoin(BaseModel):
|
|
464
488
|
right_datasource: Union[BuildDatasource, "QueryDatasource"]
|
|
465
489
|
join_type: JoinType
|
|
@@ -479,27 +503,24 @@ class BaseJoin(BaseModel):
|
|
|
479
503
|
)
|
|
480
504
|
|
|
481
505
|
# Early returns maintained as in original code
|
|
482
|
-
if self.concept_pairs:
|
|
483
|
-
return self
|
|
484
|
-
|
|
485
|
-
if self.concepts == []:
|
|
506
|
+
if self.concept_pairs or self.concepts == []:
|
|
486
507
|
return self
|
|
487
508
|
|
|
488
|
-
#
|
|
509
|
+
# reduce concept list to just the mutual keys
|
|
489
510
|
final_concepts = []
|
|
490
|
-
assert self.left_datasource and self.right_datasource
|
|
491
|
-
|
|
492
511
|
for concept in self.concepts or []:
|
|
493
512
|
include = True
|
|
494
513
|
for ds in [self.left_datasource, self.right_datasource]:
|
|
495
514
|
synonyms = []
|
|
515
|
+
if not ds:
|
|
516
|
+
continue
|
|
496
517
|
for c in ds.output_concepts:
|
|
497
518
|
synonyms += list(c.pseudonyms)
|
|
498
519
|
if (
|
|
499
|
-
concept.address not in
|
|
520
|
+
concept.address not in ds.output_concepts
|
|
500
521
|
and concept.address not in synonyms
|
|
501
522
|
):
|
|
502
|
-
raise
|
|
523
|
+
raise InvalidSyntaxException(
|
|
503
524
|
f"Invalid join, missing {concept} on {ds.name}, have"
|
|
504
525
|
f" {[c.address for c in ds.output_concepts]}"
|
|
505
526
|
)
|
|
@@ -507,32 +528,10 @@ class BaseJoin(BaseModel):
|
|
|
507
528
|
final_concepts.append(concept)
|
|
508
529
|
|
|
509
530
|
if not final_concepts and self.concepts:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if all(
|
|
515
|
-
[
|
|
516
|
-
c.granularity == Granularity.SINGLE_ROW
|
|
517
|
-
for c in ds.output_concepts
|
|
518
|
-
]
|
|
519
|
-
):
|
|
520
|
-
self.concepts = []
|
|
521
|
-
return self
|
|
522
|
-
# if everything is at abstract grain, we can skip joins
|
|
523
|
-
if all([c.grain.abstract for c in ds.output_concepts]):
|
|
524
|
-
self.concepts = []
|
|
525
|
-
return self
|
|
526
|
-
|
|
527
|
-
left_keys = [c.address for c in self.left_datasource.output_concepts]
|
|
528
|
-
right_keys = [c.address for c in self.right_datasource.output_concepts]
|
|
529
|
-
match_concepts = [c.address for c in self.concepts]
|
|
530
|
-
raise SyntaxError(
|
|
531
|
-
"No mutual join keys found between"
|
|
532
|
-
f" {self.left_datasource.identifier} and"
|
|
533
|
-
f" {self.right_datasource.identifier}, left_keys {left_keys},"
|
|
534
|
-
f" right_keys {right_keys},"
|
|
535
|
-
f" provided join concepts {match_concepts}"
|
|
531
|
+
raise_helpful_join_validation_error(
|
|
532
|
+
self.concepts,
|
|
533
|
+
self.left_datasource,
|
|
534
|
+
self.right_datasource,
|
|
536
535
|
)
|
|
537
536
|
|
|
538
537
|
self.concepts = final_concepts
|
|
@@ -1087,7 +1086,7 @@ class UnionCTE(BaseModel):
|
|
|
1087
1086
|
class Join(BaseModel):
|
|
1088
1087
|
right_cte: CTE | UnionCTE
|
|
1089
1088
|
jointype: JoinType
|
|
1090
|
-
left_cte: CTE | None = None
|
|
1089
|
+
left_cte: CTE | UnionCTE | None = None
|
|
1091
1090
|
joinkey_pairs: List[CTEConceptPair] | None = None
|
|
1092
1091
|
inlined_ctes: set[str] = Field(default_factory=set)
|
|
1093
1092
|
quote: str | None = None
|
|
@@ -1096,7 +1095,7 @@ class Join(BaseModel):
|
|
|
1096
1095
|
def inline_cte(self, cte: CTE):
|
|
1097
1096
|
self.inlined_ctes.add(cte.name)
|
|
1098
1097
|
|
|
1099
|
-
def get_name(self, cte: CTE):
|
|
1098
|
+
def get_name(self, cte: CTE | UnionCTE) -> str:
|
|
1100
1099
|
if cte.identifier in self.inlined_ctes:
|
|
1101
1100
|
return cte.source.datasources[0].safe_identifier
|
|
1102
1101
|
return cte.safe_identifier
|
|
@@ -291,7 +291,7 @@ def evaluate_loop_conditions(
|
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
def check_for_early_exit(
|
|
294
|
-
complete, partial, context: LoopContext, priority_concept: BuildConcept
|
|
294
|
+
complete, partial, missing, context: LoopContext, priority_concept: BuildConcept
|
|
295
295
|
) -> bool:
|
|
296
296
|
if complete == ValidationResult.INCOMPLETE_CONDITION:
|
|
297
297
|
cond_dict = {str(node): node.preexisting_conditions for node in context.stack}
|
|
@@ -321,7 +321,7 @@ def check_for_early_exit(
|
|
|
321
321
|
)
|
|
322
322
|
else:
|
|
323
323
|
logger.info(
|
|
324
|
-
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Not complete, continuing search"
|
|
324
|
+
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Not complete (missing {missing}), continuing search"
|
|
325
325
|
)
|
|
326
326
|
# if we have attempted on root node, we've tried them all.
|
|
327
327
|
# inject in another search with filter concepts
|
|
@@ -412,7 +412,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
|
|
|
412
412
|
elif context.conditions:
|
|
413
413
|
output.preexisting_conditions = context.conditions.conditional
|
|
414
414
|
logger.info(
|
|
415
|
-
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node output {[x.address for x in output.usable_outputs]} partial {[c.address for c in output.partial_concepts]} with {context.conditions}"
|
|
415
|
+
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node output {[x.address for x in output.usable_outputs]} partial {[c.address for c in output.partial_concepts or []]} with {context.conditions}"
|
|
416
416
|
)
|
|
417
417
|
if condition_required and context.conditions and non_virtual_different:
|
|
418
418
|
logger.info(
|
|
@@ -532,7 +532,9 @@ def _search_concepts(
|
|
|
532
532
|
)
|
|
533
533
|
# assign
|
|
534
534
|
context.found = found_c
|
|
535
|
-
early_exit = check_for_early_exit(
|
|
535
|
+
early_exit = check_for_early_exit(
|
|
536
|
+
complete, partial, missing_c, context, priority_concept
|
|
537
|
+
)
|
|
536
538
|
if early_exit:
|
|
537
539
|
break
|
|
538
540
|
|
{pytrilogy-0.0.3.70 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/node_merge_node.py
RENAMED
|
@@ -253,7 +253,9 @@ def resolve_weak_components(
|
|
|
253
253
|
break_flag = False
|
|
254
254
|
found = []
|
|
255
255
|
search_graph = environment_graph.copy()
|
|
256
|
-
prune_sources_for_conditions(
|
|
256
|
+
prune_sources_for_conditions(
|
|
257
|
+
search_graph, accept_partial, conditions=search_conditions
|
|
258
|
+
)
|
|
257
259
|
reduced_concept_sets: list[set[str]] = []
|
|
258
260
|
|
|
259
261
|
# loop through, removing new nodes we find
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from collections import defaultdict
|
|
2
3
|
from datetime import date, datetime, timedelta
|
|
3
4
|
from typing import List, Tuple, TypeVar
|
|
@@ -14,7 +15,7 @@ from trilogy.core.models.build import (
|
|
|
14
15
|
from trilogy.core.models.core import DataType
|
|
15
16
|
|
|
16
17
|
# Define a generic type that ensures start and end are the same type
|
|
17
|
-
T = TypeVar("T", int, date, datetime)
|
|
18
|
+
T = TypeVar("T", int, float, date, datetime)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def reduce_expression(
|
|
@@ -37,21 +38,26 @@ def reduce_expression(
|
|
|
37
38
|
elif var.datatype == DataType.DATETIME:
|
|
38
39
|
lower_check = datetime.min # type: ignore
|
|
39
40
|
upper_check = datetime.max # type: ignore
|
|
41
|
+
elif var.datatype == DataType.BOOL:
|
|
42
|
+
lower_check = False # type: ignore
|
|
43
|
+
upper_check = True # type: ignore
|
|
44
|
+
elif var.datatype == DataType.FLOAT:
|
|
45
|
+
lower_check = float("-inf") # type: ignore
|
|
46
|
+
upper_check = float("inf") # type: ignore
|
|
40
47
|
else:
|
|
41
|
-
|
|
48
|
+
return False
|
|
42
49
|
|
|
43
50
|
ranges: list[Tuple[T, T]] = []
|
|
44
51
|
for op, value in group_tuple:
|
|
45
|
-
increment: int | timedelta
|
|
52
|
+
increment: int | timedelta | float
|
|
46
53
|
if isinstance(value, date):
|
|
47
54
|
increment = timedelta(days=1)
|
|
48
55
|
elif isinstance(value, datetime):
|
|
49
56
|
increment = timedelta(seconds=1)
|
|
50
57
|
elif isinstance(value, int):
|
|
51
58
|
increment = 1
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# increment = Decimal(0.0000000001)
|
|
59
|
+
elif isinstance(value, float):
|
|
60
|
+
increment = sys.float_info.epsilon
|
|
55
61
|
|
|
56
62
|
if op == ">":
|
|
57
63
|
ranges.append(
|
|
@@ -88,24 +94,43 @@ def reduce_expression(
|
|
|
88
94
|
value,
|
|
89
95
|
)
|
|
90
96
|
)
|
|
97
|
+
elif op == ComparisonOperator.IS:
|
|
98
|
+
ranges.append(
|
|
99
|
+
(
|
|
100
|
+
value,
|
|
101
|
+
value,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
elif op == ComparisonOperator.NE:
|
|
105
|
+
pass
|
|
91
106
|
else:
|
|
92
|
-
|
|
107
|
+
return False
|
|
93
108
|
return is_fully_covered(lower_check, upper_check, ranges, increment)
|
|
94
109
|
|
|
95
110
|
|
|
111
|
+
TARGET_TYPES = (
|
|
112
|
+
int,
|
|
113
|
+
date,
|
|
114
|
+
float,
|
|
115
|
+
datetime,
|
|
116
|
+
bool,
|
|
117
|
+
)
|
|
118
|
+
REDUCABLE_TYPES = (int, float, date, bool, datetime, BuildFunction)
|
|
119
|
+
|
|
120
|
+
|
|
96
121
|
def simplify_conditions(
|
|
97
122
|
conditions: list[BuildComparison | BuildConditional | BuildParenthetical],
|
|
98
123
|
) -> bool:
|
|
99
124
|
# Group conditions by variable
|
|
100
125
|
grouped: dict[
|
|
101
|
-
BuildConcept, list[tuple[ComparisonOperator, datetime | int | date]]
|
|
126
|
+
BuildConcept, list[tuple[ComparisonOperator, datetime | int | date | float]]
|
|
102
127
|
] = defaultdict(list)
|
|
103
128
|
for condition in conditions:
|
|
104
129
|
if not isinstance(condition, BuildComparison):
|
|
105
130
|
return False
|
|
106
|
-
if not isinstance(
|
|
107
|
-
condition.
|
|
108
|
-
)
|
|
131
|
+
if not isinstance(condition.left, REDUCABLE_TYPES) and not isinstance(
|
|
132
|
+
condition.right, REDUCABLE_TYPES
|
|
133
|
+
):
|
|
109
134
|
return False
|
|
110
135
|
if not isinstance(condition.left, BuildConcept) and not isinstance(
|
|
111
136
|
condition.right, BuildConcept
|
|
@@ -113,15 +138,20 @@ def simplify_conditions(
|
|
|
113
138
|
return False
|
|
114
139
|
vars = [condition.left, condition.right]
|
|
115
140
|
concept = [x for x in vars if isinstance(x, BuildConcept)][0]
|
|
116
|
-
|
|
117
|
-
if isinstance(
|
|
118
|
-
if not
|
|
141
|
+
raw_comparison = [x for x in vars if not isinstance(x, BuildConcept)][0]
|
|
142
|
+
if isinstance(raw_comparison, BuildFunction):
|
|
143
|
+
if not raw_comparison.operator == FunctionType.CONSTANT:
|
|
119
144
|
return False
|
|
120
|
-
first_arg =
|
|
121
|
-
if not isinstance(first_arg,
|
|
145
|
+
first_arg = raw_comparison.arguments[0]
|
|
146
|
+
if not isinstance(first_arg, TARGET_TYPES):
|
|
122
147
|
return False
|
|
123
148
|
comparison = first_arg
|
|
124
|
-
|
|
149
|
+
else:
|
|
150
|
+
if not isinstance(raw_comparison, TARGET_TYPES):
|
|
151
|
+
return False
|
|
152
|
+
comparison = raw_comparison
|
|
153
|
+
|
|
154
|
+
if not isinstance(comparison, REDUCABLE_TYPES):
|
|
125
155
|
return False
|
|
126
156
|
|
|
127
157
|
var = concept
|
|
@@ -136,11 +166,25 @@ def simplify_conditions(
|
|
|
136
166
|
return True if all(isinstance(s, bool) and s for s in simplified) else False
|
|
137
167
|
|
|
138
168
|
|
|
169
|
+
def boolean_fully_covered(
|
|
170
|
+
start: bool,
|
|
171
|
+
end: bool,
|
|
172
|
+
ranges: List[Tuple[bool, bool]],
|
|
173
|
+
):
|
|
174
|
+
all = []
|
|
175
|
+
for r_start, r_end in ranges:
|
|
176
|
+
if r_start is True and r_end is True:
|
|
177
|
+
all.append(True)
|
|
178
|
+
elif r_start is False and r_end is False:
|
|
179
|
+
all.append(False)
|
|
180
|
+
return set(all) == {False, True}
|
|
181
|
+
|
|
182
|
+
|
|
139
183
|
def is_fully_covered(
|
|
140
184
|
start: T,
|
|
141
185
|
end: T,
|
|
142
186
|
ranges: List[Tuple[T, T]],
|
|
143
|
-
increment: int | timedelta,
|
|
187
|
+
increment: int | timedelta | float,
|
|
144
188
|
):
|
|
145
189
|
"""
|
|
146
190
|
Check if the list of range pairs fully covers the set [start, end].
|
|
@@ -153,6 +197,11 @@ def is_fully_covered(
|
|
|
153
197
|
Returns:
|
|
154
198
|
- bool: True if the ranges fully cover [start, end], False otherwise.
|
|
155
199
|
"""
|
|
200
|
+
if isinstance(start, bool) and isinstance(end, bool):
|
|
201
|
+
# convert each element of each tuple to a boolean
|
|
202
|
+
bool_ranges = [(bool(r_start), bool(r_end)) for r_start, r_end in ranges]
|
|
203
|
+
|
|
204
|
+
return boolean_fully_covered(start, end, bool_ranges)
|
|
156
205
|
# Sort ranges by their start values (and by end values for ties)
|
|
157
206
|
ranges.sort()
|
|
158
207
|
|
|
@@ -173,14 +222,14 @@ def get_union_sources(
|
|
|
173
222
|
datasources: list[BuildDatasource], concepts: list[BuildConcept]
|
|
174
223
|
) -> List[list[BuildDatasource]]:
|
|
175
224
|
candidates: list[BuildDatasource] = []
|
|
225
|
+
|
|
176
226
|
for x in datasources:
|
|
177
|
-
if
|
|
227
|
+
if any([c.address in x.output_concepts for c in concepts]):
|
|
178
228
|
if (
|
|
179
229
|
any([c.address in x.partial_concepts for c in concepts])
|
|
180
230
|
and x.non_partial_for
|
|
181
231
|
):
|
|
182
232
|
candidates.append(x)
|
|
183
|
-
|
|
184
233
|
assocs: dict[str, list[BuildDatasource]] = defaultdict(list[BuildDatasource])
|
|
185
234
|
for x in candidates:
|
|
186
235
|
if not x.non_partial_for:
|