pytrilogy 0.0.3.54__tar.gz → 0.0.3.55__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.

Files changed (143) hide show
  1. {pytrilogy-0.0.3.54/pytrilogy.egg-info → pytrilogy-0.0.3.55}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/pytrilogy.egg-info/SOURCES.txt +2 -0
  4. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/constants.py +2 -0
  6. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/enums.py +5 -0
  7. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/functions.py +3 -0
  8. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/author.py +6 -0
  9. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/execute.py +207 -2
  10. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/optimization.py +3 -3
  11. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/optimizations/inline_datasource.py +5 -7
  12. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/concept_strategies_v3.py +17 -0
  13. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/__init__.py +2 -0
  14. pytrilogy-0.0.3.55/trilogy/core/processing/node_generators/recursive_node.py +87 -0
  15. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/__init__.py +2 -0
  16. pytrilogy-0.0.3.55/trilogy/core/processing/nodes/recursive_node.py +46 -0
  17. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/query_processor.py +7 -1
  18. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/base.py +11 -2
  19. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/bigquery.py +5 -6
  20. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/common.py +19 -3
  21. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/duckdb.py +1 -1
  22. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/snowflake.py +8 -8
  23. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/common.py +3 -0
  24. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/parse_engine.py +6 -0
  25. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/trilogy.lark +3 -1
  26. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/LICENSE.md +0 -0
  27. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/README.md +0 -0
  28. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/pyproject.toml +0 -0
  29. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/pytrilogy.egg-info/dependency_links.txt +0 -0
  30. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/pytrilogy.egg-info/entry_points.txt +0 -0
  31. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/pytrilogy.egg-info/requires.txt +0 -0
  32. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/pytrilogy.egg-info/top_level.txt +0 -0
  33. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/setup.cfg +0 -0
  34. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/setup.py +0 -0
  35. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_datatypes.py +0 -0
  36. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_declarations.py +0 -0
  37. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_derived_concepts.py +0 -0
  38. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_discovery_nodes.py +0 -0
  39. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_enums.py +0 -0
  40. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_environment.py +0 -0
  41. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_executor.py +0 -0
  42. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_failure.py +0 -0
  43. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_functions.py +0 -0
  44. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_imports.py +0 -0
  45. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_metadata.py +0 -0
  46. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_models.py +0 -0
  47. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_multi_join_assignments.py +0 -0
  48. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_parse_engine.py +0 -0
  49. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_parsing.py +0 -0
  50. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_parsing_failures.py +0 -0
  51. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_partial_handling.py +0 -0
  52. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_query_processing.py +0 -0
  53. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_query_render.py +0 -0
  54. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_select.py +0 -0
  55. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_show.py +0 -0
  56. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_statements.py +0 -0
  57. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_typing.py +0 -0
  58. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_undefined_concept.py +0 -0
  59. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_user_functions.py +0 -0
  60. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/tests/test_where_clause.py +0 -0
  61. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/authoring/__init__.py +0 -0
  62. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/compiler.py +0 -0
  63. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/__init__.py +0 -0
  64. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/constants.py +0 -0
  65. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/env_processor.py +0 -0
  66. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/environment_helpers.py +0 -0
  67. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/ergonomics.py +0 -0
  68. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/exceptions.py +0 -0
  69. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/graph_models.py +0 -0
  70. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/internal.py +0 -0
  71. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/__init__.py +0 -0
  72. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/build.py +0 -0
  73. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/build_environment.py +0 -0
  74. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/core.py +0 -0
  75. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/datasource.py +0 -0
  76. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/models/environment.py +0 -0
  77. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/optimizations/__init__.py +0 -0
  78. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/optimizations/base_optimization.py +0 -0
  79. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  80. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/__init__.py +0 -0
  81. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/graph_utils.py +0 -0
  82. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  83. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/common.py +0 -0
  84. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  85. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/group_node.py +0 -0
  86. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  87. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  88. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  89. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  90. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  91. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  92. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  93. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/select_node.py +0 -0
  94. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  95. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/union_node.py +0 -0
  96. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  97. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/node_generators/window_node.py +0 -0
  98. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/base_node.py +0 -0
  99. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/filter_node.py +0 -0
  100. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/group_node.py +0 -0
  101. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/merge_node.py +0 -0
  102. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  103. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/union_node.py +0 -0
  104. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  105. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/nodes/window_node.py +0 -0
  106. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/processing/utility.py +0 -0
  107. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/statements/__init__.py +0 -0
  108. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/statements/author.py +0 -0
  109. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/statements/build.py +0 -0
  110. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/statements/common.py +0 -0
  111. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/statements/execute.py +0 -0
  112. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/core/utility.py +0 -0
  113. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/__init__.py +0 -0
  114. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/config.py +0 -0
  115. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/dataframe.py +0 -0
  116. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/enums.py +0 -0
  117. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/postgres.py +0 -0
  118. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/presto.py +0 -0
  119. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/dialect/sql_server.py +0 -0
  120. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/engine.py +0 -0
  121. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/executor.py +0 -0
  122. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/hooks/__init__.py +0 -0
  123. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/hooks/base_hook.py +0 -0
  124. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/hooks/graph_hook.py +0 -0
  125. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/hooks/query_debugger.py +0 -0
  126. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/metadata/__init__.py +0 -0
  127. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parser.py +0 -0
  128. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/__init__.py +0 -0
  129. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/config.py +0 -0
  130. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/exceptions.py +0 -0
  131. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/helpers.py +0 -0
  132. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/parsing/render.py +0 -0
  133. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/py.typed +0 -0
  134. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/render.py +0 -0
  135. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/scripts/__init__.py +0 -0
  136. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/scripts/trilogy.py +0 -0
  137. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/std/__init__.py +0 -0
  138. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/std/date.preql +0 -0
  139. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/std/display.preql +0 -0
  140. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/std/geography.preql +0 -0
  141. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/std/money.preql +0 -0
  142. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/std/report.preql +0 -0
  143. {pytrilogy-0.0.3.54 → pytrilogy-0.0.3.55}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.54
3
+ Version: 0.0.3.55
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.54
3
+ Version: 0.0.3.55
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -81,6 +81,7 @@ trilogy/core/processing/node_generators/group_node.py
81
81
  trilogy/core/processing/node_generators/group_to_node.py
82
82
  trilogy/core/processing/node_generators/multiselect_node.py
83
83
  trilogy/core/processing/node_generators/node_merge_node.py
84
+ trilogy/core/processing/node_generators/recursive_node.py
84
85
  trilogy/core/processing/node_generators/rowset_node.py
85
86
  trilogy/core/processing/node_generators/select_merge_node.py
86
87
  trilogy/core/processing/node_generators/select_node.py
@@ -95,6 +96,7 @@ trilogy/core/processing/nodes/base_node.py
95
96
  trilogy/core/processing/nodes/filter_node.py
96
97
  trilogy/core/processing/nodes/group_node.py
97
98
  trilogy/core/processing/nodes/merge_node.py
99
+ trilogy/core/processing/nodes/recursive_node.py
98
100
  trilogy/core/processing/nodes/select_node_v2.py
99
101
  trilogy/core/processing/nodes/union_node.py
100
102
  trilogy/core/processing/nodes/unnest_node.py
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.54"
7
+ __version__ = "0.0.3.55"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -7,6 +7,8 @@ logger = getLogger("trilogy")
7
7
 
8
8
  DEFAULT_NAMESPACE = "local"
9
9
 
10
+ RECURSIVE_GATING_CONCEPT = "_terminal"
11
+
10
12
  VIRTUAL_CONCEPT_PREFIX = "_virt"
11
13
 
12
14
  ENV_CACHE_NAME = ".preql_cache.json"
@@ -51,6 +51,7 @@ class Derivation(Enum):
51
51
  ROOT = "root"
52
52
  ROWSET = "rowset"
53
53
  MULTISELECT = "multiselect"
54
+ RECURSIVE = "recursive"
54
55
 
55
56
 
56
57
  class Granularity(Enum):
@@ -117,6 +118,7 @@ class FunctionType(Enum):
117
118
 
118
119
  # structural
119
120
  UNNEST = "unnest"
121
+ RECURSE_EDGE = "recurse_edge"
120
122
 
121
123
  UNION = "union"
122
124
 
@@ -233,6 +235,8 @@ class FunctionClass(Enum):
233
235
 
234
236
  ONE_TO_MANY = [FunctionType.UNNEST]
235
237
 
238
+ RECURSIVE = [FunctionType.RECURSE_EDGE]
239
+
236
240
 
237
241
  class Boolean(Enum):
238
242
  TRUE = "true"
@@ -333,6 +337,7 @@ class SourceType(Enum):
333
337
  MERGE = "merge"
334
338
  BASIC = "basic"
335
339
  UNION = "union"
340
+ RECURSIVE = "recursive"
336
341
 
337
342
 
338
343
  class ShowCategory(Enum):
@@ -190,6 +190,9 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
190
190
  output_type_function=get_unnest_output_type,
191
191
  arg_count=1,
192
192
  ),
193
+ FunctionType.RECURSE_EDGE: FunctionConfig(
194
+ arg_count=2,
195
+ ),
193
196
  FunctionType.GROUP: FunctionConfig(
194
197
  arg_count=-1,
195
198
  output_type_function=lambda args: get_output_type_at_index(args, 0),
@@ -1164,6 +1164,12 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1164
1164
  and lineage.operator == FunctionType.UNNEST
1165
1165
  ):
1166
1166
  return Derivation.UNNEST
1167
+ elif (
1168
+ lineage
1169
+ and isinstance(lineage, (BuildFunction, Function))
1170
+ and lineage.operator == FunctionType.RECURSE_EDGE
1171
+ ):
1172
+ return Derivation.RECURSIVE
1167
1173
  elif (
1168
1174
  lineage
1169
1175
  and isinstance(lineage, (BuildFunction, Function))
@@ -12,9 +12,16 @@ from pydantic import (
12
12
  model_validator,
13
13
  )
14
14
 
15
- from trilogy.constants import CONFIG, logger
15
+ from trilogy.constants import (
16
+ CONFIG,
17
+ DEFAULT_NAMESPACE,
18
+ RECURSIVE_GATING_CONCEPT,
19
+ MagicConstants,
20
+ logger,
21
+ )
16
22
  from trilogy.core.constants import CONSTANT_DATASET
17
23
  from trilogy.core.enums import (
24
+ ComparisonOperator,
18
25
  Derivation,
19
26
  FunctionType,
20
27
  Granularity,
@@ -24,16 +31,20 @@ from trilogy.core.enums import (
24
31
  SourceType,
25
32
  )
26
33
  from trilogy.core.models.build import (
34
+ BuildCaseElse,
35
+ BuildCaseWhen,
27
36
  BuildComparison,
28
37
  BuildConcept,
29
38
  BuildConditional,
30
39
  BuildDatasource,
40
+ BuildExpr,
31
41
  BuildFunction,
32
42
  BuildGrain,
33
43
  BuildOrderBy,
34
44
  BuildParamaterizedConceptReference,
35
45
  BuildParenthetical,
36
46
  BuildRowsetItem,
47
+ DataType,
37
48
  LooseBuildConceptList,
38
49
  )
39
50
  from trilogy.core.models.datasource import Address
@@ -841,6 +852,195 @@ class QueryDatasource(BaseModel):
841
852
  return self.identifier
842
853
 
843
854
 
855
+ class AliasedExpression(BaseModel):
856
+ expr: BuildExpr
857
+ alias: str
858
+
859
+
860
+ class RecursiveCTE(CTE):
861
+
862
+ def generate_loop_functions(
863
+ self,
864
+ recursive_derived: BuildConcept,
865
+ left_recurse_concept: BuildConcept,
866
+ right_recurse_concept: BuildConcept,
867
+ ) -> tuple[BuildConcept, BuildConcept, BuildConcept]:
868
+
869
+ join_gate = BuildConcept(
870
+ name=RECURSIVE_GATING_CONCEPT,
871
+ namespace=DEFAULT_NAMESPACE,
872
+ grain=recursive_derived.grain,
873
+ build_is_aggregate=False,
874
+ datatype=DataType.BOOL,
875
+ purpose=Purpose.KEY,
876
+ derivation=Derivation.BASIC,
877
+ lineage=BuildFunction(
878
+ operator=FunctionType.CASE,
879
+ arguments=[
880
+ BuildCaseWhen(
881
+ comparison=BuildComparison(
882
+ left=right_recurse_concept,
883
+ right=MagicConstants.NULL,
884
+ operator=ComparisonOperator.IS,
885
+ ),
886
+ expr=True,
887
+ ),
888
+ BuildCaseElse(expr=False),
889
+ ],
890
+ output_datatype=DataType.BOOL,
891
+ output_purpose=Purpose.KEY,
892
+ ),
893
+ )
894
+ bottom_join_gate = BuildConcept(
895
+ name=f"{RECURSIVE_GATING_CONCEPT}_two",
896
+ namespace=DEFAULT_NAMESPACE,
897
+ grain=recursive_derived.grain,
898
+ build_is_aggregate=False,
899
+ datatype=DataType.BOOL,
900
+ purpose=Purpose.KEY,
901
+ derivation=Derivation.BASIC,
902
+ lineage=BuildFunction(
903
+ operator=FunctionType.CASE,
904
+ arguments=[
905
+ BuildCaseWhen(
906
+ comparison=BuildComparison(
907
+ left=right_recurse_concept,
908
+ right=MagicConstants.NULL,
909
+ operator=ComparisonOperator.IS,
910
+ ),
911
+ expr=True,
912
+ ),
913
+ BuildCaseElse(expr=False),
914
+ ],
915
+ output_datatype=DataType.BOOL,
916
+ output_purpose=Purpose.KEY,
917
+ ),
918
+ )
919
+ bottom_derivation = BuildConcept(
920
+ name=recursive_derived.name + "_bottom",
921
+ namespace=recursive_derived.namespace,
922
+ grain=recursive_derived.grain,
923
+ build_is_aggregate=False,
924
+ datatype=recursive_derived.datatype,
925
+ purpose=recursive_derived.purpose,
926
+ derivation=Derivation.RECURSIVE,
927
+ lineage=BuildFunction(
928
+ operator=FunctionType.CASE,
929
+ arguments=[
930
+ BuildCaseWhen(
931
+ comparison=BuildComparison(
932
+ left=right_recurse_concept,
933
+ right=MagicConstants.NULL,
934
+ operator=ComparisonOperator.IS,
935
+ ),
936
+ expr=recursive_derived,
937
+ ),
938
+ BuildCaseElse(expr=right_recurse_concept),
939
+ ],
940
+ output_datatype=recursive_derived.datatype,
941
+ output_purpose=recursive_derived.purpose,
942
+ ),
943
+ )
944
+ return bottom_derivation, join_gate, bottom_join_gate
945
+
946
+ @property
947
+ def internal_ctes(self) -> List[CTE]:
948
+ filtered_output = [
949
+ x for x in self.output_columns if x.name != RECURSIVE_GATING_CONCEPT
950
+ ]
951
+ recursive_derived = [
952
+ x for x in self.output_columns if x.derivation == Derivation.RECURSIVE
953
+ ][0]
954
+ if not isinstance(recursive_derived.lineage, BuildFunction):
955
+ raise SyntaxError(
956
+ "Recursive CTEs must have a function lineage, found"
957
+ f" {recursive_derived.lineage}"
958
+ )
959
+ left_recurse_concept = recursive_derived.lineage.concept_arguments[0]
960
+ right_recurse_concept = recursive_derived.lineage.concept_arguments[1]
961
+ parent_ctes: List[CTE | UnionCTE]
962
+ if self.parent_ctes:
963
+ base = self.parent_ctes[0]
964
+ loop_input_cte = base
965
+ parent_ctes = [base]
966
+ parent_identifier = base.identifier
967
+ else:
968
+ raise SyntaxError("Recursive CTEs must have a parent CTE currently")
969
+ bottom_derivation, join_gate, bottom_join_gate = self.generate_loop_functions(
970
+ recursive_derived, left_recurse_concept, right_recurse_concept
971
+ )
972
+ base_output = [*filtered_output, join_gate]
973
+ bottom_output = []
974
+ for x in filtered_output:
975
+ if x.address == recursive_derived.address:
976
+ bottom_output.append(bottom_derivation)
977
+ else:
978
+ bottom_output.append(x)
979
+
980
+ bottom_output = [*bottom_output, bottom_join_gate]
981
+ top = CTE(
982
+ name=self.name,
983
+ source=self.source,
984
+ output_columns=base_output,
985
+ source_map=self.source_map,
986
+ grain=self.grain,
987
+ existence_source_map=self.existence_source_map,
988
+ parent_ctes=self.parent_ctes,
989
+ joins=self.joins,
990
+ condition=self.condition,
991
+ partial_concepts=self.partial_concepts,
992
+ hidden_concepts=self.hidden_concepts,
993
+ nullable_concepts=self.nullable_concepts,
994
+ join_derived_concepts=self.join_derived_concepts,
995
+ group_to_grain=self.group_to_grain,
996
+ order_by=self.order_by,
997
+ limit=self.limit,
998
+ )
999
+ top_cte_array: list[CTE | UnionCTE] = [top]
1000
+ bottom_source_map = {
1001
+ left_recurse_concept.address: [top.identifier],
1002
+ right_recurse_concept.address: [parent_identifier],
1003
+ # recursive_derived.address: self.source_map[recursive_derived.address],
1004
+ join_gate.address: [top.identifier],
1005
+ recursive_derived.address: [top.identifier],
1006
+ }
1007
+ bottom = CTE(
1008
+ name=self.name,
1009
+ source=self.source,
1010
+ output_columns=bottom_output,
1011
+ source_map=bottom_source_map,
1012
+ grain=self.grain,
1013
+ existence_source_map=self.existence_source_map,
1014
+ parent_ctes=top_cte_array + parent_ctes,
1015
+ joins=[
1016
+ Join(
1017
+ right_cte=loop_input_cte,
1018
+ jointype=JoinType.INNER,
1019
+ joinkey_pairs=[
1020
+ CTEConceptPair(
1021
+ left=recursive_derived,
1022
+ right=left_recurse_concept,
1023
+ existing_datasource=loop_input_cte.source,
1024
+ modifiers=[],
1025
+ cte=top,
1026
+ )
1027
+ ],
1028
+ condition=BuildComparison(
1029
+ left=join_gate, right=True, operator=ComparisonOperator.IS_NOT
1030
+ ),
1031
+ )
1032
+ ],
1033
+ partial_concepts=self.partial_concepts,
1034
+ hidden_concepts=self.hidden_concepts,
1035
+ nullable_concepts=self.nullable_concepts,
1036
+ join_derived_concepts=self.join_derived_concepts,
1037
+ group_to_grain=self.group_to_grain,
1038
+ order_by=self.order_by,
1039
+ limit=self.limit,
1040
+ )
1041
+ return [top, bottom]
1042
+
1043
+
844
1044
  class UnionCTE(BaseModel):
845
1045
  name: str
846
1046
  source: QueryDatasource
@@ -891,6 +1091,10 @@ class UnionCTE(BaseModel):
891
1091
  def condition(self, value):
892
1092
  raise NotImplementedError
893
1093
 
1094
+ @property
1095
+ def identifier(self) -> str:
1096
+ return self.name
1097
+
894
1098
  @property
895
1099
  def safe_identifier(self):
896
1100
  return self.name
@@ -906,12 +1110,13 @@ class UnionCTE(BaseModel):
906
1110
 
907
1111
 
908
1112
  class Join(BaseModel):
909
- right_cte: CTE
1113
+ right_cte: CTE | UnionCTE
910
1114
  jointype: JoinType
911
1115
  left_cte: CTE | None = None
912
1116
  joinkey_pairs: List[CTEConceptPair] | None = None
913
1117
  inlined_ctes: set[str] = Field(default_factory=set)
914
1118
  quote: str | None = None
1119
+ condition: BuildConditional | BuildComparison | BuildParenthetical | None = None
915
1120
 
916
1121
  def inline_cte(self, cte: CTE):
917
1122
  self.inlined_ctes.add(cte.name)
@@ -3,7 +3,7 @@ from trilogy.core.enums import BooleanOperator, Derivation
3
3
  from trilogy.core.models.build import (
4
4
  BuildConditional,
5
5
  )
6
- from trilogy.core.models.execute import CTE, UnionCTE
6
+ from trilogy.core.models.execute import CTE, RecursiveCTE, UnionCTE
7
7
  from trilogy.core.optimizations import (
8
8
  InlineDatasource,
9
9
  OptimizationRule,
@@ -123,7 +123,7 @@ def gen_inverse_map(input: list[CTE | UnionCTE]) -> dict[str, list[CTE | UnionCT
123
123
  SENSITIVE_DERIVATIONS = [
124
124
  Derivation.UNNEST,
125
125
  Derivation.WINDOW,
126
- # Derivation.AGGREGATE,
126
+ Derivation.RECURSIVE,
127
127
  ]
128
128
 
129
129
 
@@ -133,7 +133,7 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
133
133
  if len(cte.parent_ctes) != 1:
134
134
  return None
135
135
  direct_parent = cte.parent_ctes[0]
136
- if isinstance(direct_parent, UnionCTE):
136
+ if isinstance(direct_parent, (UnionCTE, RecursiveCTE)):
137
137
  return None
138
138
 
139
139
  output_addresses = set([x.address for x in cte.output_columns])
@@ -1,13 +1,8 @@
1
1
  from collections import defaultdict
2
2
 
3
3
  from trilogy.constants import CONFIG
4
-
5
- # from trilogy.core.models.datasource import Datasource
6
4
  from trilogy.core.models.build import BuildDatasource
7
- from trilogy.core.models.execute import (
8
- CTE,
9
- UnionCTE,
10
- )
5
+ from trilogy.core.models.execute import CTE, RecursiveCTE, UnionCTE
11
6
  from trilogy.core.optimizations.base_optimization import OptimizationRule
12
7
 
13
8
 
@@ -24,7 +19,8 @@ class InlineDatasource(OptimizationRule):
24
19
  return any(
25
20
  self.optimize(x, inverse_map=inverse_map) for x in cte.internal_ctes
26
21
  )
27
-
22
+ if isinstance(cte, RecursiveCTE):
23
+ return False
28
24
  if not cte.parent_ctes:
29
25
  return False
30
26
 
@@ -36,6 +32,8 @@ class InlineDatasource(OptimizationRule):
36
32
  for parent_cte in cte.parent_ctes:
37
33
  if isinstance(parent_cte, UnionCTE):
38
34
  continue
35
+ if isinstance(parent_cte, RecursiveCTE):
36
+ continue
39
37
  if not parent_cte.is_root_datasource:
40
38
  self.debug(f"Cannot inline: parent {parent_cte.name} is not root")
41
39
  continue
@@ -24,6 +24,7 @@ from trilogy.core.processing.node_generators import (
24
24
  gen_group_to_node,
25
25
  gen_merge_node,
26
26
  gen_multiselect_node,
27
+ gen_recursive_node,
27
28
  gen_rowset_node,
28
29
  gen_synonym_node,
29
30
  gen_union_node,
@@ -150,6 +151,7 @@ def get_priority_concept(
150
151
  + [c for c in remaining_concept if c.derivation == Derivation.FILTER]
151
152
  # unnests are weird?
152
153
  + [c for c in remaining_concept if c.derivation == Derivation.UNNEST]
154
+ + [c for c in remaining_concept if c.derivation == Derivation.RECURSIVE]
153
155
  + [c for c in remaining_concept if c.derivation == Derivation.BASIC]
154
156
  # finally our plain selects
155
157
  + [
@@ -294,6 +296,20 @@ def generate_node(
294
296
  source_concepts=source_concepts,
295
297
  conditions=conditions,
296
298
  )
299
+ elif concept.derivation == Derivation.RECURSIVE:
300
+ logger.info(
301
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating recursive node with optional {[x.address for x in local_optional]} and condition {conditions}"
302
+ )
303
+ return gen_recursive_node(
304
+ concept,
305
+ local_optional,
306
+ history=history,
307
+ environment=environment,
308
+ g=g,
309
+ depth=depth + 1,
310
+ source_concepts=source_concepts,
311
+ conditions=conditions,
312
+ )
297
313
  elif concept.derivation == Derivation.UNION:
298
314
  logger.info(
299
315
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating union node with optional {[x.address for x in local_optional]} and condition {conditions}"
@@ -920,6 +936,7 @@ def _search_concepts(
920
936
  Derivation.FILTER,
921
937
  Derivation.WINDOW,
922
938
  Derivation.UNNEST,
939
+ Derivation.RECURSIVE,
923
940
  Derivation.ROWSET,
924
941
  Derivation.BASIC,
925
942
  Derivation.MULTISELECT,
@@ -4,6 +4,7 @@ from .group_node import gen_group_node
4
4
  from .group_to_node import gen_group_to_node
5
5
  from .multiselect_node import gen_multiselect_node
6
6
  from .node_merge_node import gen_merge_node
7
+ from .recursive_node import gen_recursive_node
7
8
  from .rowset_node import gen_rowset_node
8
9
  from .select_node import gen_select_node
9
10
  from .synonym_node import gen_synonym_node
@@ -24,4 +25,5 @@ __all__ = [
24
25
  "gen_rowset_node",
25
26
  "gen_multiselect_node",
26
27
  "gen_synonym_node",
28
+ "gen_recursive_node",
27
29
  ]
@@ -0,0 +1,87 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import DEFAULT_NAMESPACE, RECURSIVE_GATING_CONCEPT, logger
4
+ from trilogy.core.models.build import (
5
+ BuildComparison,
6
+ BuildConcept,
7
+ BuildFunction,
8
+ BuildGrain,
9
+ BuildWhereClause,
10
+ ComparisonOperator,
11
+ DataType,
12
+ Derivation,
13
+ Purpose,
14
+ )
15
+ from trilogy.core.models.build_environment import BuildEnvironment
16
+ from trilogy.core.processing.nodes import History, RecursiveNode, StrategyNode
17
+ from trilogy.core.processing.utility import padding
18
+
19
+ LOGGER_PREFIX = "[GEN_RECURSIVE_NODE]"
20
+
21
+ GATING_CONCEPT = BuildConcept(
22
+ name=RECURSIVE_GATING_CONCEPT,
23
+ namespace=DEFAULT_NAMESPACE,
24
+ grain=BuildGrain(),
25
+ build_is_aggregate=False,
26
+ datatype=DataType.BOOL,
27
+ purpose=Purpose.KEY,
28
+ derivation=Derivation.BASIC,
29
+ )
30
+
31
+
32
+ def gen_recursive_node(
33
+ concept: BuildConcept,
34
+ local_optional: List[BuildConcept],
35
+ history: History,
36
+ environment: BuildEnvironment,
37
+ g,
38
+ depth: int,
39
+ source_concepts,
40
+ conditions: BuildWhereClause | None = None,
41
+ ) -> StrategyNode | None:
42
+ arguments = []
43
+ if isinstance(concept.lineage, BuildFunction):
44
+ arguments = concept.lineage.concept_arguments
45
+ logger.info(
46
+ f"{padding(depth)}{LOGGER_PREFIX} Fetching recursive node for {concept.name} with arguments {arguments} and conditions {conditions}"
47
+ )
48
+ parent = source_concepts(
49
+ mandatory_list=arguments,
50
+ environment=environment,
51
+ g=g,
52
+ depth=depth + 1,
53
+ history=history,
54
+ # conditions=conditions,
55
+ )
56
+ if not parent:
57
+ logger.info(
58
+ f"{padding(depth)}{LOGGER_PREFIX} could not find recursive node parents"
59
+ )
60
+ return None
61
+ outputs = (
62
+ [concept]
63
+ + arguments
64
+ + [
65
+ GATING_CONCEPT,
66
+ ]
67
+ )
68
+ base = RecursiveNode(
69
+ input_concepts=arguments,
70
+ output_concepts=outputs,
71
+ environment=environment,
72
+ parents=([parent] if (arguments or local_optional) else []),
73
+ # preexisting_conditions=conditions
74
+ )
75
+ # TODO:
76
+ # recursion will result in a union; group up to our final targets
77
+ wrapped_base = StrategyNode(
78
+ input_concepts=outputs,
79
+ output_concepts=outputs,
80
+ environment=environment,
81
+ parents=[base],
82
+ depth=depth,
83
+ conditions=BuildComparison(
84
+ left=GATING_CONCEPT, right=True, operator=ComparisonOperator.IS
85
+ ),
86
+ )
87
+ return wrapped_base
@@ -10,6 +10,7 @@ from .base_node import NodeJoin, StrategyNode, WhereSafetyNode
10
10
  from .filter_node import FilterNode
11
11
  from .group_node import GroupNode
12
12
  from .merge_node import MergeNode
13
+ from .recursive_node import RecursiveNode
13
14
  from .select_node_v2 import ConstantNode, SelectNode
14
15
  from .union_node import UnionNode
15
16
  from .unnest_node import UnnestNode
@@ -194,4 +195,5 @@ __all__ = [
194
195
  "UnionNode",
195
196
  "History",
196
197
  "WhereSafetyNode",
198
+ "RecursiveNode",
197
199
  ]
@@ -0,0 +1,46 @@
1
+ from typing import List
2
+
3
+ from trilogy.core.enums import SourceType
4
+ from trilogy.core.models.build import BuildConcept
5
+ from trilogy.core.models.build_environment import BuildEnvironment
6
+ from trilogy.core.models.execute import QueryDatasource
7
+ from trilogy.core.processing.nodes.base_node import StrategyNode
8
+
9
+
10
+ class RecursiveNode(StrategyNode):
11
+ """Union nodes represent combining two keyspaces"""
12
+
13
+ source_type = SourceType.RECURSIVE
14
+
15
+ def __init__(
16
+ self,
17
+ input_concepts: List[BuildConcept],
18
+ output_concepts: List[BuildConcept],
19
+ environment: BuildEnvironment,
20
+ whole_grain: bool = False,
21
+ parents: List["StrategyNode"] | None = None,
22
+ depth: int = 0,
23
+ ):
24
+ super().__init__(
25
+ input_concepts=input_concepts,
26
+ output_concepts=output_concepts,
27
+ environment=environment,
28
+ whole_grain=whole_grain,
29
+ parents=parents,
30
+ depth=depth,
31
+ )
32
+
33
+ def _resolve(self) -> QueryDatasource:
34
+ """We need to ensure that any filtered values are removed from the output to avoid inappropriate references"""
35
+ base = super()._resolve()
36
+ return base
37
+
38
+ def copy(self) -> "RecursiveNode":
39
+ return RecursiveNode(
40
+ input_concepts=list(self.input_concepts),
41
+ output_concepts=list(self.output_concepts),
42
+ environment=self.environment,
43
+ whole_grain=self.whole_grain,
44
+ parents=self.parents,
45
+ depth=self.depth,
46
+ )
@@ -26,6 +26,7 @@ from trilogy.core.models.execute import (
26
26
  InstantiatedUnnestJoin,
27
27
  Join,
28
28
  QueryDatasource,
29
+ RecursiveCTE,
29
30
  UnionCTE,
30
31
  UnnestJoin,
31
32
  )
@@ -340,7 +341,12 @@ def datasource_to_cte(
340
341
  base_name, base_alias = resolve_cte_base_name_and_alias_v2(
341
342
  human_id, query_datasource, source_map, final_joins
342
343
  )
343
- cte = CTE(
344
+ cte_class = CTE
345
+
346
+ if query_datasource.source_type == SourceType.RECURSIVE:
347
+ cte_class = RecursiveCTE
348
+ # extra_kwargs['left_recursive_concept'] = query_datasource.left
349
+ cte = cte_class(
344
350
  name=human_id,
345
351
  source=query_datasource,
346
352
  # output columns are what are selected/grouped by