pytrilogy 0.3.330__tar.gz → 0.3.332__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.
Files changed (409) hide show
  1. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/PKG-INFO +1 -1
  2. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/__init__.py +1 -1
  3. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/authoring/__init__.py +2 -0
  4. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/execute.py +17 -1
  5. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/union_dim_pushdown.py +35 -5
  6. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +87 -29
  7. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/utility.py +47 -13
  8. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/strategy_builder.py +36 -2
  9. pytrilogy-0.3.332/trilogy/core/processing/v4_node_generators/basic.py +104 -0
  10. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/query_processor.py +101 -18
  11. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/statements/author.py +24 -1
  12. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/statements/execute.py +10 -0
  13. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/base.py +18 -0
  14. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/python_source.py +7 -0
  15. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/AGENTS.md +6 -0
  16. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/__init__.py +2 -0
  17. pytrilogy-0.3.332/trilogy/execution/state/isolation.py +39 -0
  18. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/partitions.py +19 -19
  19. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/state_store.py +10 -14
  20. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/watermarks.py +72 -30
  21. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/executor.py +104 -0
  22. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parser.py +2 -0
  23. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/parse_engine_v2.py +4 -3
  24. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/render.py +79 -1
  25. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/trilogy.lark +6 -0
  26. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/hydration.py +24 -1
  27. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/chart_rules.py +9 -2
  28. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/operational_rules.py +53 -0
  29. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/statement_planner.py +3 -0
  30. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/statement_plans.py +32 -1
  31. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/syntax.py +2 -0
  32. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/altair_renderer.py +17 -9
  33. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_info_docs/content.py +19 -0
  34. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/cloud.py +634 -33
  35. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/cloud_models.py +37 -10
  36. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/common.py +18 -6
  37. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/Cargo.lock +1 -1
  38. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/Cargo.toml +1 -1
  39. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/trilogy.pest +3 -0
  40. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display.py +2 -0
  41. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_parallel.py +67 -39
  42. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/parallel_execution.py +11 -3
  43. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/refresh.py +1 -1
  44. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/single_execution.py +1 -1
  45. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/source.py +2 -5
  46. pytrilogy-0.3.330/trilogy/core/processing/v4_node_generators/basic.py +0 -53
  47. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/.scripts/__init__.py +0 -0
  48. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/.scripts/build_backend.py +0 -0
  49. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/.scripts/sync_version.py +0 -0
  50. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/LICENSE.md +0 -0
  51. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/README.md +0 -0
  52. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/pyproject.toml +0 -0
  53. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/README.md +0 -0
  54. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/__init__.py +0 -0
  55. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/constants.py +0 -0
  56. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/conversation.py +0 -0
  57. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/enums.py +0 -0
  58. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/execute.py +0 -0
  59. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/models.py +0 -0
  60. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/prompts.py +0 -0
  61. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/__init__.py +0 -0
  62. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/anthropic.py +0 -0
  63. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/base.py +0 -0
  64. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/deepseek.py +0 -0
  65. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/google.py +0 -0
  66. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/openai.py +0 -0
  67. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/openrouter.py +0 -0
  68. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/providers/utils.py +0 -0
  69. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/ai/syntax_examples.py +0 -0
  70. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/authoring/README.md +0 -0
  71. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/constants.py +0 -0
  72. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/README.md +0 -0
  73. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/__init__.py +0 -0
  74. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/constants.py +0 -0
  75. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/domain_graph.py +0 -0
  76. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/domain_validation.py +0 -0
  77. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/enums.py +0 -0
  78. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/env_processor.py +0 -0
  79. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/environment_helpers.py +0 -0
  80. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/ergonomics.py +0 -0
  81. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/exceptions.py +0 -0
  82. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/fingerprint.py +0 -0
  83. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/functions.py +0 -0
  84. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/graph.py +0 -0
  85. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/graph_models.py +0 -0
  86. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/having_normalization.py +0 -0
  87. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/internal.py +0 -0
  88. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/__init__.py +0 -0
  89. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/author.py +0 -0
  90. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/build.py +0 -0
  91. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/build_environment.py +0 -0
  92. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/core.py +0 -0
  93. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/datasource.py +0 -0
  94. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/models/environment.py +0 -0
  95. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimization.py +0 -0
  96. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/__init__.py +0 -0
  97. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/base_optimization.py +0 -0
  98. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/collapse_single_parent.py +0 -0
  99. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/filtered_aggregate.py +0 -0
  100. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/filtered_count_join.py +0 -0
  101. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/full_join_lowering.py +0 -0
  102. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/hide_unused_concept.py +0 -0
  103. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/inline_datasource.py +0 -0
  104. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/join_hoist.py +0 -0
  105. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/join_upgrade.py +0 -0
  106. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/keyless_full_join.py +0 -0
  107. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/merge_irrelevant_group_by.py +0 -0
  108. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/null_safe_join.py +0 -0
  109. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/order_inner_joins.py +0 -0
  110. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  111. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/semi_join_pushdown.py +0 -0
  112. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/strip_redundant_not_null.py +0 -0
  113. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/utils.py +0 -0
  114. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/optimizations/value_set_join_upgrade.py +0 -0
  115. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/README.md +0 -0
  116. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/VIRTUAL_UNNEST.md +0 -0
  117. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/__init__.py +0 -0
  118. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/aggregate_rollup.py +0 -0
  119. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/concept_strategies_v4.py +0 -0
  120. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/condition_utility.py +0 -0
  121. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/constants.py +0 -0
  122. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/discovery_utility.py +0 -0
  123. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/discovery_validation.py +0 -0
  124. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/grain_utility.py +0 -0
  125. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/join_resolution.py +0 -0
  126. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/model_ambiguity.py +0 -0
  127. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/__init__.py +0 -0
  128. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/common.py +0 -0
  129. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/presence_probe.py +0 -0
  130. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  131. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_helpers/condition_routing.py +0 -0
  132. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_helpers/datasource_nodes.py +0 -0
  133. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_helpers/source_scoring.py +0 -0
  134. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  135. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/node_generators/select_node.py +0 -0
  136. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/README.md +0 -0
  137. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/__init__.py +0 -0
  138. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/base_node.py +0 -0
  139. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/filter_node.py +0 -0
  140. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/group_node.py +0 -0
  141. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/merge_node.py +0 -0
  142. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  143. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  144. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/subselect_node.py +0 -0
  145. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/union_node.py +0 -0
  146. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  147. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/nodes/window_node.py +0 -0
  148. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/rowset_islanding.py +0 -0
  149. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/__init__.py +0 -0
  150. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/concept_graph.py +0 -0
  151. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/condition_injection.py +0 -0
  152. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/condition_placement.py +0 -0
  153. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/constants.py +0 -0
  154. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/edges.py +0 -0
  155. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/functional_dependency.py +0 -0
  156. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/group_behaviors.py +0 -0
  157. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/group_graph.py +0 -0
  158. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/group_rules.py +0 -0
  159. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/history.py +0 -0
  160. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/models.py +0 -0
  161. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/network_build.py +0 -0
  162. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/network_coalescing.py +0 -0
  163. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/network_model.py +0 -0
  164. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/network_obligations.py +0 -0
  165. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/network_search.py +0 -0
  166. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/network_topology.py +0 -0
  167. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/projection.py +0 -0
  168. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/source_planning.py +0 -0
  169. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_helper/staged_where.py +0 -0
  170. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/__init__.py +0 -0
  171. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/aggregate.py +0 -0
  172. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/common.py +0 -0
  173. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/condition_sources.py +0 -0
  174. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/constant.py +0 -0
  175. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/dispatch.py +0 -0
  176. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/filter.py +0 -0
  177. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/group_to.py +0 -0
  178. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/multiselect.py +0 -0
  179. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/nested_select.py +0 -0
  180. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/recursive.py +0 -0
  181. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/root.py +0 -0
  182. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/rowset.py +0 -0
  183. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/subselect.py +0 -0
  184. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/union.py +0 -0
  185. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/union_select.py +0 -0
  186. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/unnest.py +0 -0
  187. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/processing/v4_node_generators/window.py +0 -0
  188. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/scope_diagnostics.py +0 -0
  189. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/statements/README.md +0 -0
  190. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/statements/__init__.py +0 -0
  191. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/statements/build.py +0 -0
  192. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/statements/common.py +0 -0
  193. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/table_processor.py +0 -0
  194. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/utility.py +0 -0
  195. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/README.md +0 -0
  196. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/__init__.py +0 -0
  197. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/common.py +0 -0
  198. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/concept.py +0 -0
  199. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/datasource.py +0 -0
  200. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/environment.py +0 -0
  201. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/fix.py +0 -0
  202. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/validation/rows.py +0 -0
  203. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/core/where_scope_normalization.py +0 -0
  204. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/__init__.py +0 -0
  205. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/bigquery.py +0 -0
  206. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/bigquery_engine.py +0 -0
  207. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/bigquery_persist.py +0 -0
  208. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/bigquery_staging.py +0 -0
  209. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/cancel.py +0 -0
  210. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/clickhouse.py +0 -0
  211. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/clickhouse_chdb.py +0 -0
  212. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/common.py +0 -0
  213. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/config.py +0 -0
  214. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/dataframe.py +0 -0
  215. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/duckdb.py +0 -0
  216. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/duckdb_uv.py +0 -0
  217. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/enums.py +0 -0
  218. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/metadata.py +0 -0
  219. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/mock.py +0 -0
  220. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/mysql.py +0 -0
  221. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/postgres.py +0 -0
  222. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/presto.py +0 -0
  223. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/results.py +0 -0
  224. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/snowflake.py +0 -0
  225. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/source_pushdown.py +0 -0
  226. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/sql_server.py +0 -0
  227. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/dialect/sqlite.py +0 -0
  228. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/engine.py +0 -0
  229. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/__init__.py +0 -0
  230. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/config.py +0 -0
  231. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/envs.py +0 -0
  232. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/model_fingerprint.py +0 -0
  233. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/report.py +0 -0
  234. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/CLAUDE.md +0 -0
  235. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/cache.py +0 -0
  236. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/exceptions.py +0 -0
  237. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/persistence.py +0 -0
  238. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/phases.py +0 -0
  239. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/execution/state/snapshot.py +0 -0
  240. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/hooks/__init__.py +0 -0
  241. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/hooks/base_hook.py +0 -0
  242. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/hooks/graph_hook.py +0 -0
  243. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/hooks/query_debugger.py +0 -0
  244. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/__init__.py +0 -0
  245. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/adapters.py +0 -0
  246. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/arrow.py +0 -0
  247. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/click_support.py +0 -0
  248. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/contract.py +0 -0
  249. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/describe.py +0 -0
  250. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/errors.py +0 -0
  251. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/runner.py +0 -0
  252. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/io/sinks.py +0 -0
  253. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/metadata/__init__.py +0 -0
  254. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/README.md +0 -0
  255. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/__init__.py +0 -0
  256. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/common.py +0 -0
  257. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/config.py +0 -0
  258. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/exceptions.py +0 -0
  259. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/helpers.py +0 -0
  260. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/pretty.py +0 -0
  261. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/README.md +0 -0
  262. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/__init__.py +0 -0
  263. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/concept_factory.py +0 -0
  264. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/concept_syntax.py +0 -0
  265. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/errors.py +0 -0
  266. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/function_syntax.py +0 -0
  267. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/import_service.py +0 -0
  268. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/lark_backend.py +0 -0
  269. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/model.py +0 -0
  270. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/pest_backend.py +0 -0
  271. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rowset_semantics.py +0 -0
  272. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/__init__.py +0 -0
  273. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/concept_rules.py +0 -0
  274. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/conditional_rules.py +0 -0
  275. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/datasource_rules.py +0 -0
  276. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/expression_rules.py +0 -0
  277. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/function_definition_rules.py +0 -0
  278. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/function_rules.py +0 -0
  279. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/import_rules.py +0 -0
  280. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/merge_rules.py +0 -0
  281. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/multiselect_rules.py +0 -0
  282. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/order_rules.py +0 -0
  283. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/persist_rules.py +0 -0
  284. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/rawsql_rules.py +0 -0
  285. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/rowset_rules.py +0 -0
  286. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/select_rules.py +0 -0
  287. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/select_statement_rules.py +0 -0
  288. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/statement_rules.py +0 -0
  289. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/subselect_rules.py +0 -0
  290. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/token_rules.py +0 -0
  291. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/tvf_rules.py +0 -0
  292. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules/type_rules.py +0 -0
  293. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/rules_context.py +0 -0
  294. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/select_finalize.py +0 -0
  295. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/semantic_scope.py +0 -0
  296. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/semantic_state.py +0 -0
  297. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/parsing/v2/symbols.py +0 -0
  298. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/py.typed +0 -0
  299. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/render.py +0 -0
  300. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/__init__.py +0 -0
  301. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/base.py +0 -0
  302. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/chart_theme.py +0 -0
  303. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/rich_types.py +0 -0
  304. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/terminal_renderer.py +0 -0
  305. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/rendering/theme.py +0 -0
  306. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/__init__.py +0 -0
  307. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/backends/__init__.py +0 -0
  308. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/backends/base.py +0 -0
  309. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/backends/html.py +0 -0
  310. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/backends/png.py +0 -0
  311. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/charts.py +0 -0
  312. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/document.py +0 -0
  313. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/runner.py +0 -0
  314. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/report/tables.py +0 -0
  315. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/AGENTS.md +0 -0
  316. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/CLAUDE.md +0 -0
  317. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/README.md +0 -0
  318. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/__init__.py +0 -0
  319. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent.py +0 -0
  320. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_info.py +0 -0
  321. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_info_docs/__init__.py +0 -0
  322. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_info_docs/cli.py +0 -0
  323. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_info_docs/directory.py +0 -0
  324. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_sessions.py +0 -0
  325. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_sql_tools.py +0 -0
  326. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/agent_tools.py +0 -0
  327. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/click_utils.py +0 -0
  328. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/database.py +0 -0
  329. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/README.md +0 -0
  330. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/build.sh +0 -0
  331. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/examples/basic_usage.rs +0 -0
  332. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/directory_resolver.rs +0 -0
  333. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/graph.rs +0 -0
  334. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/lib.rs +0 -0
  335. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/main.rs +0 -0
  336. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/network_search.rs +0 -0
  337. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/parser.rs +0 -0
  338. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/python_bindings.rs +0 -0
  339. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/resolver.rs +0 -0
  340. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/src/trilogy_parser.rs +0 -0
  341. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/base.preql +0 -0
  342. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/cli_integration.rs +0 -0
  343. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/customer.preql +0 -0
  344. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/main.preql +0 -0
  345. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/orders.preql +0 -0
  346. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/public_api.rs +0 -0
  347. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/test_data/base.preql +0 -0
  348. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency/tests/test_data/consumer.preql +0 -0
  349. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/dependency.py +0 -0
  350. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_core.py +0 -0
  351. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_execution.py +0 -0
  352. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_ingest.py +0 -0
  353. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_init.py +0 -0
  354. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_models.py +0 -0
  355. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_refresh.py +0 -0
  356. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/display_validation.py +0 -0
  357. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/env_commands.py +0 -0
  358. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/environment.py +0 -0
  359. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/explore.py +0 -0
  360. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/explore_seen.py +0 -0
  361. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/file.py +0 -0
  362. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/file_helpers/__init__.py +0 -0
  363. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/file_helpers/backends.py +0 -0
  364. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/file_helpers/preql_description.py +0 -0
  365. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/file_helpers/preql_validation.py +0 -0
  366. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/fmt.py +0 -0
  367. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest.py +0 -0
  368. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest_helpers/__init__.py +0 -0
  369. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest_helpers/fk_inference.py +0 -0
  370. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest_helpers/foreign_keys.py +0 -0
  371. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest_helpers/formatting.py +0 -0
  372. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest_helpers/introspection.py +0 -0
  373. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/ingest_helpers/typing.py +0 -0
  374. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/init.py +0 -0
  375. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/plan.py +0 -0
  376. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/project_config.py +0 -0
  377. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/public.py +0 -0
  378. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/render.py +0 -0
  379. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/run.py +0 -0
  380. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve.py +0 -0
  381. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/__init__.py +0 -0
  382. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/connection_spec.py +0 -0
  383. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/file_discovery.py +0 -0
  384. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/index_generation.py +0 -0
  385. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/jobs.py +0 -0
  386. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/models.py +0 -0
  387. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/state_cache.py +0 -0
  388. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/state_computation.py +0 -0
  389. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/serve_helpers/studio_bundle.py +0 -0
  390. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/source_identity.py +0 -0
  391. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/state.py +0 -0
  392. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/testing.py +0 -0
  393. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/trilogy.py +0 -0
  394. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/scripts/validate_agent.py +0 -0
  395. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/staging.py +0 -0
  396. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/__init__.py +0 -0
  397. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/chart.preql +0 -0
  398. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/color.preql +0 -0
  399. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/currency.preql +0 -0
  400. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/date.preql +0 -0
  401. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/display.preql +0 -0
  402. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/geography.preql +0 -0
  403. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/metric.preql +0 -0
  404. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/money.preql +0 -0
  405. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/net.preql +0 -0
  406. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/ranking.preql +0 -0
  407. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/report.preql +0 -0
  408. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/std/semantic.preql +0 -0
  409. {pytrilogy-0.3.330 → pytrilogy-0.3.332}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.3.330
3
+ Version: 0.3.332
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
7
7
  from trilogy.executor import Executor
8
8
  from trilogy.parser import parse
9
9
 
10
- __version__ = "0.3.330"
10
+ __version__ = "0.3.332"
11
11
 
12
12
  __all__ = [
13
13
  "CONFIG",
@@ -55,6 +55,7 @@ from trilogy.core.models.datasource import Address, Datasource, DatasourceMetada
55
55
  from trilogy.core.models.environment import DictImportResolver, Environment
56
56
  from trilogy.core.statements.author import (
57
57
  STATEMENT_TYPES,
58
+ CallStatement,
58
59
  ConceptDeclarationStatement,
59
60
  ConceptTransform,
60
61
  CopyStatement,
@@ -89,6 +90,7 @@ __all__ = [
89
90
  "ArrayType",
90
91
  # trilogy.core.enums
91
92
  "BooleanOperator",
93
+ "CallStatement",
92
94
  "CaseElse",
93
95
  "CaseWhen",
94
96
  "Comment",
@@ -1830,11 +1830,27 @@ class UnionCTE:
1830
1830
 
1831
1831
  @property
1832
1832
  def group_to_grain(self) -> bool:
1833
+ """A union never dedupes itself — only its consumer can decide.
1834
+
1835
+ A UNION ALL whose projection drops its arms' grain keys does hold
1836
+ duplicate rows against its own declared grain, so this is NOT the
1837
+ "already a unique set" it looks like. But the duplicates are only noise
1838
+ to a consumer joining the union as a key set; to a consumer aggregating
1839
+ it they are the data. `select species, sum(dbh)` over two tree-grain
1840
+ partitions projects (species, dbh) and must keep all three of three
1841
+ identically-valued trees — deduping here silently under-counts the sum.
1842
+
1843
+ The union cannot tell those apart, so the obligation sits with whoever
1844
+ reads it at a coarser grain: that consumer must group. See
1845
+ `_grain_claim_needs_group` in v4_node_generators/basic.py, which is the
1846
+ projection-shaped case of exactly that.
1847
+ """
1833
1848
  return False
1834
1849
 
1835
1850
  @property
1836
1851
  def group_concepts(self) -> list[BuildConcept]:
1837
- # unions should always be on unique sets
1852
+ # Nothing to group by: see group_to_grain — deduping is the consumer's
1853
+ # obligation, never the union's.
1838
1854
  return []
1839
1855
 
1840
1856
  def __add__(self, other):
@@ -192,6 +192,23 @@ def _datasource_matches_raw_id(
192
192
  return isinstance(base, BuildDatasource) and base.identifier == source_id
193
193
 
194
194
 
195
+ def _derives_from(node: CTE | UnionCTE, container_name: str) -> bool:
196
+ """``node`` IS ``container_name`` or reads from it, at any depth."""
197
+ seen: set[str] = set()
198
+ stack: list[CTE | UnionCTE] = [node]
199
+ while stack:
200
+ current = stack.pop()
201
+ if current.name == container_name:
202
+ return True
203
+ if current.name in seen:
204
+ continue
205
+ seen.add(current.name)
206
+ stack.extend(current.dependency_nodes())
207
+ if isinstance(current, UnionCTE):
208
+ stack.extend(current.internal_ctes)
209
+ return False
210
+
211
+
195
212
  def _dim_local_atoms(
196
213
  cte: CTE,
197
214
  dim_qds: BuildDatasource | QueryDatasource,
@@ -604,12 +621,25 @@ class UnionDimPushdown(OptimizationRule):
604
621
  return needed.issubset(out)
605
622
 
606
623
  def _resolve_push_context(
607
- self, consumers: list[CTE], d: _DimDescriptor
624
+ self, consumers: list[CTE], d: _DimDescriptor, container: CTE | UnionCTE
608
625
  ) -> _PushContext | None:
609
626
  for c in consumers:
610
627
  found = _find_dim_cte_for_qds(c, d.join_qds_id)
611
- if found is not None and self._dim_cte_exposes(found, d):
612
- return _PushContext(dim_cte=found, source_consumer=c)
628
+ if found is None or not self._dim_cte_exposes(found, d):
629
+ continue
630
+ # A "dim" carved OUT of the container (a single-key projection of the
631
+ # union, which the planner peels when the union is the only source of
632
+ # that key) cannot be computed before it. Pushing it in would make the
633
+ # container's branches depend on a CTE that depends on the container —
634
+ # `reorder_ctes` then fails the whole query with "CTE dependency graph
635
+ # contains a cycle".
636
+ if _derives_from(found, container.name):
637
+ self.log(
638
+ f"Skipping dim {d.dim_qds.identifier}: its CTE {found.name} "
639
+ f"is derived from {container.name}"
640
+ )
641
+ continue
642
+ return _PushContext(dim_cte=found, source_consumer=c)
613
643
  return None
614
644
 
615
645
  def _can_push_into_branch(self, branch: CTE, d: _DimDescriptor) -> bool:
@@ -634,7 +664,7 @@ class UnionDimPushdown(OptimizationRule):
634
664
  if not d.strip_safe and not d.where_atoms:
635
665
  return False
636
666
 
637
- context = self._resolve_push_context(consumers, d)
667
+ context = self._resolve_push_context(consumers, d, union)
638
668
  if context is None:
639
669
  return False
640
670
 
@@ -707,7 +737,7 @@ class UnionDimPushdown(OptimizationRule):
707
737
  """
708
738
  if not d.strip_safe or not d.where_atoms:
709
739
  return False
710
- context = self._resolve_push_context(consumers, d)
740
+ context = self._resolve_push_context(consumers, d, target)
711
741
  if context is None:
712
742
  return False
713
743
  if not self._can_push_into_branch(target, d):
@@ -1,5 +1,4 @@
1
1
  from collections import defaultdict
2
- from itertools import product
3
2
 
4
3
  from trilogy.core.enums import (
5
4
  AddressType,
@@ -76,9 +75,12 @@ def _best_enum_union(
76
75
  ) -> list[list[BuildDatasource]] | None:
77
76
  """Find the best minimal covering combinations for an enum-partitioned key.
78
77
 
79
- Groups by covered enum value, enumerates one-source-per-value combos,
80
- computes the combo's actual concept overlap (minus the merge key), and
81
- keeps the highest-scoring combo per distinct overlap signature. Returning
78
+ Groups by covered enum value, then searches one-source-per-value combos via
79
+ a dynamic program over values whose state is the combo's concept overlap so
80
+ far (minus the merge key), keeping the highest-scoring combo per distinct
81
+ overlap signature. The score is separable (a per-source sum) and the
82
+ overlap is the only coupling between values, so this is exhaustive over
83
+ achievable signatures without enumerating the k^V combo product. Returning
82
84
  one combo per overlap lets parallel partitionings (e.g., sales vs.
83
85
  returns vs. dim, all keyed by the same channel enum) each contribute
84
86
  their own union datasource instead of collapsing into the single best.
@@ -100,36 +102,92 @@ def _best_enum_union(
100
102
  return None
101
103
 
102
104
  values = list(by_value.keys())
103
- merge_key_addr = {merge_key.address}
104
-
105
- best_per_overlap: dict[frozenset[str], tuple[list[BuildDatasource], int]] = {}
105
+ # A union requires at least 2 distinct sources; a single source is not a union
106
+ if len(values) < 2:
107
+ return None
106
108
 
107
- for combo in product(*[by_value[v] for v in values]):
108
- combo_list = list(combo)
109
+ cols: dict[int, frozenset[str]] = {}
110
+ scores: dict[int, int] = {}
111
+ for candidates in by_value.values():
112
+ for ds in candidates:
113
+ cols[id(ds)] = frozenset(col.concept.address for col in ds.columns)
114
+ scores[id(ds)] = _datasource_score(ds)
109
115
 
110
- # A union requires at least 2 distinct sources; a single source is not a union
111
- if len(combo_list) < 2:
116
+ # Members MAY disagree on intrinsic (~) partiality of a shared column
117
+ # (a "mixed-family" combo, e.g. web_sales + catalog/store_returns).
118
+ # Such a union is a legitimate per-channel provider of columns it binds
119
+ # complete (q05: a web return's return-site lives on web_sales), and
120
+ # union partial propagation keeps its ~-partial keys from ever outranking
121
+ # a pure family that binds them complete (q14) — so don't reject it here.
122
+ # An empty overlap beyond the merge key IS rejected, at the first step it
123
+ # appears: intersection only shrinks, so no continuation can revive it.
124
+ # Ties reproduce the old product enumeration exactly: per signature the
125
+ # winner is the first max-scoring combo in product order (`combo_key` =
126
+ # candidate index tuple, lexicographic = product order), and signatures
127
+ # order by their first-achieving combo (`min_key`, tracked over ALL combos
128
+ # reaching a signature, not just the best-scoring one).
129
+ merge_key_addr = merge_key.address
130
+ # signature -> (score, combo, combo_key, min_key)
131
+ states: dict[
132
+ frozenset[str],
133
+ tuple[int, list[BuildDatasource], tuple[int, ...], tuple[int, ...]],
134
+ ] = {}
135
+ for idx, ds in enumerate(by_value[values[0]]):
136
+ sig = cols[id(ds)] - {merge_key_addr}
137
+ if not sig:
112
138
  continue
139
+ existing = states.get(sig)
140
+ if existing is None:
141
+ states[sig] = (scores[id(ds)], [ds], (idx,), (idx,))
142
+ elif scores[id(ds)] > existing[0]:
143
+ states[sig] = (scores[id(ds)], [ds], (idx,), existing[3])
113
144
 
114
- # Require at least one shared concept beyond the merge key
115
- overlap = {col.concept.address for col in combo_list[0].columns}
116
- for ds in combo_list[1:]:
117
- overlap &= {col.concept.address for col in ds.columns}
118
- signature = frozenset(overlap - merge_key_addr)
119
- if not signature:
120
- continue
121
- # Members MAY disagree on intrinsic (~) partiality of a shared column
122
- # (a "mixed-family" combo, e.g. web_sales + catalog/store_returns).
123
- # Such a union is a legitimate per-channel provider of columns it binds
124
- # complete (q05: a web return's return-site lives on web_sales), and
125
- # union partial propagation keeps its ~-partial keys from ever outranking
126
- # a pure family that binds them complete (q14) — so don't reject it here.
127
-
128
- score = sum(_datasource_score(ds) for ds in combo_list)
129
- existing = best_per_overlap.get(signature)
130
- if existing is None or score > existing[1]:
131
- best_per_overlap[signature] = (combo_list, score)
145
+ for v in values[1:]:
146
+ next_states: dict[
147
+ frozenset[str],
148
+ tuple[int, list[BuildDatasource], tuple[int, ...], tuple[int, ...]],
149
+ ] = {}
150
+ for sig, (score, combo, combo_key, min_key) in states.items():
151
+ for idx, ds in enumerate(by_value[v]):
152
+ new_sig = sig & cols[id(ds)]
153
+ if not new_sig:
154
+ continue
155
+ new_score = score + scores[id(ds)]
156
+ new_key = combo_key + (idx,)
157
+ existing = next_states.get(new_sig)
158
+ if existing is None:
159
+ next_states[new_sig] = (
160
+ new_score,
161
+ combo + [ds],
162
+ new_key,
163
+ min_key + (idx,),
164
+ )
165
+ continue
166
+ new_min_key = min(existing[3], min_key + (idx,))
167
+ if new_score > existing[0] or (
168
+ new_score == existing[0] and new_key < existing[2]
169
+ ):
170
+ next_states[new_sig] = (
171
+ new_score,
172
+ combo + [ds],
173
+ new_key,
174
+ new_min_key,
175
+ )
176
+ else:
177
+ next_states[new_sig] = (
178
+ existing[0],
179
+ existing[1],
180
+ existing[2],
181
+ new_min_key,
182
+ )
183
+ states = next_states
184
+ if not states:
185
+ return None
132
186
 
187
+ best_per_overlap: dict[frozenset[str], tuple[list[BuildDatasource], int]] = {
188
+ sig: (state[1], state[0])
189
+ for sig, state in sorted(states.items(), key=lambda kv: kv[1][3])
190
+ }
133
191
  if not best_per_overlap:
134
192
  return None
135
193
  # Keep only maximal overlap signatures: drop a signature whose concept set
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Sequence
3
4
  from dataclasses import dataclass
4
5
  from enum import Enum
5
6
  from typing import TYPE_CHECKING
@@ -8,6 +9,7 @@ if TYPE_CHECKING:
8
9
  from trilogy.core import graph as nx
9
10
 
10
11
  from trilogy.core.enums import Derivation, Granularity, JoinType, Purpose
12
+ from trilogy.core.models.author import ConceptRef
11
13
  from trilogy.core.models.build import (
12
14
  BuildConcept,
13
15
  BuildDatasource,
@@ -210,6 +212,44 @@ def find_nullable_concepts(
210
212
  return sorted(final_nullable)
211
213
 
212
214
 
215
+ def _resolve_output_target(
216
+ cte: CTE | UnionCTE,
217
+ target: BuildConcept | ConceptRef,
218
+ scoped_merge_map: dict[str, str],
219
+ ) -> tuple[BuildConcept, bool] | None:
220
+ """The CTE column that renders `target`, and whether it must be re-aliased
221
+ to the written name. None when the CTE cannot render it at all."""
222
+ mapping = {x.address: x for x in cte.output_columns}
223
+ if target.address in mapping:
224
+ return mapping[target.address], False
225
+ for oc in cte.output_columns:
226
+ if target.address in oc.pseudonyms:
227
+ return oc, True
228
+ # an in-query JOIN collapses a source onto its canonical target; the
229
+ # target's column is present, render it under the written source name
230
+ canonical = scoped_merge_map.get(target.address)
231
+ if canonical is not None and canonical in mapping:
232
+ return mapping[canonical], True
233
+ return None
234
+
235
+
236
+ def unrenderable_outputs(
237
+ cte: CTE | UnionCTE,
238
+ targets: Sequence[BuildConcept | ConceptRef],
239
+ scoped_merge_map: dict[str, str] | None = None,
240
+ ) -> list[str]:
241
+ """Target addresses `sort_select_output` would silently omit from the
242
+ projection. A caller that cannot tolerate a short SELECT — a persist writes
243
+ positionally, so a missing column shifts every later one — asks here and
244
+ fails naming the column instead."""
245
+ merge_map = scoped_merge_map or {}
246
+ return [
247
+ target.address
248
+ for target in targets
249
+ if _resolve_output_target(cte, target, merge_map) is None
250
+ ]
251
+
252
+
213
253
  def sort_select_output_processed(
214
254
  cte: CTE | UnionCTE, query: SelectStatement | MultiSelectStatement | ProcessedQuery
215
255
  ) -> CTE | UnionCTE:
@@ -221,7 +261,6 @@ def sort_select_output_processed(
221
261
  hidden = query.hidden_components
222
262
 
223
263
  output_addresses = {c.address for c in targets}
224
- mapping = {x.address: x for x in cte.output_columns}
225
264
  scoped_merge_map: dict[str, str] = (
226
265
  query.scoped_merge_map if isinstance(query, ProcessedQuery) else {}
227
266
  )
@@ -243,19 +282,14 @@ def sort_select_output_processed(
243
282
 
244
283
  new_output: list[BuildConcept] = []
245
284
  for x in targets:
246
- if x.address in mapping:
247
- new_output.append(mapping[x.address])
285
+ # A target the CTE cannot render is dropped here; `unrenderable_outputs`
286
+ # is the same resolution, asked ahead of time by callers that must fail
287
+ # rather than emit a short projection.
288
+ resolved = _resolve_output_target(cte, x, scoped_merge_map)
289
+ if resolved is None:
248
290
  continue
249
- for oc in cte.output_columns:
250
- if x.address in oc.pseudonyms:
251
- new_output.append(render_as(x, oc))
252
- break
253
- else:
254
- # an in-query JOIN collapses a source onto its canonical target; the
255
- # target's column is present, render it under the written source name
256
- canonical = scoped_merge_map.get(x.address)
257
- if canonical is not None and canonical in mapping:
258
- new_output.append(render_as(x, mapping[canonical]))
291
+ source, rename = resolved
292
+ new_output.append(render_as(x, source) if rename else source)
259
293
 
260
294
  for oc in cte.output_columns:
261
295
  # add hidden back
@@ -2019,7 +2019,19 @@ def _satisfy_parent_projection_contract(
2019
2019
  if not any(_contains_shape_barrier(parent) for parent in parents):
2020
2020
  return parents
2021
2021
 
2022
+ # Two different questions, two different sets. "What is available TO this
2023
+ # parent" is its own parents' outputs (`parent_output_addresses`) — that is
2024
+ # what `parent_needed` asks, and a leaf scan's empty answer is what keeps
2025
+ # leaf scans out of this projection entirely (q66). "What does a SIBLING
2026
+ # supply to the merge" is that sibling's own projection: a WINDOW sibling
2027
+ # READS `event_time` and emits only `lag(event_time)`, so crediting it with
2028
+ # its inputs made this projection strip `event_time` off the dimension
2029
+ # parent — and the consumer computing `event_time - prior_event_time` then
2030
+ # had no source for it and was dropped from the plan outright.
2022
2031
  outputs_by_parent = [parent_output_addresses(parent) for parent in parents]
2032
+ own_outputs_by_parent = [
2033
+ {output.address for output in parent.usable_outputs} for parent in parents
2034
+ ]
2023
2035
  projected: list[StrategyNode] = []
2024
2036
  for idx, parent in enumerate(parents):
2025
2037
  if _contains_shape_barrier(parent):
@@ -2027,7 +2039,7 @@ def _satisfy_parent_projection_contract(
2027
2039
  continue
2028
2040
  parent_needed = outputs_by_parent[idx] & needed
2029
2041
  other_outputs = set().union(
2030
- *(outputs for j, outputs in enumerate(outputs_by_parent) if j != idx)
2042
+ *(outputs for j, outputs in enumerate(own_outputs_by_parent) if j != idx)
2031
2043
  )
2032
2044
  fd_candidates = {
2033
2045
  addr
@@ -2049,6 +2061,28 @@ def _satisfy_parent_projection_contract(
2049
2061
  if not concepts or non_fd_needed:
2050
2062
  projected.append(parent)
2051
2063
  continue
2064
+ # `parent_needed` is measured off the parent's INPUTS, so a value the
2065
+ # parent computes itself — a BASIC sibling's `revenue`, a date
2066
+ # projection — is invisible to it and the projection below would strip
2067
+ # it, leaving the consuming aggregate with no source for that output
2068
+ # (sibling aggregates where one reads a derived row value: the plan
2069
+ # computed `revenue` in a CTE and then never projected `total_revenue`).
2070
+ # Carry those through. Restricted to FD-at-grain so the projection's
2071
+ # row count is unchanged, and to what no sibling parent already
2072
+ # supplies, so this never re-shapes a plain dimension re-join.
2073
+ carry = {
2074
+ output.address
2075
+ for output in parent.usable_outputs
2076
+ if output.address in needed
2077
+ and output.address not in parent_needed
2078
+ and output.address not in other_outputs
2079
+ and _fd_at_grain(output, projection_grain_components)
2080
+ }
2081
+ concepts.extend(
2082
+ c
2083
+ for addr in sorted(carry)
2084
+ if (c := _concept_at(environment, addr)) is not None
2085
+ )
2052
2086
  # When the dimension's projected grain (fd_needed) shares NO key with the
2053
2087
  # barrier sibling, the post-projection merge has nothing to join on and
2054
2088
  # cross-joins ON 1=1 — the bridge between the two is a projection-grain key
@@ -2079,7 +2113,7 @@ def _satisfy_parent_projection_contract(
2079
2113
  parent_outputs = {o.address for o in parent.usable_outputs}
2080
2114
  grain_concepts = [
2081
2115
  c
2082
- for addr in sorted((fd_needed | join_keys) & parent_outputs)
2116
+ for addr in sorted((fd_needed | join_keys | carry) & parent_outputs)
2083
2117
  if (c := _concept_at(environment, addr)) is not None
2084
2118
  ]
2085
2119
  projected.append(
@@ -0,0 +1,104 @@
1
+ from trilogy.core.models.build import BuildConcept, BuildGrain, BuildWhereClause
2
+ from trilogy.core.models.build_environment import BuildEnvironment
3
+ from trilogy.core.processing.nodes import (
4
+ ConstantNode,
5
+ MergeNode,
6
+ SelectNode,
7
+ StrategyNode,
8
+ )
9
+ from trilogy.core.processing.v4_helper.functional_dependency import (
10
+ build_fd_determines,
11
+ )
12
+
13
+ from .common import outputs_with_parent_grain_keys, parent_outputs_needed
14
+
15
+
16
+ def _grain_claim_needs_group(
17
+ outputs: list[BuildConcept],
18
+ parent: StrategyNode,
19
+ environment: BuildEnvironment,
20
+ ) -> bool:
21
+ """True when this projection can only make its grain claim true by grouping.
22
+
23
+ A projection's grain is inferred from what it projects, but a plain SELECT
24
+ emits one row per PARENT row. Project `species` and `upper(species)` off a
25
+ `tree_id`-grain union and the node advertises grain `species` while emitting
26
+ one row per tree; a consumer then joins it on `species` and fans the other
27
+ side out — silently, since the SQL is valid.
28
+
29
+ Two things can make the claim true, and only the second is ours to do here:
30
+
31
+ - The claim is already true. Either the parent's grain keys survive in the
32
+ projection, or the projected grain functionally determines them (the
33
+ parent holds at most one row per it, so dropping them loses nothing).
34
+ - The projection can be widened back to the parent's grain. That is
35
+ ``outputs_with_parent_grain_keys``'s job, and it needs the parent to
36
+ actually expose those keys.
37
+
38
+ When the parent does NOT expose its own grain there is nothing to widen with,
39
+ and grouping is the only repair left. The outputs are functionally determined
40
+ by the grouped set, so the dedupe cannot drop a row — it removes exactly the
41
+ duplicates the parent's finer grain introduced.
42
+
43
+ Returns SelectNode's own default (False) when it does not fire -- passing
44
+ None instead would flip every other basic projection onto ``_resolve``'s
45
+ grain-less branch, which is a plan change for nodes this has no business
46
+ touching.
47
+ """
48
+ parent_grain = set(parent.resolve().grain.components)
49
+ if not parent_grain or parent_grain.issubset({c.address for c in outputs}):
50
+ return False
51
+ if parent_grain.issubset({c.address for c in parent.output_concepts}):
52
+ return False
53
+ claimed = BuildGrain.from_concepts(outputs).components
54
+ return not all(
55
+ component in claimed
56
+ or build_fd_determines(
57
+ environment, claimed, component, include_empty_grain=False
58
+ )
59
+ for component in parent_grain
60
+ )
61
+
62
+
63
+ def gen_basic(
64
+ outputs: list[BuildConcept],
65
+ parents: list[StrategyNode],
66
+ environment: BuildEnvironment,
67
+ conditions: BuildWhereClause | None = None,
68
+ preexisting_conditions: BuildWhereClause | None = None,
69
+ ) -> StrategyNode | None:
70
+ """Projection of derived basic expressions over already-built parents.
71
+
72
+ Zero parents → ConstantNode. One parent → SelectNode (just projection).
73
+ Multiple parents → MergeNode, which auto-joins on shared output concepts
74
+ (typically the common grain). A SelectNode here would render with no
75
+ join and emit `INVALID_REFERENCE_BUG_<...>` for the unjoined parent."""
76
+ pre = preexisting_conditions.conditional if preexisting_conditions else None
77
+ if not parents:
78
+ return ConstantNode(
79
+ input_concepts=[],
80
+ output_concepts=outputs,
81
+ environment=environment,
82
+ conditions=conditions.conditional if conditions else None,
83
+ preexisting_conditions=pre,
84
+ )
85
+ full_outputs = outputs_with_parent_grain_keys(outputs, parents)
86
+ inputs = parent_outputs_needed(full_outputs, parents, conditions)
87
+ if len(parents) == 1:
88
+ return SelectNode(
89
+ input_concepts=inputs,
90
+ output_concepts=full_outputs,
91
+ environment=environment,
92
+ parents=parents,
93
+ conditions=conditions.conditional if conditions else None,
94
+ preexisting_conditions=pre,
95
+ force_group=_grain_claim_needs_group(full_outputs, parents[0], environment),
96
+ )
97
+ return MergeNode(
98
+ input_concepts=inputs,
99
+ output_concepts=full_outputs,
100
+ environment=environment,
101
+ parents=parents,
102
+ conditions=conditions.conditional if conditions else None,
103
+ preexisting_conditions=pre,
104
+ )