pytrilogy 0.3.149__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
- pytrilogy-0.3.149.dist-info/METADATA +555 -0
- pytrilogy-0.3.149.dist-info/RECORD +207 -0
- pytrilogy-0.3.149.dist-info/WHEEL +4 -0
- pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.149.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +27 -0
- trilogy/ai/README.md +10 -0
- trilogy/ai/__init__.py +19 -0
- trilogy/ai/constants.py +92 -0
- trilogy/ai/conversation.py +107 -0
- trilogy/ai/enums.py +7 -0
- trilogy/ai/execute.py +50 -0
- trilogy/ai/models.py +34 -0
- trilogy/ai/prompts.py +100 -0
- trilogy/ai/providers/__init__.py +0 -0
- trilogy/ai/providers/anthropic.py +106 -0
- trilogy/ai/providers/base.py +24 -0
- trilogy/ai/providers/google.py +146 -0
- trilogy/ai/providers/openai.py +89 -0
- trilogy/ai/providers/utils.py +68 -0
- trilogy/authoring/README.md +3 -0
- trilogy/authoring/__init__.py +148 -0
- trilogy/constants.py +119 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +454 -0
- trilogy/core/env_processor.py +239 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1240 -0
- trilogy/core/graph_models.py +142 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2670 -0
- trilogy/core/models/build.py +2603 -0
- trilogy/core/models/build_environment.py +165 -0
- trilogy/core/models/core.py +506 -0
- trilogy/core/models/datasource.py +436 -0
- trilogy/core/models/environment.py +756 -0
- trilogy/core/models/execute.py +1213 -0
- trilogy/core/optimization.py +251 -0
- trilogy/core/optimizations/__init__.py +12 -0
- trilogy/core/optimizations/base_optimization.py +17 -0
- trilogy/core/optimizations/hide_unused_concept.py +47 -0
- trilogy/core/optimizations/inline_datasource.py +102 -0
- trilogy/core/optimizations/predicate_pushdown.py +245 -0
- trilogy/core/processing/README.md +94 -0
- trilogy/core/processing/READMEv2.md +121 -0
- trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +508 -0
- trilogy/core/processing/constants.py +15 -0
- trilogy/core/processing/discovery_node_factory.py +451 -0
- trilogy/core/processing/discovery_utility.py +548 -0
- trilogy/core/processing/discovery_validation.py +167 -0
- trilogy/core/processing/graph_utils.py +43 -0
- trilogy/core/processing/node_generators/README.md +9 -0
- trilogy/core/processing/node_generators/__init__.py +31 -0
- trilogy/core/processing/node_generators/basic_node.py +160 -0
- trilogy/core/processing/node_generators/common.py +270 -0
- trilogy/core/processing/node_generators/constant_node.py +38 -0
- trilogy/core/processing/node_generators/filter_node.py +315 -0
- trilogy/core/processing/node_generators/group_node.py +213 -0
- trilogy/core/processing/node_generators/group_to_node.py +117 -0
- trilogy/core/processing/node_generators/multiselect_node.py +207 -0
- trilogy/core/processing/node_generators/node_merge_node.py +695 -0
- trilogy/core/processing/node_generators/recursive_node.py +88 -0
- trilogy/core/processing/node_generators/rowset_node.py +165 -0
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
- trilogy/core/processing/node_generators/select_merge_node.py +846 -0
- trilogy/core/processing/node_generators/select_node.py +95 -0
- trilogy/core/processing/node_generators/synonym_node.py +98 -0
- trilogy/core/processing/node_generators/union_node.py +91 -0
- trilogy/core/processing/node_generators/unnest_node.py +182 -0
- trilogy/core/processing/node_generators/window_node.py +201 -0
- trilogy/core/processing/nodes/README.md +28 -0
- trilogy/core/processing/nodes/__init__.py +179 -0
- trilogy/core/processing/nodes/base_node.py +522 -0
- trilogy/core/processing/nodes/filter_node.py +75 -0
- trilogy/core/processing/nodes/group_node.py +194 -0
- trilogy/core/processing/nodes/merge_node.py +420 -0
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/processing/nodes/select_node_v2.py +242 -0
- trilogy/core/processing/nodes/union_node.py +53 -0
- trilogy/core/processing/nodes/unnest_node.py +62 -0
- trilogy/core/processing/nodes/window_node.py +56 -0
- trilogy/core/processing/utility.py +823 -0
- trilogy/core/query_processor.py +604 -0
- trilogy/core/statements/README.md +35 -0
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +536 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +20 -0
- trilogy/core/statements/execute.py +155 -0
- trilogy/core/table_processor.py +66 -0
- trilogy/core/utility.py +8 -0
- trilogy/core/validation/README.md +46 -0
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +161 -0
- trilogy/core/validation/concept.py +146 -0
- trilogy/core/validation/datasource.py +227 -0
- trilogy/core/validation/environment.py +73 -0
- trilogy/core/validation/fix.py +256 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1432 -0
- trilogy/dialect/bigquery.py +314 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +159 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +397 -0
- trilogy/dialect/enums.py +151 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +117 -0
- trilogy/dialect/presto.py +110 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +129 -0
- trilogy/dialect/sql_server.py +137 -0
- trilogy/engine.py +48 -0
- trilogy/execution/__init__.py +17 -0
- trilogy/execution/config.py +119 -0
- trilogy/execution/state/__init__.py +0 -0
- trilogy/execution/state/exceptions.py +26 -0
- trilogy/execution/state/file_state_store.py +0 -0
- trilogy/execution/state/sqllite_state_store.py +0 -0
- trilogy/execution/state/state_store.py +406 -0
- trilogy/executor.py +692 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +135 -0
- trilogy/hooks/query_debugger.py +166 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/README.md +21 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +1069 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +8 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +2876 -0
- trilogy/parsing/render.py +775 -0
- trilogy/parsing/trilogy.lark +546 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +45 -0
- trilogy/scripts/README.md +9 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/agent.py +41 -0
- trilogy/scripts/agent_info.py +306 -0
- trilogy/scripts/common.py +432 -0
- trilogy/scripts/dependency/Cargo.lock +617 -0
- trilogy/scripts/dependency/Cargo.toml +39 -0
- trilogy/scripts/dependency/README.md +131 -0
- trilogy/scripts/dependency/build.sh +25 -0
- trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
- trilogy/scripts/dependency/src/lib.rs +16 -0
- trilogy/scripts/dependency/src/main.rs +770 -0
- trilogy/scripts/dependency/src/parser.rs +435 -0
- trilogy/scripts/dependency/src/preql.pest +208 -0
- trilogy/scripts/dependency/src/python_bindings.rs +311 -0
- trilogy/scripts/dependency/src/resolver.rs +716 -0
- trilogy/scripts/dependency/tests/base.preql +3 -0
- trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
- trilogy/scripts/dependency/tests/customer.preql +6 -0
- trilogy/scripts/dependency/tests/main.preql +9 -0
- trilogy/scripts/dependency/tests/orders.preql +7 -0
- trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
- trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
- trilogy/scripts/dependency.py +323 -0
- trilogy/scripts/display.py +555 -0
- trilogy/scripts/environment.py +59 -0
- trilogy/scripts/fmt.py +32 -0
- trilogy/scripts/ingest.py +487 -0
- trilogy/scripts/ingest_helpers/__init__.py +1 -0
- trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
- trilogy/scripts/ingest_helpers/formatting.py +93 -0
- trilogy/scripts/ingest_helpers/typing.py +161 -0
- trilogy/scripts/init.py +105 -0
- trilogy/scripts/parallel_execution.py +762 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/refresh.py +161 -0
- trilogy/scripts/run.py +79 -0
- trilogy/scripts/serve.py +202 -0
- trilogy/scripts/serve_helpers/__init__.py +41 -0
- trilogy/scripts/serve_helpers/file_discovery.py +142 -0
- trilogy/scripts/serve_helpers/index_generation.py +206 -0
- trilogy/scripts/serve_helpers/models.py +38 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/testing.py +143 -0
- trilogy/scripts/trilogy.py +75 -0
- trilogy/std/__init__.py +0 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/date.preql +13 -0
- trilogy/std/display.preql +18 -0
- trilogy/std/geography.preql +22 -0
- trilogy/std/metric.preql +15 -0
- trilogy/std/money.preql +67 -0
- trilogy/std/net.preql +14 -0
- trilogy/std/ranking.preql +7 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/semantic.preql +6 -0
- trilogy/utility.py +34 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from trilogy.constants import logger
|
|
2
|
+
from trilogy.core.enums import Derivation
|
|
3
|
+
from trilogy.core.exceptions import NoDatasourceException
|
|
4
|
+
from trilogy.core.models.build import (
|
|
5
|
+
BuildConcept,
|
|
6
|
+
BuildWhereClause,
|
|
7
|
+
CanonicalBuildConceptList,
|
|
8
|
+
)
|
|
9
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
10
|
+
from trilogy.core.processing.node_generators.select_merge_node import (
|
|
11
|
+
gen_select_merge_node,
|
|
12
|
+
)
|
|
13
|
+
from trilogy.core.processing.nodes import (
|
|
14
|
+
StrategyNode,
|
|
15
|
+
)
|
|
16
|
+
from trilogy.core.processing.utility import padding
|
|
17
|
+
|
|
18
|
+
LOGGER_PREFIX = "[GEN_SELECT_NODE]"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_query_is_resolvable(
|
|
22
|
+
missing: list[str],
|
|
23
|
+
environment: BuildEnvironment,
|
|
24
|
+
materialized_lcl: CanonicalBuildConceptList,
|
|
25
|
+
) -> None:
|
|
26
|
+
# if a query cannot ever be resolved, exit early with an error
|
|
27
|
+
for x in missing:
|
|
28
|
+
if x not in environment.concepts:
|
|
29
|
+
# if it's locally derived, we can assume it can be resolved
|
|
30
|
+
continue
|
|
31
|
+
validation_concept = environment.concepts[x]
|
|
32
|
+
# if the concept we look up isn't what we searched for,
|
|
33
|
+
# we're in a pseudonym anyway, don't worry about validating
|
|
34
|
+
if validation_concept.address != x:
|
|
35
|
+
continue
|
|
36
|
+
if validation_concept.derivation == Derivation.ROOT:
|
|
37
|
+
has_source = False
|
|
38
|
+
for x in validation_concept.pseudonyms:
|
|
39
|
+
if x in environment.alias_origin_lookup:
|
|
40
|
+
pseudonym_concept = environment.alias_origin_lookup[x]
|
|
41
|
+
else:
|
|
42
|
+
pseudonym_concept = environment.concepts[x]
|
|
43
|
+
# if it's not a root concept pseudonym,
|
|
44
|
+
# assume we can derive it
|
|
45
|
+
if pseudonym_concept.derivation != Derivation.ROOT:
|
|
46
|
+
has_source = True
|
|
47
|
+
break
|
|
48
|
+
if pseudonym_concept.address in materialized_lcl:
|
|
49
|
+
has_source = True
|
|
50
|
+
break
|
|
51
|
+
if not has_source:
|
|
52
|
+
raise NoDatasourceException(
|
|
53
|
+
f"No datasource exists for root concept {validation_concept}, and no resolvable pseudonyms found from {validation_concept.pseudonyms}. This query is unresolvable from your environment. Check your datasources and imports to make sure this concept is bound."
|
|
54
|
+
)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def gen_select_node(
|
|
59
|
+
concepts: list[BuildConcept],
|
|
60
|
+
environment: BuildEnvironment,
|
|
61
|
+
g,
|
|
62
|
+
depth: int,
|
|
63
|
+
accept_partial: bool = False,
|
|
64
|
+
fail_if_not_found: bool = True,
|
|
65
|
+
conditions: BuildWhereClause | None = None,
|
|
66
|
+
) -> StrategyNode | None:
|
|
67
|
+
all_lcl = CanonicalBuildConceptList(concepts=concepts)
|
|
68
|
+
# search all concepts here, including partial
|
|
69
|
+
materialized_lcl = CanonicalBuildConceptList(
|
|
70
|
+
concepts=[
|
|
71
|
+
x
|
|
72
|
+
for x in concepts
|
|
73
|
+
if x.canonical_address in environment.materialized_canonical_concepts
|
|
74
|
+
or x.derivation == Derivation.CONSTANT
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
if materialized_lcl != all_lcl:
|
|
78
|
+
missing = all_lcl.difference(materialized_lcl)
|
|
79
|
+
logger.info(
|
|
80
|
+
f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concepts}"
|
|
81
|
+
f" as it + optional includes non-materialized concepts (looking for all {all_lcl}, missing {missing})."
|
|
82
|
+
)
|
|
83
|
+
validate_query_is_resolvable(missing, environment, materialized_lcl)
|
|
84
|
+
if fail_if_not_found:
|
|
85
|
+
raise NoDatasourceException(f"No datasource exists for {concepts}")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
return gen_select_merge_node(
|
|
89
|
+
concepts,
|
|
90
|
+
g=g,
|
|
91
|
+
environment=environment,
|
|
92
|
+
depth=depth,
|
|
93
|
+
accept_partial=accept_partial,
|
|
94
|
+
conditions=conditions,
|
|
95
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from trilogy.constants import logger
|
|
6
|
+
from trilogy.core.enums import Derivation
|
|
7
|
+
from trilogy.core.models.build import BuildConcept, BuildWhereClause
|
|
8
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
9
|
+
from trilogy.core.processing.nodes import History, StrategyNode
|
|
10
|
+
from trilogy.core.processing.utility import padding
|
|
11
|
+
|
|
12
|
+
LOGGER_PREFIX = "[GEN_SYNONYM_NODE]"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gen_synonym_node(
|
|
16
|
+
all_concepts: List[BuildConcept],
|
|
17
|
+
environment: BuildEnvironment,
|
|
18
|
+
g,
|
|
19
|
+
depth: int,
|
|
20
|
+
source_concepts,
|
|
21
|
+
history: History | None = None,
|
|
22
|
+
conditions: BuildWhereClause | None = None,
|
|
23
|
+
accept_partial: bool = False,
|
|
24
|
+
) -> StrategyNode | None:
|
|
25
|
+
local_prefix = f"{padding(depth)}[GEN_SYNONYM_NODE]"
|
|
26
|
+
base_fingerprint = tuple(sorted([x.address for x in all_concepts]))
|
|
27
|
+
synonyms = defaultdict(list)
|
|
28
|
+
has_synonyms = False
|
|
29
|
+
for x in all_concepts:
|
|
30
|
+
synonyms[x.address] = [x]
|
|
31
|
+
if x.address in environment.alias_origin_lookup:
|
|
32
|
+
parent = environment.concepts[x.address]
|
|
33
|
+
if parent.address != x.address:
|
|
34
|
+
synonyms[x.address].append(parent)
|
|
35
|
+
has_synonyms = True
|
|
36
|
+
for y in x.pseudonyms:
|
|
37
|
+
if y in environment.alias_origin_lookup:
|
|
38
|
+
synonyms[x.address].append(environment.alias_origin_lookup[y])
|
|
39
|
+
has_synonyms = True
|
|
40
|
+
elif y in environment.concepts:
|
|
41
|
+
synonyms[x.address].append(environment.concepts[y])
|
|
42
|
+
has_synonyms = True
|
|
43
|
+
for address in synonyms:
|
|
44
|
+
synonyms[address].sort(key=lambda obj: obj.address)
|
|
45
|
+
if not has_synonyms:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
logger.info(f"{local_prefix} Generating Synonym Node with {len(synonyms)} synonyms")
|
|
49
|
+
sorted_keys = sorted(synonyms.keys())
|
|
50
|
+
combinations_list: list[tuple[BuildConcept, ...]] = list(
|
|
51
|
+
itertools.product(*(synonyms[obj] for obj in sorted_keys))
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def similarity_sort_key(combo: tuple[BuildConcept, ...]):
|
|
55
|
+
addresses = [x.address for x in combo]
|
|
56
|
+
|
|
57
|
+
# Calculate similarity score - count how many pairs share prefixes
|
|
58
|
+
similarity_score = 0
|
|
59
|
+
roots = sum(
|
|
60
|
+
[1 for x in combo if x.derivation in (Derivation.ROOT, Derivation.CONSTANT)]
|
|
61
|
+
)
|
|
62
|
+
for i in range(len(addresses)):
|
|
63
|
+
for j in range(i + 1, len(addresses)):
|
|
64
|
+
# Find common prefix length
|
|
65
|
+
addr1_parts = addresses[i].split(".")
|
|
66
|
+
addr2_parts = addresses[j].split(".")
|
|
67
|
+
common_prefix_len = 0
|
|
68
|
+
for k in range(min(len(addr1_parts), len(addr2_parts))):
|
|
69
|
+
if addr1_parts[k] == addr2_parts[k]:
|
|
70
|
+
common_prefix_len += 1
|
|
71
|
+
else:
|
|
72
|
+
break
|
|
73
|
+
similarity_score += common_prefix_len
|
|
74
|
+
|
|
75
|
+
# Sort by roots, similarity (descending), then by addresses (ascending) for ties
|
|
76
|
+
return (-roots, -similarity_score, addresses)
|
|
77
|
+
|
|
78
|
+
combinations_list.sort(key=similarity_sort_key)
|
|
79
|
+
for combo in combinations_list:
|
|
80
|
+
fingerprint = tuple(sorted([x.address for x in combo]))
|
|
81
|
+
if fingerprint == base_fingerprint:
|
|
82
|
+
continue
|
|
83
|
+
logger.info(
|
|
84
|
+
f"{local_prefix} checking combination {fingerprint} with {len(combo)} concepts"
|
|
85
|
+
)
|
|
86
|
+
attempt: StrategyNode | None = source_concepts(
|
|
87
|
+
list(combo),
|
|
88
|
+
history=history,
|
|
89
|
+
environment=environment,
|
|
90
|
+
depth=depth,
|
|
91
|
+
conditions=conditions,
|
|
92
|
+
g=g,
|
|
93
|
+
accept_partial=accept_partial,
|
|
94
|
+
)
|
|
95
|
+
if attempt:
|
|
96
|
+
logger.info(f"{local_prefix} found inputs with {combo}")
|
|
97
|
+
return attempt
|
|
98
|
+
return None
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from trilogy.constants import logger
|
|
4
|
+
from trilogy.core.enums import FunctionType
|
|
5
|
+
from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
|
|
6
|
+
from trilogy.core.processing.nodes import History, StrategyNode, UnionNode
|
|
7
|
+
from trilogy.core.processing.utility import padding
|
|
8
|
+
|
|
9
|
+
LOGGER_PREFIX = "[GEN_UNION_NODE]"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_union(c: BuildConcept):
|
|
13
|
+
return (
|
|
14
|
+
isinstance(c.lineage, BuildFunction)
|
|
15
|
+
and c.lineage.operator == FunctionType.UNION
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_layers(
|
|
20
|
+
concepts: list[BuildConcept],
|
|
21
|
+
) -> tuple[list[list[BuildConcept]], list[BuildConcept]]:
|
|
22
|
+
sources = {
|
|
23
|
+
x.address: x.lineage.concept_arguments if x.lineage else [] for x in concepts
|
|
24
|
+
}
|
|
25
|
+
root = concepts[0]
|
|
26
|
+
|
|
27
|
+
built_layers = []
|
|
28
|
+
layers = root.lineage.concept_arguments if root.lineage else []
|
|
29
|
+
sourced = set()
|
|
30
|
+
while layers:
|
|
31
|
+
layer = []
|
|
32
|
+
current = layers.pop()
|
|
33
|
+
sourced.add(current.address)
|
|
34
|
+
layer.append(current)
|
|
35
|
+
for key, values in sources.items():
|
|
36
|
+
if key == current.address:
|
|
37
|
+
continue
|
|
38
|
+
for value in values:
|
|
39
|
+
if value.address in (current.keys or []) or current.address in (
|
|
40
|
+
value.keys or []
|
|
41
|
+
):
|
|
42
|
+
layer.append(value)
|
|
43
|
+
sourced.add(value.address)
|
|
44
|
+
built_layers.append(layer)
|
|
45
|
+
complete = [
|
|
46
|
+
x for x in concepts if all([x.address in sourced for x in sources[x.address]])
|
|
47
|
+
]
|
|
48
|
+
return built_layers, complete
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def gen_union_node(
|
|
52
|
+
concept: BuildConcept,
|
|
53
|
+
local_optional: List[BuildConcept],
|
|
54
|
+
environment,
|
|
55
|
+
g,
|
|
56
|
+
depth: int,
|
|
57
|
+
source_concepts,
|
|
58
|
+
history: History | None = None,
|
|
59
|
+
conditions: BuildWhereClause | None = None,
|
|
60
|
+
) -> StrategyNode | None:
|
|
61
|
+
all_unions = [x for x in local_optional if is_union(x)] + [concept]
|
|
62
|
+
logger.info(f"{padding(depth)}{LOGGER_PREFIX} found unions {all_unions}")
|
|
63
|
+
parent_nodes = []
|
|
64
|
+
layers, resolved = build_layers(all_unions)
|
|
65
|
+
for layer in layers:
|
|
66
|
+
logger.info(
|
|
67
|
+
f"{padding(depth)}{LOGGER_PREFIX} fetching layer {layer} with resolved {resolved}"
|
|
68
|
+
)
|
|
69
|
+
parent: StrategyNode = source_concepts(
|
|
70
|
+
mandatory_list=layer,
|
|
71
|
+
environment=environment,
|
|
72
|
+
g=g,
|
|
73
|
+
depth=depth + 1,
|
|
74
|
+
history=history,
|
|
75
|
+
conditions=conditions,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
parent.add_output_concepts(resolved)
|
|
79
|
+
parent_nodes.append(parent)
|
|
80
|
+
if not parent:
|
|
81
|
+
logger.info(
|
|
82
|
+
f"{padding(depth)}{LOGGER_PREFIX} could not find union node parents"
|
|
83
|
+
)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return UnionNode(
|
|
87
|
+
input_concepts=resolved,
|
|
88
|
+
output_concepts=resolved,
|
|
89
|
+
environment=environment,
|
|
90
|
+
parents=parent_nodes,
|
|
91
|
+
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from trilogy.constants import logger
|
|
4
|
+
from trilogy.core.models.build import (
|
|
5
|
+
BuildConcept,
|
|
6
|
+
BuildFunction,
|
|
7
|
+
BuildWhereClause,
|
|
8
|
+
)
|
|
9
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
10
|
+
from trilogy.core.processing.nodes import (
|
|
11
|
+
History,
|
|
12
|
+
MergeNode,
|
|
13
|
+
StrategyNode,
|
|
14
|
+
UnnestNode,
|
|
15
|
+
WhereSafetyNode,
|
|
16
|
+
)
|
|
17
|
+
from trilogy.core.processing.utility import padding
|
|
18
|
+
|
|
19
|
+
LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_pseudonym_parents(
|
|
23
|
+
concept: BuildConcept,
|
|
24
|
+
local_optional: List[BuildConcept],
|
|
25
|
+
source_concepts,
|
|
26
|
+
environment: BuildEnvironment,
|
|
27
|
+
g,
|
|
28
|
+
depth,
|
|
29
|
+
history,
|
|
30
|
+
conditions,
|
|
31
|
+
) -> List[StrategyNode]:
|
|
32
|
+
for x in concept.pseudonyms:
|
|
33
|
+
attempt = source_concepts(
|
|
34
|
+
mandatory_list=[environment.alias_origin_lookup[x]] + local_optional,
|
|
35
|
+
environment=environment,
|
|
36
|
+
g=g,
|
|
37
|
+
depth=depth + 1,
|
|
38
|
+
history=history,
|
|
39
|
+
conditions=conditions,
|
|
40
|
+
accept_partial=True,
|
|
41
|
+
)
|
|
42
|
+
if not attempt:
|
|
43
|
+
continue
|
|
44
|
+
return [attempt]
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def gen_unnest_node(
|
|
49
|
+
concept: BuildConcept,
|
|
50
|
+
local_optional: List[BuildConcept],
|
|
51
|
+
history: History,
|
|
52
|
+
environment: BuildEnvironment,
|
|
53
|
+
g,
|
|
54
|
+
depth: int,
|
|
55
|
+
source_concepts,
|
|
56
|
+
conditions: BuildWhereClause | None = None,
|
|
57
|
+
) -> StrategyNode | None:
|
|
58
|
+
arguments = []
|
|
59
|
+
join_nodes: list[StrategyNode] = []
|
|
60
|
+
depth_prefix = "\t" * depth
|
|
61
|
+
if isinstance(concept.lineage, BuildFunction):
|
|
62
|
+
arguments = concept.lineage.concept_arguments
|
|
63
|
+
search_optional = local_optional
|
|
64
|
+
if (not arguments) and (local_optional and concept.pseudonyms):
|
|
65
|
+
logger.info(
|
|
66
|
+
f"{padding(depth)}{LOGGER_PREFIX} unnest node for {concept} has no parents; creating solo unnest node"
|
|
67
|
+
)
|
|
68
|
+
join_nodes += get_pseudonym_parents(
|
|
69
|
+
concept,
|
|
70
|
+
local_optional,
|
|
71
|
+
source_concepts,
|
|
72
|
+
environment,
|
|
73
|
+
g,
|
|
74
|
+
depth,
|
|
75
|
+
history,
|
|
76
|
+
conditions,
|
|
77
|
+
)
|
|
78
|
+
logger.info(
|
|
79
|
+
f"{padding(depth)}{LOGGER_PREFIX} unnest node for {concept} got join nodes {join_nodes}"
|
|
80
|
+
)
|
|
81
|
+
search_optional = []
|
|
82
|
+
|
|
83
|
+
equivalent_optional = [x for x in search_optional if x.lineage == concept.lineage]
|
|
84
|
+
|
|
85
|
+
non_equivalent_optional = [
|
|
86
|
+
x for x in search_optional if x not in equivalent_optional
|
|
87
|
+
]
|
|
88
|
+
all_parents = arguments + non_equivalent_optional
|
|
89
|
+
logger.info(
|
|
90
|
+
f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} with lineage {concept.lineage} has parents + optional {all_parents} and equivalent optional {equivalent_optional}"
|
|
91
|
+
)
|
|
92
|
+
local_conditions = False
|
|
93
|
+
expected_outputs = [concept] + local_optional
|
|
94
|
+
parent: StrategyNode | None = None
|
|
95
|
+
if arguments or search_optional:
|
|
96
|
+
parent = source_concepts(
|
|
97
|
+
mandatory_list=all_parents,
|
|
98
|
+
environment=environment,
|
|
99
|
+
g=g,
|
|
100
|
+
depth=depth + 1,
|
|
101
|
+
history=history,
|
|
102
|
+
conditions=conditions,
|
|
103
|
+
)
|
|
104
|
+
if not parent:
|
|
105
|
+
logger.info(
|
|
106
|
+
f"{padding(depth)}{LOGGER_PREFIX} could not find unnest node parents"
|
|
107
|
+
)
|
|
108
|
+
return None
|
|
109
|
+
elif conditions:
|
|
110
|
+
logger.info(
|
|
111
|
+
f"{padding(depth)}{LOGGER_PREFIX} unnest node has no parents but conditions inputs {conditions.row_arguments} vs expected output {expected_outputs}"
|
|
112
|
+
)
|
|
113
|
+
if all([x.address in expected_outputs for x in conditions.row_arguments]):
|
|
114
|
+
local_conditions = True
|
|
115
|
+
else:
|
|
116
|
+
parent = source_concepts(
|
|
117
|
+
mandatory_list=conditions.conditional.row_arguments,
|
|
118
|
+
environment=environment,
|
|
119
|
+
g=g,
|
|
120
|
+
depth=depth + 1,
|
|
121
|
+
history=history,
|
|
122
|
+
conditions=conditions,
|
|
123
|
+
)
|
|
124
|
+
if not parent:
|
|
125
|
+
logger.info(
|
|
126
|
+
f"{padding(depth)}{LOGGER_PREFIX} could not find unnest node condition inputs with no parents"
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
else:
|
|
130
|
+
parent = None
|
|
131
|
+
logger.info(
|
|
132
|
+
f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} got parent {parent}"
|
|
133
|
+
)
|
|
134
|
+
base = UnnestNode(
|
|
135
|
+
unnest_concepts=[concept] + equivalent_optional,
|
|
136
|
+
input_concepts=arguments + non_equivalent_optional,
|
|
137
|
+
output_concepts=[concept] + search_optional,
|
|
138
|
+
environment=environment,
|
|
139
|
+
parents=([parent] if parent else []),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
conditional = conditions.conditional if conditions else None
|
|
143
|
+
if join_nodes:
|
|
144
|
+
logger.info(
|
|
145
|
+
f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} needs to merge with join nodes {join_nodes}"
|
|
146
|
+
)
|
|
147
|
+
for x in join_nodes:
|
|
148
|
+
logger.info(
|
|
149
|
+
f"{depth_prefix}{LOGGER_PREFIX} join node {x} with partial {x.partial_concepts}"
|
|
150
|
+
)
|
|
151
|
+
pseudonyms = [
|
|
152
|
+
environment.alias_origin_lookup[p] for p in concept.pseudonyms
|
|
153
|
+
]
|
|
154
|
+
x.add_partial_concepts(pseudonyms)
|
|
155
|
+
return MergeNode(
|
|
156
|
+
input_concepts=base.output_concepts
|
|
157
|
+
+ [j for n in join_nodes for j in n.output_concepts],
|
|
158
|
+
output_concepts=[concept] + local_optional,
|
|
159
|
+
environment=environment,
|
|
160
|
+
parents=[base] + join_nodes,
|
|
161
|
+
conditions=conditional if local_conditions is True else None,
|
|
162
|
+
preexisting_conditions=(
|
|
163
|
+
conditional if conditional and local_conditions is False else None
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
# we need to sometimes nest an unnest node,
|
|
167
|
+
# as unnest operations are not valid in all situations
|
|
168
|
+
new = WhereSafetyNode(
|
|
169
|
+
input_concepts=base.output_concepts,
|
|
170
|
+
output_concepts=base.output_concepts,
|
|
171
|
+
environment=environment,
|
|
172
|
+
parents=[base],
|
|
173
|
+
conditions=conditional if local_conditions is True else None,
|
|
174
|
+
preexisting_conditions=(
|
|
175
|
+
conditional if conditional and local_conditions is False else None
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
# qds = new.resolve()
|
|
179
|
+
# assert qds.source_map[concept.address] == {base.resolve()}
|
|
180
|
+
# for x in equivalent_optional:
|
|
181
|
+
# assert qds.source_map[x.address] == {base.resolve()}
|
|
182
|
+
return new
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from trilogy.constants import logger
|
|
4
|
+
from trilogy.core.models.build import (
|
|
5
|
+
BuildConcept,
|
|
6
|
+
BuildGrain,
|
|
7
|
+
BuildWhereClause,
|
|
8
|
+
BuildWindowItem,
|
|
9
|
+
)
|
|
10
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
11
|
+
from trilogy.core.processing.node_generators.common import (
|
|
12
|
+
gen_enrichment_node,
|
|
13
|
+
)
|
|
14
|
+
from trilogy.core.processing.nodes import (
|
|
15
|
+
History,
|
|
16
|
+
StrategyNode,
|
|
17
|
+
WhereSafetyNode,
|
|
18
|
+
WindowNode,
|
|
19
|
+
)
|
|
20
|
+
from trilogy.core.processing.utility import create_log_lambda, padding
|
|
21
|
+
from trilogy.utility import unique
|
|
22
|
+
|
|
23
|
+
LOGGER_PREFIX = "[GEN_WINDOW_NODE]"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
WINDOW_TYPES = (BuildWindowItem,)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_window_parent_concepts(
|
|
30
|
+
concept: BuildConcept, environment: BuildEnvironment, depth: int
|
|
31
|
+
) -> tuple[BuildConcept, List[BuildConcept]]:
|
|
32
|
+
if not isinstance(concept.lineage, WINDOW_TYPES):
|
|
33
|
+
raise ValueError
|
|
34
|
+
base = []
|
|
35
|
+
if concept.lineage.over:
|
|
36
|
+
base += concept.lineage.over
|
|
37
|
+
if concept.lineage.order_by:
|
|
38
|
+
for item in concept.lineage.order_by:
|
|
39
|
+
base += item.concept_arguments
|
|
40
|
+
if concept.grain:
|
|
41
|
+
for gitem in concept.grain.components:
|
|
42
|
+
logger.info(
|
|
43
|
+
f"{padding(depth)}{LOGGER_PREFIX} appending grain item {gitem} to base"
|
|
44
|
+
)
|
|
45
|
+
base.append(environment.concepts[gitem])
|
|
46
|
+
return concept.lineage.content, unique(base, "address")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def gen_window_node(
|
|
50
|
+
concept: BuildConcept,
|
|
51
|
+
local_optional: list[BuildConcept],
|
|
52
|
+
environment: BuildEnvironment,
|
|
53
|
+
g,
|
|
54
|
+
depth: int,
|
|
55
|
+
source_concepts,
|
|
56
|
+
history: History,
|
|
57
|
+
conditions: BuildWhereClause | None = None,
|
|
58
|
+
) -> StrategyNode | None:
|
|
59
|
+
base, parent_concepts = resolve_window_parent_concepts(concept, environment, depth)
|
|
60
|
+
logger.info(
|
|
61
|
+
f"{padding(depth)}{LOGGER_PREFIX} generating window node for {concept} with parents {[x.address for x in parent_concepts]} and optional {local_optional}"
|
|
62
|
+
)
|
|
63
|
+
equivalent_optional = [
|
|
64
|
+
x
|
|
65
|
+
for x in local_optional
|
|
66
|
+
if isinstance(x.lineage, WINDOW_TYPES)
|
|
67
|
+
and resolve_window_parent_concepts(x, environment, depth)[1] == parent_concepts
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
targets = [base]
|
|
71
|
+
# append in keys to get the right grain
|
|
72
|
+
if concept.keys:
|
|
73
|
+
for item in concept.keys:
|
|
74
|
+
if item in targets:
|
|
75
|
+
continue
|
|
76
|
+
logger.info(
|
|
77
|
+
f"{padding(depth)}{LOGGER_PREFIX} appending search for key {item}"
|
|
78
|
+
)
|
|
79
|
+
targets.append(environment.concepts[item])
|
|
80
|
+
additional_outputs = []
|
|
81
|
+
if equivalent_optional:
|
|
82
|
+
for x in equivalent_optional:
|
|
83
|
+
assert isinstance(x.lineage, WINDOW_TYPES)
|
|
84
|
+
base, parents = resolve_window_parent_concepts(x, environment, depth)
|
|
85
|
+
logger.info(
|
|
86
|
+
f"{padding(depth)}{LOGGER_PREFIX} found equivalent optional {x} with parents {parents}"
|
|
87
|
+
)
|
|
88
|
+
additional_outputs.append(x)
|
|
89
|
+
# also append the base concept it's being grouped over
|
|
90
|
+
targets.append(base)
|
|
91
|
+
|
|
92
|
+
grain_equivalents = [
|
|
93
|
+
x
|
|
94
|
+
for x in local_optional
|
|
95
|
+
if x.keys
|
|
96
|
+
and all([key in targets for key in x.keys])
|
|
97
|
+
and x.grain == concept.grain
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
for x in grain_equivalents:
|
|
101
|
+
if x.address in additional_outputs:
|
|
102
|
+
continue
|
|
103
|
+
targets.append(x)
|
|
104
|
+
|
|
105
|
+
# finally, the ones we'll need to enrich
|
|
106
|
+
non_equivalent_optional = [x for x in local_optional if x.address not in targets]
|
|
107
|
+
|
|
108
|
+
logger.info(
|
|
109
|
+
f"{padding(depth)}{LOGGER_PREFIX} resolving final parents {parent_concepts + targets}"
|
|
110
|
+
)
|
|
111
|
+
parent_node: StrategyNode = source_concepts(
|
|
112
|
+
mandatory_list=parent_concepts + targets,
|
|
113
|
+
environment=environment,
|
|
114
|
+
g=g,
|
|
115
|
+
depth=depth + 1,
|
|
116
|
+
history=history,
|
|
117
|
+
conditions=conditions,
|
|
118
|
+
)
|
|
119
|
+
if not parent_node:
|
|
120
|
+
logger.info(f"{padding(depth)}{LOGGER_PREFIX} window node parents unresolvable")
|
|
121
|
+
return None
|
|
122
|
+
parent_node.resolve()
|
|
123
|
+
if not all(
|
|
124
|
+
[
|
|
125
|
+
x.address in [y.address for y in parent_node.output_concepts]
|
|
126
|
+
for x in parent_concepts
|
|
127
|
+
]
|
|
128
|
+
):
|
|
129
|
+
missing = [
|
|
130
|
+
x
|
|
131
|
+
for x in parent_concepts
|
|
132
|
+
if x.address not in [y.address for y in parent_node.output_concepts]
|
|
133
|
+
]
|
|
134
|
+
logger.info(
|
|
135
|
+
f"{padding(depth)}{LOGGER_PREFIX} window node parents unresolvable, missing {missing}"
|
|
136
|
+
)
|
|
137
|
+
raise SyntaxError
|
|
138
|
+
_window_node = WindowNode(
|
|
139
|
+
input_concepts=parent_concepts + targets,
|
|
140
|
+
output_concepts=[concept] + additional_outputs + parent_concepts + targets,
|
|
141
|
+
environment=environment,
|
|
142
|
+
parents=[
|
|
143
|
+
parent_node,
|
|
144
|
+
],
|
|
145
|
+
depth=depth,
|
|
146
|
+
preexisting_conditions=conditions.conditional if conditions else None,
|
|
147
|
+
)
|
|
148
|
+
_window_node.rebuild_cache()
|
|
149
|
+
_window_node.resolve()
|
|
150
|
+
|
|
151
|
+
window_node = WhereSafetyNode(
|
|
152
|
+
input_concepts=[concept] + additional_outputs + parent_concepts + targets,
|
|
153
|
+
output_concepts=[concept] + additional_outputs + parent_concepts + targets,
|
|
154
|
+
environment=environment,
|
|
155
|
+
parents=[_window_node],
|
|
156
|
+
preexisting_conditions=conditions.conditional if conditions else None,
|
|
157
|
+
grain=BuildGrain.from_concepts(
|
|
158
|
+
concepts=[concept] + additional_outputs + parent_concepts + targets,
|
|
159
|
+
environment=environment,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
if not non_equivalent_optional:
|
|
163
|
+
logger.info(
|
|
164
|
+
f"{padding(depth)}{LOGGER_PREFIX} no optional concepts, returning window node"
|
|
165
|
+
)
|
|
166
|
+
# prune outputs if we don't need join keys
|
|
167
|
+
window_node.set_output_concepts([concept] + additional_outputs + targets)
|
|
168
|
+
return window_node
|
|
169
|
+
|
|
170
|
+
missing_optional = [
|
|
171
|
+
x.address
|
|
172
|
+
for x in local_optional
|
|
173
|
+
if x.address not in window_node.output_concepts
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
if not missing_optional:
|
|
177
|
+
logger.info(
|
|
178
|
+
f"{padding(depth)}{LOGGER_PREFIX} no extra enrichment needed for window node, has all of {[x.address for x in local_optional]}"
|
|
179
|
+
)
|
|
180
|
+
return window_node
|
|
181
|
+
logger.info(
|
|
182
|
+
f"{padding(depth)}{LOGGER_PREFIX} window node for {concept.address} requires enrichment, missing {missing_optional}, has {[x.address for x in window_node.output_concepts]}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return gen_enrichment_node(
|
|
186
|
+
window_node,
|
|
187
|
+
join_keys=[
|
|
188
|
+
environment.concepts[c]
|
|
189
|
+
for c in BuildGrain.from_concepts(
|
|
190
|
+
concepts=targets, environment=environment
|
|
191
|
+
).components
|
|
192
|
+
],
|
|
193
|
+
local_optional=local_optional,
|
|
194
|
+
environment=environment,
|
|
195
|
+
g=g,
|
|
196
|
+
depth=depth,
|
|
197
|
+
source_concepts=source_concepts,
|
|
198
|
+
log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
|
|
199
|
+
history=history,
|
|
200
|
+
conditions=conditions,
|
|
201
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Nodes
|
|
2
|
+
|
|
3
|
+
Nodes are the initial logical planning unit for a query path.
|
|
4
|
+
|
|
5
|
+
A query will initially resolve recursively to nodes, which are a lightweight operatoror representation.
|
|
6
|
+
|
|
7
|
+
(Nodes will then later be instantiated as QueryDatasources/Datasources, a more complete intermediate representation,
|
|
8
|
+
before finally becoming CTEs; a complete simplified object that is ready to be rendered as SQL).
|
|
9
|
+
|
|
10
|
+
## Union Nodes
|
|
11
|
+
|
|
12
|
+
Union nodes attempt to logically resolve union bindings.
|
|
13
|
+
|
|
14
|
+
For a union concept:
|
|
15
|
+
|
|
16
|
+
``` sql
|
|
17
|
+
select
|
|
18
|
+
a,
|
|
19
|
+
b,
|
|
20
|
+
c,
|
|
21
|
+
union(a,b),
|
|
22
|
+
union(b,c),
|
|
23
|
+
a.prop,
|
|
24
|
+
b.prop,
|
|
25
|
+
c.prop
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
We
|