pytrilogy 0.0.3.32__tar.gz → 0.0.3.34__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 (139) hide show
  1. {pytrilogy-0.0.3.32/pytrilogy.egg-info → pytrilogy-0.0.3.34}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_query_processing.py +3 -1
  4. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_user_functions.py +66 -3
  5. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/authoring/__init__.py +6 -0
  7. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/enums.py +1 -0
  8. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/author.py +19 -6
  9. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/build.py +1 -1
  10. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/build_environment.py +6 -13
  11. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/environment.py +2 -1
  12. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/execute.py +9 -1
  13. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/optimization.py +8 -0
  14. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/concept_strategies_v3.py +63 -3
  15. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/group_node.py +3 -1
  16. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/rowset_node.py +5 -1
  17. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_merge_node.py +2 -0
  18. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/group_node.py +58 -1
  19. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/merge_node.py +13 -1
  20. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/utility.py +11 -5
  21. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/statements/author.py +5 -0
  22. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/common.py +78 -29
  23. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/parse_engine.py +33 -2
  24. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/render.py +6 -0
  25. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/trilogy.lark +5 -1
  26. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/LICENSE.md +0 -0
  27. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/README.md +0 -0
  28. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/pyproject.toml +0 -0
  29. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/SOURCES.txt +0 -0
  30. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/dependency_links.txt +0 -0
  31. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/entry_points.txt +0 -0
  32. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/requires.txt +0 -0
  33. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/top_level.txt +0 -0
  34. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/setup.cfg +0 -0
  35. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/setup.py +0 -0
  36. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_datatypes.py +0 -0
  37. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_declarations.py +0 -0
  38. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_derived_concepts.py +0 -0
  39. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_discovery_nodes.py +0 -0
  40. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_enums.py +0 -0
  41. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_environment.py +0 -0
  42. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_executor.py +0 -0
  43. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_functions.py +0 -0
  44. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_imports.py +0 -0
  45. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_metadata.py +0 -0
  46. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_models.py +0 -0
  47. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_multi_join_assignments.py +0 -0
  48. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_parse_engine.py +0 -0
  49. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_parsing.py +0 -0
  50. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_partial_handling.py +0 -0
  51. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_query_render.py +0 -0
  52. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_select.py +0 -0
  53. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_show.py +0 -0
  54. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_statements.py +0 -0
  55. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_typing.py +0 -0
  56. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_undefined_concept.py +0 -0
  57. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/tests/test_where_clause.py +0 -0
  58. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/compiler.py +0 -0
  59. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/constants.py +0 -0
  60. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/__init__.py +0 -0
  61. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/constants.py +0 -0
  62. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/env_processor.py +0 -0
  63. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/environment_helpers.py +0 -0
  64. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/ergonomics.py +0 -0
  65. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/exceptions.py +0 -0
  66. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/functions.py +0 -0
  67. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/graph_models.py +0 -0
  68. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/internal.py +0 -0
  69. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/__init__.py +0 -0
  70. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/core.py +0 -0
  71. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/models/datasource.py +0 -0
  72. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/__init__.py +0 -0
  73. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/base_optimization.py +0 -0
  74. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/inline_constant.py +0 -0
  75. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/inline_datasource.py +0 -0
  76. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  77. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/__init__.py +0 -0
  78. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/graph_utils.py +0 -0
  79. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/__init__.py +0 -0
  80. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  81. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/common.py +0 -0
  82. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  83. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  84. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  85. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  86. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  87. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  88. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_node.py +0 -0
  89. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  90. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/union_node.py +0 -0
  91. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  92. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/window_node.py +0 -0
  93. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/__init__.py +0 -0
  94. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/base_node.py +0 -0
  95. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/filter_node.py +0 -0
  96. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  97. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/union_node.py +0 -0
  98. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  99. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/window_node.py +0 -0
  100. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/query_processor.py +0 -0
  101. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/statements/__init__.py +0 -0
  102. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/statements/build.py +0 -0
  103. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/statements/common.py +0 -0
  104. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/core/statements/execute.py +0 -0
  105. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/__init__.py +0 -0
  106. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/base.py +0 -0
  107. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/bigquery.py +0 -0
  108. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/common.py +0 -0
  109. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/config.py +0 -0
  110. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/dataframe.py +0 -0
  111. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/duckdb.py +0 -0
  112. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/enums.py +0 -0
  113. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/postgres.py +0 -0
  114. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/presto.py +0 -0
  115. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/snowflake.py +0 -0
  116. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/dialect/sql_server.py +0 -0
  117. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/engine.py +0 -0
  118. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/executor.py +0 -0
  119. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/hooks/__init__.py +0 -0
  120. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/hooks/base_hook.py +0 -0
  121. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/hooks/graph_hook.py +0 -0
  122. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/hooks/query_debugger.py +0 -0
  123. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/metadata/__init__.py +0 -0
  124. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parser.py +0 -0
  125. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/__init__.py +0 -0
  126. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/config.py +0 -0
  127. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/exceptions.py +0 -0
  128. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/parsing/helpers.py +0 -0
  129. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/py.typed +0 -0
  130. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/render.py +0 -0
  131. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/scripts/__init__.py +0 -0
  132. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/scripts/trilogy.py +0 -0
  133. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/std/__init__.py +0 -0
  134. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/std/dashboard.preql +0 -0
  135. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/std/date.preql +0 -0
  136. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/std/display.preql +0 -0
  137. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/std/geography.preql +0 -0
  138. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/std/money.preql +0 -0
  139. {pytrilogy-0.0.3.32 → pytrilogy-0.0.3.34}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.32
3
+ Version: 0.0.3.34
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.32
3
+ Version: 0.0.3.34
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -164,7 +164,9 @@ def test_query_aggregation(test_environment, test_environment_graph):
164
164
  select = SelectStatement(selection=[test_environment.concepts["total_revenue"]])
165
165
  datasource = get_query_datasources(environment=test_environment, statement=select)
166
166
 
167
- assert {datasource.identifier} == {"revenue_at_local_order_id_at_abstract"}
167
+ assert {datasource.identifier} == {
168
+ "revenue_at_local_order_id_grouped_by__at_abstract"
169
+ }
168
170
  check = datasource
169
171
  assert len(check.input_concepts) == 2
170
172
  assert check.input_concepts[0].name == "revenue"
@@ -119,9 +119,6 @@ select rowset.total;
119
119
 
120
120
 
121
121
  def test_user_function_case():
122
- from trilogy.hooks.query_debugger import DebuggingHook
123
-
124
- DebuggingHook()
125
122
  x = Dialects.DUCK_DB.default_executor()
126
123
 
127
124
  results = x.execute_query(
@@ -228,3 +225,69 @@ select
228
225
  )
229
226
  results = results.fetchall()
230
227
  assert results[0].quad_test == 16.414213562373096
228
+
229
+
230
+ def test_user_function_nesting():
231
+ x = Dialects.DUCK_DB.default_executor()
232
+
233
+ results = x.execute_query(
234
+ """
235
+ key x int;
236
+ key y int;
237
+ property x.price float;
238
+
239
+ datasource raw_data (
240
+ x: x,
241
+ price: price
242
+ )
243
+ grain (x)
244
+ query '''
245
+ select 1 as x, 2.0 as price
246
+ union all
247
+ select 2 as x, 3.0 as price
248
+ union all
249
+ select 10 as x, 5.0 as price
250
+ ''';
251
+
252
+ datasource join_x_y (
253
+ x: x,
254
+ y: y)
255
+ grain (x, y)
256
+ query '''
257
+ select 1 as x, 1 as y
258
+ union all
259
+ select 2 as x, 1 as y
260
+ union all
261
+ select 10 as x, 2 as y
262
+ ''';
263
+
264
+ def weekday_sales(weekday) ->
265
+ SUM(CASE WHEN 10 = weekday THEN x ELSE 0 END) +
266
+ SUM(CASE WHEN 10 = weekday THEN price ELSE 0.0 END)
267
+ ;
268
+
269
+ def plus_two(a) -> a + 2;
270
+
271
+ # auto random_no_f <- SUM(CASE WHEN 10 = weekday THEN x ELSE 0 END) + SUM(CASE WHEN 10 = weekday THEN price ELSE 0.0 END) +2;
272
+ auto random_one_f <- @weekday_sales(10) +2;
273
+ auto random <- @plus_two(@weekday_sales(10));
274
+
275
+ """
276
+ )
277
+ # assert x.environment.concepts['random_no_f'].purpose == Purpose.METRIC, x.environment.concepts['random']
278
+ assert (
279
+ x.environment.concepts["random_one_f"].purpose == Purpose.METRIC
280
+ ), x.environment.concepts["random"]
281
+ assert (
282
+ x.environment.concepts["random"].purpose == Purpose.METRIC
283
+ ), x.environment.concepts["random"]
284
+
285
+ results = x.execute_query(
286
+ """select
287
+ y,
288
+ @plus_two(@weekday_sales(10)) -> test2
289
+ order by y asc;"""
290
+ )
291
+ results = results.fetchall()
292
+ assert results[0].test2 == 10
293
+ assert results[1].test2 == 17
@@ -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.32"
7
+ __version__ = "0.0.3.34"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -19,12 +19,15 @@ from trilogy.core.models.author import (
19
19
  Conditional,
20
20
  FilterItem,
21
21
  Function,
22
+ FunctionCallWrapper,
22
23
  HavingClause,
23
24
  MagicConstants,
24
25
  Metadata,
26
+ MultiSelectLineage,
25
27
  OrderBy,
26
28
  OrderItem,
27
29
  Parenthetical,
30
+ RowsetItem,
28
31
  SubselectComparison,
29
32
  WhereClause,
30
33
  WindowItem,
@@ -103,4 +106,7 @@ __all__ = [
103
106
  "RawSQLStatement",
104
107
  "Datasource",
105
108
  "DatasourceMetadata",
109
+ "MultiSelectLineage",
110
+ "RowsetItem",
111
+ "FunctionCallWrapper",
106
112
  ]
@@ -27,6 +27,7 @@ class Purpose(Enum):
27
27
  CONSTANT = "const"
28
28
  KEY = "key"
29
29
  PROPERTY = "property"
30
+ UNIQUE_PROPERTY = "unique_property"
30
31
  METRIC = "metric"
31
32
  ROWSET = "rowset"
32
33
  AUTO = "auto"
@@ -1089,7 +1089,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1089
1089
  pseudonyms=self.pseudonyms,
1090
1090
  )
1091
1091
 
1092
- @property
1092
+ @cached_property
1093
1093
  def sources(self) -> List["ConceptRef"]:
1094
1094
  if self.lineage:
1095
1095
  output: List[ConceptRef] = []
@@ -1326,7 +1326,7 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1326
1326
 
1327
1327
  class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1328
1328
  type: WindowType
1329
- content: ConceptRef
1329
+ content: FuncArgs
1330
1330
  order_by: List["OrderItem"]
1331
1331
  over: List["ConceptRef"] = Field(default_factory=list)
1332
1332
  index: Optional[int] = None
@@ -1335,7 +1335,7 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1335
1335
  return self.__repr__()
1336
1336
 
1337
1337
  def __repr__(self):
1338
- return f"{self.type}({self.content} {self.index}, {self.over}, {self.order_by})"
1338
+ return f"{self.type.value} {self.content} by {self.index} over {self.over} order {self.order_by}"
1339
1339
 
1340
1340
  @field_validator("content", mode="before")
1341
1341
  def enforce_concept_ref(cls, v):
@@ -1358,7 +1358,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1358
1358
  ) -> "WindowItem":
1359
1359
  output = WindowItem.model_construct(
1360
1360
  type=self.type,
1361
- content=self.content.with_merge(source, target, modifiers),
1361
+ content=(
1362
+ self.content.with_merge(source, target, modifiers)
1363
+ if isinstance(self.content, Mergeable)
1364
+ else self.content
1365
+ ),
1362
1366
  over=[x.with_merge(source, target, modifiers) for x in self.over],
1363
1367
  order_by=[x.with_merge(source, target, modifiers) for x in self.order_by],
1364
1368
  index=self.index,
@@ -1379,7 +1383,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1379
1383
  def with_namespace(self, namespace: str) -> "WindowItem":
1380
1384
  return WindowItem.model_construct(
1381
1385
  type=self.type,
1382
- content=self.content.with_namespace(namespace),
1386
+ content=(
1387
+ self.content.with_namespace(namespace)
1388
+ if isinstance(self.content, Namespaced)
1389
+ else self.content
1390
+ ),
1383
1391
  over=[x.with_namespace(namespace) for x in self.over],
1384
1392
  order_by=[x.with_namespace(namespace) for x in self.order_by],
1385
1393
  index=self.index,
@@ -1387,7 +1395,8 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1387
1395
 
1388
1396
  @property
1389
1397
  def concept_arguments(self) -> List[ConceptRef]:
1390
- output = [self.content]
1398
+ output = []
1399
+ output += get_concept_arguments(self.content)
1391
1400
  for order in self.order_by:
1392
1401
  output += get_concept_arguments(order)
1393
1402
  for item in self.over:
@@ -1787,6 +1796,10 @@ class FunctionCallWrapper(
1787
1796
  ],
1788
1797
  )
1789
1798
 
1799
+ def with_reference_replacement(self, source, target):
1800
+ raise NotImplementedError("Cannot reference replace")
1801
+ return self
1802
+
1790
1803
  def with_merge(
1791
1804
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1792
1805
  ) -> "FunctionCallWrapper":
@@ -1646,7 +1646,7 @@ class Factory:
1646
1646
 
1647
1647
  @build.register
1648
1648
  def _(self, base: WindowItem) -> BuildWindowItem:
1649
-
1649
+ # to do proper discovery, we need to inject virtual intermediate ocncepts
1650
1650
  return BuildWindowItem.model_construct(
1651
1651
  type=base.type,
1652
1652
  content=self.build(base.content),
@@ -31,19 +31,12 @@ class BuildEnvironmentConceptDict(dict):
31
31
  def raise_undefined(
32
32
  self, key: str, line_no: int | None = None, file: Path | str | None = None
33
33
  ) -> Never:
34
-
35
- matches = self._find_similar_concepts(key)
36
- message = f"Undefined concept: {key}."
37
- if matches:
38
- message += f" Suggestions: {matches}"
39
-
40
- if line_no:
41
- if file:
42
- raise UndefinedConceptException(
43
- f"{file}: {line_no}: " + message, matches
44
- )
45
- raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
46
- raise UndefinedConceptException(message, matches)
34
+ # build environment should never check for missing values.
35
+ if line_no is not None:
36
+ message = f"Concept '{key}' not found in environment at line {line_no}."
37
+ else:
38
+ message = f"Concept '{key}' not found in environment."
39
+ raise UndefinedConceptException(message, [])
47
40
 
48
41
  def __getitem__(
49
42
  self, key: str, line_no: int | None = None, file: Path | None = None
@@ -686,7 +686,8 @@ class Environment(BaseModel):
686
686
  replacements[k] = target
687
687
  # we need to update keys and grains of all concepts
688
688
  else:
689
- replacements[k] = v.with_merge(source, target, modifiers)
689
+ if source.address in v.sources or source.address in v.grain.components:
690
+ replacements[k] = v.with_merge(source, target, modifiers)
690
691
  self.concepts.update(replacements)
691
692
  for k, ds in self.datasources.items():
692
693
  if source.address in ds.output_lcl:
@@ -282,6 +282,7 @@ class CTE(BaseModel):
282
282
  **self.existence_source_map,
283
283
  **other.existence_source_map,
284
284
  }
285
+
285
286
  return self
286
287
 
287
288
  @property
@@ -637,7 +638,7 @@ class QueryDatasource(BaseModel):
637
638
  and CONFIG.validate_missing
638
639
  ):
639
640
  raise SyntaxError(
640
- f"On query datasource missing source map for {concept.address} on {key}, have {v}"
641
+ f"On query datasource missing source map for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
641
642
  )
642
643
  return v
643
644
 
@@ -764,8 +765,15 @@ class QueryDatasource(BaseModel):
764
765
  def identifier(self) -> str:
765
766
  filters = abs(hash(str(self.condition))) if self.condition else ""
766
767
  grain = "_".join([str(c).replace(".", "_") for c in self.grain.components])
768
+ group = ""
769
+ if self.source_type == SourceType.GROUP:
770
+ keys = [
771
+ x.address for x in self.output_concepts if x.purpose != Purpose.METRIC
772
+ ]
773
+ group = "_grouped_by_" + "_".join(keys)
767
774
  return (
768
775
  "_join_".join([d.identifier for d in self.datasources])
776
+ + group
769
777
  + (f"_at_{grain}" if grain else "_at_abstract")
770
778
  + (f"_filtered_by_{filters}" if filters else "")
771
779
  )
@@ -94,6 +94,11 @@ def filter_irrelevant_ctes(
94
94
  inverse_map = gen_inverse_map(input)
95
95
  recurse(root_cte, inverse_map)
96
96
  final = [cte for cte in input if cte.name in relevant_ctes]
97
+ filtered = [cte for cte in input if cte.name not in relevant_ctes]
98
+ if filtered:
99
+ logger.info(
100
+ f"[Optimization][Irrelevent CTE filtering] Removing redundant CTEs {[x.name for x in filtered]}"
101
+ )
97
102
  if len(final) == len(input):
98
103
  return input
99
104
  return filter_irrelevant_ctes(final, root_cte)
@@ -130,6 +135,9 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
130
135
  if not output_addresses.issubset(parent_output_addresses):
131
136
  return None
132
137
  if not direct_parent.grain == cte.grain:
138
+ logger.info("grain mismatch, cannot early exit")
139
+ logger.info(direct_parent.grain)
140
+ logger.info(cte.grain)
133
141
  return None
134
142
 
135
143
  assert isinstance(cte, CTE)
@@ -625,6 +625,7 @@ def validate_stack(
625
625
  for concept in resolved.output_concepts:
626
626
  if concept.address in resolved.hidden_concepts:
627
627
  continue
628
+
628
629
  validate_concept(
629
630
  concept,
630
631
  node,
@@ -773,6 +774,8 @@ def _search_concepts(
773
774
  ) -> StrategyNode | None:
774
775
  # these are the concepts we need in the output projection
775
776
  mandatory_list = unique(mandatory_list, "address")
777
+ # cache our values before an filter injection
778
+ original_mandatory = [*mandatory_list]
776
779
  for x in mandatory_list:
777
780
  if isinstance(x, UndefinedConcept):
778
781
  raise SyntaxError(f"Undefined concept {x.address}")
@@ -857,7 +860,7 @@ def _search_concepts(
857
860
  and priority_concept.address not in conditions.row_arguments
858
861
  ):
859
862
  logger.info(
860
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Force including conditions to push filtering above complex condition that is not condition member or parent"
863
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Force including conditions in {priority_concept.address} to push filtering above complex condition that is not condition member or parent"
861
864
  )
862
865
  local_conditions = conditions
863
866
 
@@ -921,7 +924,7 @@ def _search_concepts(
921
924
  mandatory_completion = [c.address for c in completion_mandatory]
922
925
  logger.info(
923
926
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} finished concept loop for {priority_concept} {priority_concept.derivation} condition {conditions} flag for accepting partial addresses is"
924
- f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
927
+ f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} synonyms partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
925
928
  )
926
929
  if complete == ValidationResult.INCOMPLETE_CONDITION:
927
930
  cond_dict = {str(node): node.preexisting_conditions for node in stack}
@@ -933,7 +936,23 @@ def _search_concepts(
933
936
  if complete == ValidationResult.COMPLETE and (
934
937
  not accept_partial or (accept_partial and not partial)
935
938
  ):
939
+ logger.info(
940
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} breaking loop, complete"
941
+ )
936
942
  break
943
+ elif complete == ValidationResult.COMPLETE and accept_partial and partial:
944
+ if len(attempted) == len(mandatory_list):
945
+ logger.info(
946
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Breaking as we have attempted all nodes"
947
+ )
948
+ break
949
+ logger.info(
950
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {attempted} all {len(mandatory_list)}"
951
+ )
952
+ else:
953
+ logger.info(
954
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Not complete, continuing search"
955
+ )
937
956
  # if we have attempted on root node, we've tried them all.
938
957
  # inject in another search with filter concepts
939
958
  if priority_concept.derivation == Derivation.ROOT:
@@ -948,6 +967,11 @@ def _search_concepts(
948
967
  if complete == ValidationResult.COMPLETE:
949
968
  condition_required = True
950
969
  non_virtual = [c for c in completion_mandatory if c.address not in virtual]
970
+ non_virtual_output = [c for c in original_mandatory if c.address not in virtual]
971
+ non_virtual_different = len(completion_mandatory) != len(original_mandatory)
972
+ non_virtual_difference_values = set(
973
+ [x.address for x in completion_mandatory]
974
+ ).difference(set([x.address for x in original_mandatory]))
951
975
  if not conditions:
952
976
  condition_required = False
953
977
  non_virtual = [c for c in mandatory_list if c.address not in virtual]
@@ -966,7 +990,19 @@ def _search_concepts(
966
990
  )
967
991
  if len(stack) == 1:
968
992
  output: StrategyNode = stack[0]
969
- # _ = restrict_node_outputs_targets(output, mandatory_list, depth)
993
+ if non_virtual_different:
994
+ logger.info(
995
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values"
996
+ )
997
+ output.set_output_concepts(
998
+ [
999
+ x
1000
+ for x in output.output_concepts
1001
+ if x.address in non_virtual_output
1002
+ ],
1003
+ rebuild=False,
1004
+ )
1005
+
970
1006
  logger.info(
971
1007
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)}"
972
1008
  )
@@ -995,6 +1031,30 @@ def _search_concepts(
995
1031
  logger.info(
996
1032
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
997
1033
  )
1034
+ if condition_required and conditions and non_virtual_different:
1035
+ logger.info(
1036
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Conditions {conditions} were injected, checking if we need a group to restore grain"
1037
+ )
1038
+ result = GroupNode.check_if_required(
1039
+ downstream_concepts=original_mandatory,
1040
+ parents=[output.resolve()],
1041
+ environment=environment,
1042
+ depth=depth,
1043
+ )
1044
+ logger.info(f"gcheck result is {result}")
1045
+ if result.required:
1046
+ logger.info(
1047
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Adding group node"
1048
+ )
1049
+ return GroupNode(
1050
+ output_concepts=original_mandatory,
1051
+ input_concepts=original_mandatory,
1052
+ environment=environment,
1053
+ parents=[output],
1054
+ partial_concepts=output.partial_concepts,
1055
+ preexisting_conditions=conditions.conditional,
1056
+ depth=depth,
1057
+ )
998
1058
  return output
999
1059
 
1000
1060
  # if we can't find it after expanding to a merge, then
@@ -149,7 +149,9 @@ def gen_group_node(
149
149
  g=g,
150
150
  depth=depth,
151
151
  source_concepts=source_concepts,
152
- log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
152
+ log_lambda=create_log_lambda(
153
+ LOGGER_PREFIX + f" for {concept.address}", depth, logger
154
+ ),
153
155
  history=history,
154
156
  conditions=conditions,
155
157
  )
@@ -117,7 +117,11 @@ def gen_rowset_node(
117
117
  f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]}"
118
118
  )
119
119
  if not local_optional or all(
120
- x.address in node.output_concepts and x.address not in node.partial_concepts
120
+ (
121
+ x.address in node.output_concepts
122
+ or (z in x.pseudonyms for z in node.output_concepts)
123
+ )
124
+ and x.address not in node.partial_concepts
121
125
  for x in local_optional
122
126
  ):
123
127
  logger.info(
@@ -344,12 +344,14 @@ def create_datasource_node(
344
344
  for c in datasource.columns
345
345
  if not c.is_complete and c.concept.address in all_concepts
346
346
  ]
347
+
347
348
  partial_lcl = LooseBuildConceptList(concepts=partial_concepts)
348
349
  nullable_concepts = [
349
350
  c.concept
350
351
  for c in datasource.columns
351
352
  if c.is_nullable and c.concept.address in all_concepts
352
353
  ]
354
+
353
355
  nullable_lcl = LooseBuildConceptList(concepts=nullable_concepts)
354
356
  partial_is_full = conditions and (conditions == datasource.non_partial_for)
355
357
 
@@ -2,7 +2,7 @@ from dataclasses import dataclass
2
2
  from typing import List, Optional
3
3
 
4
4
  from trilogy.constants import logger
5
- from trilogy.core.enums import SourceType
5
+ from trilogy.core.enums import Purpose, SourceType
6
6
  from trilogy.core.models.build import (
7
7
  BuildComparison,
8
8
  BuildConcept,
@@ -78,7 +78,9 @@ class GroupNode(StrategyNode):
78
78
  downstream_concepts: List[BuildConcept],
79
79
  parents: list[QueryDatasource | BuildDatasource],
80
80
  environment: BuildEnvironment,
81
+ depth: int = 0,
81
82
  ) -> GroupRequiredResponse:
83
+ padding = "\t" * depth
82
84
  target_grain = BuildGrain.from_concepts(
83
85
  downstream_concepts,
84
86
  environment=environment,
@@ -95,12 +97,67 @@ class GroupNode(StrategyNode):
95
97
  lookups: list[BuildConcept | str] = [
96
98
  concept_map[x] if x in concept_map else x for x in comp_grain.components
97
99
  ]
100
+
98
101
  comp_grain = BuildGrain.from_concepts(lookups, environment=environment)
102
+
99
103
  # dynamically select if we need to group
100
104
  # because sometimes, we are already at required grain
101
105
  if comp_grain.issubset(target_grain):
106
+
107
+ logger.info(
108
+ f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, is subset, no grain required"
109
+ )
102
110
  return GroupRequiredResponse(target_grain, comp_grain, False)
111
+ # find out what extra is in the comp grain vs target grain
112
+ difference = [
113
+ environment.concepts[c] for c in (comp_grain - target_grain).components
114
+ ]
115
+ logger.info(
116
+ f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, difference {difference}"
117
+ )
118
+
119
+ # if the difference is all unique properties whose keys are in the source grain
120
+ # we can also suppress the group
121
+ if all(
122
+ [
123
+ x.purpose == Purpose.UNIQUE_PROPERTY
124
+ and x.keys
125
+ and all(z in comp_grain.components for z in x.keys)
126
+ for x in difference
127
+ ]
128
+ ):
129
+ logger.info(
130
+ f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
131
+ )
132
+ return GroupRequiredResponse(target_grain, comp_grain, False)
133
+ if all([x.purpose == Purpose.KEY for x in difference]):
134
+ logger.info(
135
+ f"{padding}{LOGGER_PREFIX} checking if downstream is unique properties of key"
136
+ )
137
+ replaced_grain_raw: list[set[str]] = [
138
+ (
139
+ x.keys or set()
140
+ if x.purpose == Purpose.UNIQUE_PROPERTY
141
+ else set([x.address])
142
+ )
143
+ for x in downstream_concepts
144
+ if x.address in target_grain.components
145
+ ]
146
+ # flatten the list of lists
147
+ replaced_grain = [
148
+ item for sublist in replaced_grain_raw for item in sublist
149
+ ]
150
+ # if the replaced grain is a subset of the comp grain, we can skip the group
151
+ unique_grain_comp = BuildGrain.from_concepts(
152
+ replaced_grain, environment=environment
153
+ )
154
+ if comp_grain.issubset(unique_grain_comp):
155
+ logger.info(
156
+ f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
157
+ )
158
+ return GroupRequiredResponse(target_grain, comp_grain, False)
103
159
 
160
+ logger.info(f"{padding}{LOGGER_PREFIX} Group requirement check: group required")
104
161
  return GroupRequiredResponse(target_grain, comp_grain, True)
105
162
 
106
163
  def _resolve(self) -> QueryDatasource:
@@ -296,9 +296,21 @@ class MergeNode(StrategyNode):
296
296
  return dataset
297
297
 
298
298
  pregrain = BuildGrain()
299
+
299
300
  for source in final_datasets:
301
+ if all(
302
+ [x.address in self.existence_concepts for x in source.output_concepts]
303
+ ):
304
+ logger.info(
305
+ f"{self.logging_prefix}{LOGGER_PREFIX} skipping existence only source with {source.output_concepts} from grain accumulation"
306
+ )
307
+ continue
300
308
  pregrain += source.grain
301
309
 
310
+ pregrain = BuildGrain.from_concepts(
311
+ pregrain.components, environment=self.environment
312
+ )
313
+
302
314
  grain = self.grain if self.grain else pregrain
303
315
  logger.info(
304
316
  f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
@@ -310,6 +322,7 @@ class MergeNode(StrategyNode):
310
322
  )
311
323
  else:
312
324
  joins = []
325
+
313
326
  logger.info(
314
327
  f"{self.logging_prefix}{LOGGER_PREFIX} Final join count for CTE parent count {len(join_candidates)} is {len(joins)}"
315
328
  )
@@ -343,7 +356,6 @@ class MergeNode(StrategyNode):
343
356
  nullable_concepts = find_nullable_concepts(
344
357
  source_map=source_map, joins=joins, datasources=final_datasets
345
358
  )
346
-
347
359
  qds = QueryDatasource(
348
360
  input_concepts=unique(self.input_concepts, "address"),
349
361
  output_concepts=unique(self.output_concepts, "address"),