pytrilogy 0.3.142__cp312-cp312-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.
Files changed (200) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp312-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.142.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.142.dist-info/RECORD +200 -0
  6. pytrilogy-0.3.142.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +16 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +113 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +443 -0
  31. trilogy/core/env_processor.py +120 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1227 -0
  36. trilogy/core/graph_models.py +139 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2669 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +501 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +751 -0
  45. trilogy/core/models/execute.py +1177 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +268 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +205 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +653 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +748 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +519 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +596 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1392 -0
  112. trilogy/dialect/bigquery.py +308 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +144 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +231 -0
  117. trilogy/dialect/enums.py +147 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/config.py +75 -0
  127. trilogy/executor.py +568 -0
  128. trilogy/hooks/__init__.py +4 -0
  129. trilogy/hooks/base_hook.py +40 -0
  130. trilogy/hooks/graph_hook.py +139 -0
  131. trilogy/hooks/query_debugger.py +166 -0
  132. trilogy/metadata/__init__.py +0 -0
  133. trilogy/parser.py +10 -0
  134. trilogy/parsing/README.md +21 -0
  135. trilogy/parsing/__init__.py +0 -0
  136. trilogy/parsing/common.py +1069 -0
  137. trilogy/parsing/config.py +5 -0
  138. trilogy/parsing/exceptions.py +8 -0
  139. trilogy/parsing/helpers.py +1 -0
  140. trilogy/parsing/parse_engine.py +2813 -0
  141. trilogy/parsing/render.py +769 -0
  142. trilogy/parsing/trilogy.lark +540 -0
  143. trilogy/py.typed +0 -0
  144. trilogy/render.py +42 -0
  145. trilogy/scripts/README.md +9 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/agent.py +41 -0
  148. trilogy/scripts/agent_info.py +303 -0
  149. trilogy/scripts/common.py +355 -0
  150. trilogy/scripts/dependency/Cargo.lock +617 -0
  151. trilogy/scripts/dependency/Cargo.toml +39 -0
  152. trilogy/scripts/dependency/README.md +131 -0
  153. trilogy/scripts/dependency/build.sh +25 -0
  154. trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
  155. trilogy/scripts/dependency/src/lib.rs +16 -0
  156. trilogy/scripts/dependency/src/main.rs +770 -0
  157. trilogy/scripts/dependency/src/parser.rs +435 -0
  158. trilogy/scripts/dependency/src/preql.pest +208 -0
  159. trilogy/scripts/dependency/src/python_bindings.rs +303 -0
  160. trilogy/scripts/dependency/src/resolver.rs +716 -0
  161. trilogy/scripts/dependency/tests/base.preql +3 -0
  162. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  163. trilogy/scripts/dependency/tests/customer.preql +6 -0
  164. trilogy/scripts/dependency/tests/main.preql +9 -0
  165. trilogy/scripts/dependency/tests/orders.preql +7 -0
  166. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  167. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  168. trilogy/scripts/dependency.py +323 -0
  169. trilogy/scripts/display.py +512 -0
  170. trilogy/scripts/environment.py +46 -0
  171. trilogy/scripts/fmt.py +32 -0
  172. trilogy/scripts/ingest.py +471 -0
  173. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  174. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  175. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  176. trilogy/scripts/ingest_helpers/typing.py +161 -0
  177. trilogy/scripts/init.py +105 -0
  178. trilogy/scripts/parallel_execution.py +713 -0
  179. trilogy/scripts/plan.py +189 -0
  180. trilogy/scripts/run.py +63 -0
  181. trilogy/scripts/serve.py +140 -0
  182. trilogy/scripts/serve_helpers/__init__.py +41 -0
  183. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  184. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  185. trilogy/scripts/serve_helpers/models.py +38 -0
  186. trilogy/scripts/single_execution.py +131 -0
  187. trilogy/scripts/testing.py +119 -0
  188. trilogy/scripts/trilogy.py +68 -0
  189. trilogy/std/__init__.py +0 -0
  190. trilogy/std/color.preql +3 -0
  191. trilogy/std/date.preql +13 -0
  192. trilogy/std/display.preql +18 -0
  193. trilogy/std/geography.preql +22 -0
  194. trilogy/std/metric.preql +15 -0
  195. trilogy/std/money.preql +67 -0
  196. trilogy/std/net.preql +14 -0
  197. trilogy/std/ranking.preql +7 -0
  198. trilogy/std/report.preql +5 -0
  199. trilogy/std/semantic.preql +6 -0
  200. 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