pytrilogy 0.0.3.99__tar.gz → 0.0.3.101__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 (159) hide show
  1. {pytrilogy-0.0.3.99/pytrilogy.egg-info → pytrilogy-0.0.3.101}/PKG-INFO +15 -7
  2. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/README.md +14 -6
  3. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101/pytrilogy.egg-info}/PKG-INFO +15 -7
  4. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/pytrilogy.egg-info/SOURCES.txt +1 -1
  5. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_discovery_nodes.py +3 -3
  6. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_parsing.py +1 -1
  7. pytrilogy-0.0.3.101/tests/test_validators.py +209 -0
  8. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/__init__.py +1 -1
  9. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/constants.py +1 -0
  10. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/exceptions.py +1 -1
  11. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/functions.py +5 -2
  12. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/core.py +3 -0
  13. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/execute.py +14 -7
  14. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/concept_strategies_v3.py +7 -37
  15. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/discovery_utility.py +30 -0
  16. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/basic_node.py +12 -18
  17. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/group_node.py +4 -2
  18. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/node_merge_node.py +16 -5
  19. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/unnest_node.py +30 -3
  20. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/group_node.py +6 -4
  21. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/merge_node.py +9 -1
  22. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/select_node_v2.py +1 -0
  23. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/validation/datasource.py +30 -6
  24. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/base.py +1 -1
  25. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/render.py +5 -3
  26. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/trilogy.lark +6 -3
  27. pytrilogy-0.0.3.99/trilogy/core/processing/discovery_loop.py +0 -0
  28. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/LICENSE.md +0 -0
  29. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/pyproject.toml +0 -0
  30. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/pytrilogy.egg-info/dependency_links.txt +0 -0
  31. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/pytrilogy.egg-info/entry_points.txt +0 -0
  32. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/pytrilogy.egg-info/requires.txt +0 -0
  33. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/pytrilogy.egg-info/top_level.txt +0 -0
  34. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/setup.cfg +0 -0
  35. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/setup.py +0 -0
  36. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_datatypes.py +0 -0
  37. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_declarations.py +0 -0
  38. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_derived_concepts.py +0 -0
  39. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_enums.py +0 -0
  40. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_environment.py +0 -0
  41. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_execute_models.py +0 -0
  42. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_executor.py +0 -0
  43. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_failure.py +0 -0
  44. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_functions.py +0 -0
  45. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_imports.py +0 -0
  46. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_metadata.py +0 -0
  47. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_models.py +0 -0
  48. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_multi_join_assignments.py +0 -0
  49. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_parse_engine.py +0 -0
  50. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_parsing_failures.py +0 -0
  51. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_partial_handling.py +0 -0
  52. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_query_processing.py +0 -0
  53. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_query_render.py +0 -0
  54. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_select.py +0 -0
  55. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_show.py +0 -0
  56. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_statements.py +0 -0
  57. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_typing.py +0 -0
  58. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_undefined_concept.py +0 -0
  59. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_user_functions.py +0 -0
  60. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/tests/test_where_clause.py +0 -0
  61. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/authoring/__init__.py +0 -0
  62. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/__init__.py +0 -0
  63. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/constants.py +0 -0
  64. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/enums.py +0 -0
  65. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/env_processor.py +0 -0
  66. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/environment_helpers.py +0 -0
  67. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/ergonomics.py +0 -0
  68. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/graph_models.py +0 -0
  69. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/internal.py +0 -0
  70. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/__init__.py +0 -0
  71. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/author.py +0 -0
  72. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/build.py +0 -0
  73. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/build_environment.py +0 -0
  74. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/datasource.py +0 -0
  75. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/models/environment.py +0 -0
  76. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/optimization.py +0 -0
  77. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/optimizations/__init__.py +0 -0
  78. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/optimizations/base_optimization.py +0 -0
  79. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/optimizations/inline_datasource.py +0 -0
  80. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  81. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/__init__.py +0 -0
  82. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/discovery_node_factory.py +0 -0
  83. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/discovery_validation.py +0 -0
  84. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/graph_utils.py +0 -0
  85. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/__init__.py +0 -0
  86. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/common.py +0 -0
  87. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/constant_node.py +0 -0
  88. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  89. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  90. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  91. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  92. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  93. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  94. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  95. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  96. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/select_node.py +0 -0
  97. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  98. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/union_node.py +0 -0
  99. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/node_generators/window_node.py +0 -0
  100. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/__init__.py +0 -0
  101. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/base_node.py +0 -0
  102. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/filter_node.py +0 -0
  103. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  104. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/union_node.py +0 -0
  105. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  106. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/nodes/window_node.py +0 -0
  107. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/processing/utility.py +0 -0
  108. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/query_processor.py +0 -0
  109. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/statements/__init__.py +0 -0
  110. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/statements/author.py +0 -0
  111. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/statements/build.py +0 -0
  112. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/statements/common.py +0 -0
  113. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/statements/execute.py +0 -0
  114. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/utility.py +0 -0
  115. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/validation/__init__.py +0 -0
  116. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/validation/common.py +0 -0
  117. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/validation/concept.py +0 -0
  118. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/validation/environment.py +0 -0
  119. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/core/validation/fix.py +0 -0
  120. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/__init__.py +0 -0
  121. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/bigquery.py +0 -0
  122. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/common.py +0 -0
  123. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/config.py +0 -0
  124. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/dataframe.py +0 -0
  125. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/duckdb.py +0 -0
  126. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/enums.py +0 -0
  127. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/metadata.py +0 -0
  128. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/postgres.py +0 -0
  129. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/presto.py +0 -0
  130. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/snowflake.py +0 -0
  131. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/dialect/sql_server.py +0 -0
  132. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/engine.py +0 -0
  133. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/executor.py +0 -0
  134. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/hooks/__init__.py +0 -0
  135. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/hooks/base_hook.py +0 -0
  136. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/hooks/graph_hook.py +0 -0
  137. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/hooks/query_debugger.py +0 -0
  138. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/metadata/__init__.py +0 -0
  139. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parser.py +0 -0
  140. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/__init__.py +0 -0
  141. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/common.py +0 -0
  142. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/config.py +0 -0
  143. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/exceptions.py +0 -0
  144. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/helpers.py +0 -0
  145. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/parsing/parse_engine.py +0 -0
  146. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/py.typed +0 -0
  147. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/render.py +0 -0
  148. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/scripts/__init__.py +0 -0
  149. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/scripts/trilogy.py +0 -0
  150. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/__init__.py +0 -0
  151. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/date.preql +0 -0
  152. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/display.preql +0 -0
  153. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/geography.preql +0 -0
  154. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/metric.preql +0 -0
  155. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/money.preql +0 -0
  156. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/net.preql +0 -0
  157. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/ranking.preql +0 -0
  158. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/std/report.preql +0 -0
  159. {pytrilogy-0.0.3.99 → pytrilogy-0.0.3.101}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.99
3
+ Version: 0.0.3.101
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -123,15 +123,12 @@ Versus SQL, Trilogy aims to:
123
123
  **Improve:**
124
124
  - Simplicity
125
125
  - Refactoring/maintainability
126
- - Reusability
126
+ - Reusability/composability
127
+ - Expressivness
127
128
 
128
129
  **Maintain:**
129
130
  - Acceptable performance
130
131
 
131
- **Remove:**
132
- - Lower-level procedural features
133
- - Transactional optimizations/non-analytics features
134
-
135
132
  ## Backend Support
136
133
 
137
134
  | Backend | Status | Notes |
@@ -161,6 +158,7 @@ property sentence_id.word_three string::word; # a description to it
161
158
  # comments in other places are just comments
162
159
 
163
160
  # define our datasource to bind the model to data
161
+ # for most work, you can import something already defined
164
162
  # testing using query fixtures is a common pattern
165
163
  datasource word_one(
166
164
  sentence: sentence_id,
@@ -323,7 +321,7 @@ from pytrilogy import Executor, Dialect
323
321
 
324
322
  ### Authoring Imports
325
323
 
326
- Are also stable, and should be used for cases which programatically generate Trilogy statements without a base text format
324
+ Are also stable, and should be used for cases which programatically generate Trilogy statements without text inputs
327
325
  or need to process/transform parsed code in more complicated ways.
328
326
 
329
327
  ```python
@@ -391,6 +389,16 @@ datasource <name>(
391
389
  )
392
390
  grain(<concept>, <concept>)
393
391
  address <table>;
392
+
393
+ datasource orders(
394
+ order_id,
395
+ order_date,
396
+ total_rev: point_of_sale_rev,
397
+ customomer_id: customer.id
398
+ )
399
+ grain orders
400
+ address orders;
401
+
394
402
  ```
395
403
 
396
404
  ### Queries
@@ -85,15 +85,12 @@ Versus SQL, Trilogy aims to:
85
85
  **Improve:**
86
86
  - Simplicity
87
87
  - Refactoring/maintainability
88
- - Reusability
88
+ - Reusability/composability
89
+ - Expressivness
89
90
 
90
91
  **Maintain:**
91
92
  - Acceptable performance
92
93
 
93
- **Remove:**
94
- - Lower-level procedural features
95
- - Transactional optimizations/non-analytics features
96
-
97
94
  ## Backend Support
98
95
 
99
96
  | Backend | Status | Notes |
@@ -123,6 +120,7 @@ property sentence_id.word_three string::word; # a description to it
123
120
  # comments in other places are just comments
124
121
 
125
122
  # define our datasource to bind the model to data
123
+ # for most work, you can import something already defined
126
124
  # testing using query fixtures is a common pattern
127
125
  datasource word_one(
128
126
  sentence: sentence_id,
@@ -285,7 +283,7 @@ from pytrilogy import Executor, Dialect
285
283
 
286
284
  ### Authoring Imports
287
285
 
288
- Are also stable, and should be used for cases which programatically generate Trilogy statements without a base text format
286
+ Are also stable, and should be used for cases which programatically generate Trilogy statements without text inputs
289
287
  or need to process/transform parsed code in more complicated ways.
290
288
 
291
289
  ```python
@@ -353,6 +351,16 @@ datasource <name>(
353
351
  )
354
352
  grain(<concept>, <concept>)
355
353
  address <table>;
354
+
355
+ datasource orders(
356
+ order_id,
357
+ order_date,
358
+ total_rev: point_of_sale_rev,
359
+ customomer_id: customer.id
360
+ )
361
+ grain orders
362
+ address orders;
363
+
356
364
  ```
357
365
 
358
366
  ### Queries
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.99
3
+ Version: 0.0.3.101
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -123,15 +123,12 @@ Versus SQL, Trilogy aims to:
123
123
  **Improve:**
124
124
  - Simplicity
125
125
  - Refactoring/maintainability
126
- - Reusability
126
+ - Reusability/composability
127
+ - Expressivness
127
128
 
128
129
  **Maintain:**
129
130
  - Acceptable performance
130
131
 
131
- **Remove:**
132
- - Lower-level procedural features
133
- - Transactional optimizations/non-analytics features
134
-
135
132
  ## Backend Support
136
133
 
137
134
  | Backend | Status | Notes |
@@ -161,6 +158,7 @@ property sentence_id.word_three string::word; # a description to it
161
158
  # comments in other places are just comments
162
159
 
163
160
  # define our datasource to bind the model to data
161
+ # for most work, you can import something already defined
164
162
  # testing using query fixtures is a common pattern
165
163
  datasource word_one(
166
164
  sentence: sentence_id,
@@ -323,7 +321,7 @@ from pytrilogy import Executor, Dialect
323
321
 
324
322
  ### Authoring Imports
325
323
 
326
- Are also stable, and should be used for cases which programatically generate Trilogy statements without a base text format
324
+ Are also stable, and should be used for cases which programatically generate Trilogy statements without text inputs
327
325
  or need to process/transform parsed code in more complicated ways.
328
326
 
329
327
  ```python
@@ -391,6 +389,16 @@ datasource <name>(
391
389
  )
392
390
  grain(<concept>, <concept>)
393
391
  address <table>;
392
+
393
+ datasource orders(
394
+ order_id,
395
+ order_date,
396
+ total_rev: point_of_sale_rev,
397
+ customomer_id: customer.id
398
+ )
399
+ grain orders
400
+ address orders;
401
+
394
402
  ```
395
403
 
396
404
  ### Queries
@@ -34,6 +34,7 @@ tests/test_statements.py
34
34
  tests/test_typing.py
35
35
  tests/test_undefined_concept.py
36
36
  tests/test_user_functions.py
37
+ tests/test_validators.py
37
38
  tests/test_where_clause.py
38
39
  trilogy/__init__.py
39
40
  trilogy/constants.py
@@ -71,7 +72,6 @@ trilogy/core/optimizations/inline_datasource.py
71
72
  trilogy/core/optimizations/predicate_pushdown.py
72
73
  trilogy/core/processing/__init__.py
73
74
  trilogy/core/processing/concept_strategies_v3.py
74
- trilogy/core/processing/discovery_loop.py
75
75
  trilogy/core/processing/discovery_node_factory.py
76
76
  trilogy/core/processing/discovery_utility.py
77
77
  trilogy/core/processing/discovery_validation.py
@@ -2,11 +2,11 @@ from trilogy.core.constants import ALL_ROWS_CONCEPT, INTERNAL_NAMESPACE
2
2
  from trilogy.core.models.build import BuildGrain
3
3
  from trilogy.core.models.environment import Environment
4
4
  from trilogy.core.processing.concept_strategies_v3 import (
5
- GroupNode,
6
5
  History,
7
6
  search_concepts,
8
7
  )
9
8
  from trilogy.core.processing.node_generators import gen_group_node
9
+ from trilogy.core.processing.nodes import GroupNode
10
10
 
11
11
 
12
12
  def test_group_node(test_environment, test_environment_graph):
@@ -53,7 +53,7 @@ def test_group_node_property(test_environment: Environment, test_environment_gra
53
53
  }
54
54
  assert input_concept_names == {
55
55
  "category_name_length",
56
- "category_name",
56
+ # "category_name",
57
57
  "category_id",
58
58
  }
59
59
  final = group_node.resolve()
@@ -85,7 +85,7 @@ def test_group_node_property_all(test_environment: Environment, test_environment
85
85
  }
86
86
  assert input_concept_names == {
87
87
  "category_name_length",
88
- "category_name",
88
+ # "category_name",
89
89
  "category_id",
90
90
  }
91
91
  final = group_node.resolve()
@@ -552,7 +552,7 @@ const labels <- '';
552
552
 
553
553
  def test_struct_attr_access():
554
554
  text = """
555
- const labels <- struct(a->1, b->2, c->3);
555
+ const labels <- struct(1->a, 2->b, 3->c);
556
556
 
557
557
 
558
558
  select
@@ -0,0 +1,209 @@
1
+ from datetime import date, datetime
2
+ from decimal import Decimal
3
+
4
+ from trilogy.core.models.core import (
5
+ ArrayType,
6
+ DataType,
7
+ MapType,
8
+ NumericType,
9
+ StructType,
10
+ TraitDataType,
11
+ )
12
+ from trilogy.core.validation.datasource import type_check
13
+
14
+
15
+ def test_type_check():
16
+ # Basic string tests
17
+ assert type_check("hello", DataType.STRING)
18
+ assert not type_check(123, DataType.STRING)
19
+ assert type_check("", DataType.STRING) # empty string
20
+
21
+ # Integer tests
22
+ assert type_check(123, DataType.INTEGER)
23
+ assert type_check(0, DataType.INTEGER)
24
+ assert type_check(-123, DataType.INTEGER)
25
+ assert not type_check("123", DataType.INTEGER)
26
+ assert not type_check(123.0, DataType.INTEGER) # float should not match int
27
+
28
+ # BIGINT tests (same as INTEGER in implementation)
29
+ assert type_check(123, DataType.BIGINT)
30
+ assert type_check(2**63 - 1, DataType.BIGINT) # large integer
31
+ assert not type_check("123", DataType.BIGINT)
32
+ assert not type_check(123.0, DataType.BIGINT)
33
+
34
+ # Float tests
35
+ assert type_check(123.45, DataType.FLOAT)
36
+ assert type_check(123, DataType.FLOAT) # int should match float
37
+ assert type_check(0.0, DataType.FLOAT)
38
+ assert type_check(-123.45, DataType.FLOAT)
39
+ assert not type_check("123.45", DataType.FLOAT)
40
+
41
+ # Decimal support in float
42
+ decimal_val = Decimal("123.45")
43
+ assert type_check(decimal_val, DataType.FLOAT)
44
+
45
+ # NumericType tests
46
+ numeric_type = NumericType() # Assuming NumericType can be instantiated
47
+ assert type_check(123.45, numeric_type)
48
+ assert type_check(123, numeric_type)
49
+ assert type_check(decimal_val, numeric_type)
50
+ assert not type_check("123", numeric_type)
51
+
52
+ # NUMBER and NUMERIC tests (both handle int, float, Decimal)
53
+ assert type_check(123, DataType.NUMBER)
54
+ assert type_check(123.45, DataType.NUMBER)
55
+ assert type_check(decimal_val, DataType.NUMBER)
56
+ assert not type_check("123", DataType.NUMBER)
57
+
58
+ assert type_check(123, DataType.NUMERIC)
59
+ assert type_check(123.45, DataType.NUMERIC)
60
+ assert type_check(decimal_val, DataType.NUMERIC)
61
+ assert not type_check("123", DataType.NUMERIC)
62
+
63
+ # Boolean tests
64
+ assert type_check(True, DataType.BOOL)
65
+ assert type_check(False, DataType.BOOL)
66
+ assert not type_check(1, DataType.BOOL) # int should not match bool
67
+ assert not type_check(0, DataType.BOOL)
68
+ assert not type_check("true", DataType.BOOL)
69
+
70
+ # Date tests
71
+ test_date = date(2023, 12, 25)
72
+ assert type_check(test_date, DataType.DATE)
73
+ assert not type_check("2023-12-25", DataType.DATE)
74
+ assert not type_check(
75
+ datetime.now(), DataType.DATE
76
+ ) # datetime should not match date
77
+
78
+ # DateTime and Timestamp tests
79
+ test_datetime = datetime(2023, 12, 25, 15, 30, 45)
80
+ assert type_check(test_datetime, DataType.DATETIME)
81
+ assert type_check(test_datetime, DataType.TIMESTAMP)
82
+ assert not type_check("2023-12-25 15:30:45", DataType.DATETIME)
83
+ assert not type_check(
84
+ test_date, DataType.DATETIME
85
+ ) # date should not match datetime
86
+
87
+ # Unix seconds tests
88
+ assert type_check(1640995200, DataType.UNIX_SECONDS) # int timestamp
89
+ assert type_check(1640995200.123, DataType.UNIX_SECONDS) # float timestamp
90
+ assert not type_check("1640995200", DataType.UNIX_SECONDS)
91
+
92
+ # Date part tests
93
+ assert type_check("year", DataType.DATE_PART)
94
+ assert type_check("month", DataType.DATE_PART)
95
+ assert type_check("day", DataType.DATE_PART)
96
+ assert not type_check(2023, DataType.DATE_PART)
97
+
98
+ # Array tests
99
+ assert type_check([1, 2, 3], DataType.ARRAY)
100
+ assert type_check([], DataType.ARRAY) # empty array
101
+ assert type_check(["a", "b"], DataType.ARRAY)
102
+ assert not type_check("not a list", DataType.ARRAY)
103
+ assert not type_check({"key": "value"}, DataType.ARRAY)
104
+
105
+ # ArrayType tests
106
+ array_type = ArrayType(type=DataType.INTEGER)
107
+ assert type_check([1, 2, 3], array_type)
108
+ assert not type_check("not a list", array_type)
109
+
110
+ # Map tests
111
+ assert type_check({"key": "value"}, DataType.MAP)
112
+ assert type_check({}, DataType.MAP) # empty dict
113
+ assert type_check({"a": 1, "b": 2}, DataType.MAP)
114
+ assert not type_check([1, 2, 3], DataType.MAP)
115
+ assert not type_check("not a dict", DataType.MAP)
116
+
117
+ # MapType tests
118
+ map_type = MapType(key_type=DataType.STRING, value_type=DataType.STRING)
119
+ assert type_check({"key": "value"}, map_type)
120
+ assert not type_check([1, 2, 3], map_type)
121
+
122
+ # Struct tests
123
+ assert type_check({"field1": "value1"}, DataType.STRUCT)
124
+ assert type_check({}, DataType.STRUCT)
125
+ assert not type_check([1, 2, 3], DataType.STRUCT)
126
+
127
+ # StructType tests
128
+ struct_type = StructType(
129
+ fields=[DataType.STRING, DataType.STRING],
130
+ fields_map={"field1": DataType.STRING, "field2": DataType.STRING},
131
+ )
132
+ assert not type_check("not a dict", struct_type)
133
+
134
+ # NULL tests
135
+ assert type_check(None, DataType.NULL)
136
+ assert not type_check("", DataType.NULL)
137
+ assert not type_check(0, DataType.NULL)
138
+ assert not type_check(False, DataType.NULL)
139
+
140
+ # UNKNOWN tests (should accept anything)
141
+ assert type_check("anything", DataType.UNKNOWN)
142
+ assert type_check(123, DataType.UNKNOWN)
143
+ assert type_check(None, DataType.UNKNOWN)
144
+ assert type_check([1, 2, 3], DataType.UNKNOWN)
145
+ assert type_check({"key": "value"}, DataType.UNKNOWN)
146
+
147
+ # Nullable tests
148
+ assert type_check(None, DataType.STRING) # nullable by default
149
+ assert type_check(None, DataType.INTEGER)
150
+ assert type_check(None, DataType.FLOAT)
151
+ assert type_check(None, DataType.BOOL)
152
+
153
+ # Non-nullable tests
154
+ assert not type_check(None, DataType.STRING, nullable=False)
155
+ assert not type_check(None, DataType.INTEGER, nullable=False)
156
+ assert not type_check(None, DataType.FLOAT, nullable=False)
157
+ assert not type_check(None, DataType.BOOL, nullable=False)
158
+
159
+ # TraitDataType tests (recursive handling)
160
+ # Assuming TraitDataType wraps another DataType
161
+ trait_string_type = TraitDataType(type=DataType.STRING, traits=[])
162
+ assert type_check("hello", trait_string_type)
163
+ assert not type_check(123, trait_string_type)
164
+ assert type_check(None, trait_string_type) # nullable by default
165
+ assert not type_check(None, trait_string_type, nullable=False)
166
+
167
+ # Nested TraitDataType
168
+ nested_trait_type = TraitDataType(type=DataType.STRING, traits=[])
169
+ assert type_check("hello", nested_trait_type)
170
+ assert not type_check(123, nested_trait_type)
171
+
172
+ # Edge cases and invalid types
173
+ # Test with unsupported/custom types should return False
174
+ class CustomType:
175
+ pass
176
+
177
+ custom_obj = CustomType()
178
+ # These should all return False as they don't match any known type
179
+ assert not type_check(custom_obj, DataType.STRING)
180
+ assert not type_check(custom_obj, DataType.INTEGER)
181
+ assert not type_check(custom_obj, DataType.ARRAY)
182
+
183
+
184
+ def test_type_check_edge_cases():
185
+ """Additional edge case tests"""
186
+
187
+ # Test very large numbers
188
+ large_int = 2**100
189
+ assert type_check(large_int, DataType.INTEGER)
190
+ assert type_check(large_int, DataType.BIGINT)
191
+ assert type_check(large_int, DataType.FLOAT) # int should match float
192
+
193
+ # Test special float values
194
+ assert type_check(float("inf"), DataType.FLOAT)
195
+ assert type_check(float("-inf"), DataType.FLOAT)
196
+ # Note: NaN might need special handling depending on requirements
197
+ # assert type_check(float('nan'), DataType.FLOAT)
198
+
199
+ # Test empty collections
200
+ assert type_check([], DataType.ARRAY)
201
+ assert type_check({}, DataType.MAP)
202
+ assert type_check({}, DataType.STRUCT)
203
+
204
+ # Test nested collections
205
+ nested_list = [[1, 2], [3, 4]]
206
+ nested_dict = {"outer": {"inner": "value"}}
207
+ assert type_check(nested_list, DataType.ARRAY)
208
+ assert type_check(nested_dict, DataType.MAP)
209
+ assert type_check(nested_dict, DataType.STRUCT)
@@ -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.99"
7
+ __version__ = "0.0.3.101"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -40,6 +40,7 @@ class Comments:
40
40
  joins: bool = True
41
41
  nullable: bool = True
42
42
  partial: bool = True
43
+ source_map: bool = False
43
44
 
44
45
 
45
46
  @dataclass
@@ -69,7 +69,7 @@ class DatasourceColumnBindingData:
69
69
  actual_modifiers: List[Modifier]
70
70
 
71
71
  def format_failure(self):
72
- return f"Concept {self.address} value '{self.value}' with type {self.value_modifiers} does not conform to expected type {str(self.actual_type)} with modifiers {self.actual_modifiers}"
72
+ return f"Concept {self.address} value '{self.value}' with type {self.value_type} and {self.value_modifiers} does not conform to expected type {str(self.actual_type)} with modifiers {self.actual_modifiers}"
73
73
 
74
74
  def is_modifier_issue(self) -> bool:
75
75
  return len(self.value_modifiers) > 0 and any(
@@ -18,6 +18,7 @@ from trilogy.core.models.author import (
18
18
  AggregateWrapper,
19
19
  Concept,
20
20
  ConceptRef,
21
+ Conditional,
21
22
  Function,
22
23
  Parenthetical,
23
24
  UndefinedConcept,
@@ -129,8 +130,8 @@ def validate_case_output(
129
130
  def create_struct_output(
130
131
  args: list[Any],
131
132
  ) -> StructType:
132
- zipped = dict(zip(args[::2], args[1::2]))
133
- types = [arg_to_datatype(x) for x in args[1::2]]
133
+ zipped = dict(zip(args[1::2], args[::2]))
134
+ types = [arg_to_datatype(x) for x in args[::2]]
134
135
  return StructType(fields=types, fields_map=zipped)
135
136
 
136
137
 
@@ -997,6 +998,8 @@ def argument_to_purpose(arg) -> Purpose:
997
998
  return argument_to_purpose(arg.content)
998
999
  elif isinstance(arg, WindowItem):
999
1000
  return Purpose.PROPERTY
1001
+ elif isinstance(arg, Conditional):
1002
+ return Purpose.PROPERTY
1000
1003
  elif isinstance(arg, Concept):
1001
1004
  base = arg.purpose
1002
1005
  if (
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC
4
4
  from collections import UserDict, UserList
5
5
  from datetime import date, datetime
6
+ from decimal import Decimal
6
7
  from enum import Enum
7
8
  from typing import (
8
9
  Any,
@@ -448,6 +449,8 @@ def arg_to_datatype(arg) -> CONCRETE_TYPES:
448
449
  return DataType.STRING
449
450
  elif isinstance(arg, float):
450
451
  return DataType.FLOAT
452
+ elif isinstance(arg, Decimal):
453
+ return DataType.NUMERIC
451
454
  elif isinstance(arg, DataType):
452
455
  return arg
453
456
  elif isinstance(arg, NumericType):
@@ -118,19 +118,20 @@ class CTE(BaseModel):
118
118
  base += f" Source: {self.source.source_type}."
119
119
  if self.parent_ctes:
120
120
  base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
121
- if self.joins:
121
+ if self.joins and CONFIG.comments.joins:
122
122
  base += f"\n-- Joins: {', '.join([str(x) for x in self.joins])}."
123
- if self.partial_concepts:
123
+ if self.partial_concepts and CONFIG.comments.partial:
124
124
  base += (
125
125
  f"\n-- Partials: {', '.join([str(x) for x in self.partial_concepts])}."
126
126
  )
127
- base += f"\n-- Source Map: {self.source_map}."
127
+ if CONFIG.comments.source_map:
128
+ base += f"\n-- Source Map: {self.source_map}."
128
129
  base += f"\n-- Output: {', '.join([str(x) for x in self.output_columns])}."
129
130
  if self.source.input_concepts:
130
131
  base += f"\n-- Inputs: {', '.join([str(x) for x in self.source.input_concepts])}."
131
132
  if self.hidden_concepts:
132
133
  base += f"\n-- Hidden: {', '.join([str(x) for x in self.hidden_concepts])}."
133
- if self.nullable_concepts:
134
+ if self.nullable_concepts and CONFIG.comments.nullable:
134
135
  base += (
135
136
  f"\n-- Nullable: {', '.join([str(x) for x in self.nullable_concepts])}."
136
137
  )
@@ -368,6 +369,7 @@ class CTE(BaseModel):
368
369
  @property
369
370
  def group_concepts(self) -> List[BuildConcept]:
370
371
  def check_is_not_in_group(c: BuildConcept):
372
+
371
373
  if len(self.source_map.get(c.address, [])) > 0:
372
374
  return False
373
375
  if c.derivation == Derivation.ROWSET:
@@ -381,8 +383,6 @@ class CTE(BaseModel):
381
383
  and c.lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
382
384
  ):
383
385
  return True
384
- if c.purpose == Purpose.METRIC:
385
- return True
386
386
 
387
387
  if c.derivation == Derivation.BASIC and c.lineage:
388
388
  if all([check_is_not_in_group(x) for x in c.lineage.concept_arguments]):
@@ -392,6 +392,10 @@ class CTE(BaseModel):
392
392
  and c.lineage.operator == FunctionType.GROUP
393
393
  ):
394
394
  return check_is_not_in_group(c.lineage.concept_arguments[0])
395
+ return False
396
+ if c.purpose == Purpose.METRIC:
397
+ return True
398
+
395
399
  return False
396
400
 
397
401
  return (
@@ -707,6 +711,8 @@ class QueryDatasource(BaseModel):
707
711
  f" {[c.address for c in self.output_concepts]} concepts and"
708
712
  f" {other.name} with {[c.address for c in other.output_concepts]} concepts"
709
713
  )
714
+ logger.info(self.source_map)
715
+ logger.info(other.source_map)
710
716
 
711
717
  merged_datasources: dict[str, Union[BuildDatasource, "QueryDatasource"]] = {}
712
718
 
@@ -770,6 +776,7 @@ class QueryDatasource(BaseModel):
770
776
  logger.debug(
771
777
  f"[Query Datasource] merged with {[c.address for c in qds.output_concepts]} concepts"
772
778
  )
779
+ logger.debug(qds.source_map)
773
780
  return qds
774
781
 
775
782
  @property
@@ -777,7 +784,7 @@ class QueryDatasource(BaseModel):
777
784
  filters = abs(hash(str(self.condition))) if self.condition else ""
778
785
  grain = "_".join([str(c).replace(".", "_") for c in self.grain.components])
779
786
  group = ""
780
- if self.source_type == SourceType.GROUP:
787
+ if self.group_required:
781
788
  keys = [
782
789
  x.address for x in self.output_concepts if x.purpose != Purpose.METRIC
783
790
  ]
@@ -19,13 +19,13 @@ from trilogy.core.processing.discovery_utility import (
19
19
  LOGGER_PREFIX,
20
20
  depth_to_prefix,
21
21
  get_priority_concept,
22
+ group_if_required,
22
23
  )
23
24
  from trilogy.core.processing.discovery_validation import (
24
25
  ValidationResult,
25
26
  validate_stack,
26
27
  )
27
28
  from trilogy.core.processing.nodes import (
28
- GroupNode,
29
29
  History,
30
30
  MergeNode,
31
31
  StrategyNode,
@@ -218,6 +218,7 @@ def initialize_loop_context(
218
218
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} derived condition row inputs {[x.address for x in required_filters]} present in mandatory list, forcing condition evaluation at this level. "
219
219
  )
220
220
  mandatory_list = completion_mandatory
221
+ all_mandatory = set(c.address for c in completion_mandatory)
221
222
  must_evaluate_condition_on_this_level_not_push_down = True
222
223
  else:
223
224
  logger.info(
@@ -263,7 +264,7 @@ def evaluate_loop_conditions(
263
264
  ]
264
265
  ) and not any(
265
266
  [
266
- x.derivation not in ROOT_DERIVATIONS
267
+ x.derivation not in ROOT_DERIVATIONS + [Derivation.BASIC]
267
268
  for x in context.mandatory_list
268
269
  if x.address not in context.conditions.row_arguments
269
270
  ]
@@ -282,7 +283,7 @@ def evaluate_loop_conditions(
282
283
  # to ensure filtering happens before something like a SUM
283
284
  if (
284
285
  context.conditions
285
- and priority_concept.derivation not in ROOT_DERIVATIONS
286
+ and priority_concept.derivation not in ROOT_DERIVATIONS + [Derivation.BASIC]
286
287
  and priority_concept.address not in context.conditions.row_arguments
287
288
  ):
288
289
  logger.info(
@@ -419,26 +420,9 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
419
420
  logger.info(
420
421
  f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Conditions {context.conditions} were injected, checking if we need a group to restore grain"
421
422
  )
422
-
423
- result = GroupNode.check_if_required(
424
- downstream_concepts=output.usable_outputs,
425
- parents=[output.resolve()],
426
- environment=context.environment,
427
- depth=context.depth,
423
+ return group_if_required(
424
+ output, context.original_mandatory, context.environment
428
425
  )
429
- if result.required:
430
- logger.info(
431
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Adding group node with outputs {[x.address for x in context.original_mandatory]}"
432
- )
433
- return GroupNode(
434
- output_concepts=context.original_mandatory,
435
- input_concepts=output.usable_outputs,
436
- environment=context.environment,
437
- parents=[output],
438
- partial_concepts=output.partial_concepts,
439
- preexisting_conditions=context.conditions.conditional,
440
- depth=context.depth,
441
- )
442
426
  return output
443
427
 
444
428
 
@@ -604,18 +588,4 @@ def source_query_concepts(
604
588
  logger.info(
605
589
  f"{depth_to_prefix(0)}{LOGGER_PREFIX} final concepts are {[x.address for x in final]}"
606
590
  )
607
- if GroupNode.check_if_required(
608
- downstream_concepts=final,
609
- parents=[root.resolve()],
610
- environment=environment,
611
- ).required:
612
- candidate: StrategyNode = GroupNode(
613
- output_concepts=final,
614
- input_concepts=final,
615
- environment=environment,
616
- parents=[root],
617
- partial_concepts=root.partial_concepts,
618
- )
619
- else:
620
- candidate = root
621
- return candidate
591
+ return group_if_required(root, output_concepts, environment)