pytrilogy 0.0.3.31__tar.gz → 0.0.3.33__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.31/pytrilogy.egg-info → pytrilogy-0.0.3.33}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_environment.py +57 -0
  4. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_user_functions.py +66 -3
  5. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/enums.py +1 -0
  7. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/environment_helpers.py +25 -1
  8. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/author.py +25 -7
  9. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/build.py +1 -1
  10. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/environment.py +1 -1
  11. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/execute.py +1 -1
  12. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/optimization.py +8 -0
  13. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/concept_strategies_v3.py +46 -2
  14. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/group_node.py +3 -1
  15. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/rowset_node.py +5 -1
  16. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/group_node.py +58 -1
  17. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/merge_node.py +13 -1
  18. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/duckdb.py +1 -0
  19. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/common.py +78 -29
  20. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/parse_engine.py +9 -2
  21. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/trilogy.lark +2 -1
  22. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/LICENSE.md +0 -0
  23. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/README.md +0 -0
  24. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/pyproject.toml +0 -0
  25. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/pytrilogy.egg-info/SOURCES.txt +0 -0
  26. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/pytrilogy.egg-info/dependency_links.txt +0 -0
  27. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/pytrilogy.egg-info/entry_points.txt +0 -0
  28. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/pytrilogy.egg-info/requires.txt +0 -0
  29. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/pytrilogy.egg-info/top_level.txt +0 -0
  30. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/setup.cfg +0 -0
  31. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/setup.py +0 -0
  32. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_datatypes.py +0 -0
  33. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_declarations.py +0 -0
  34. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_derived_concepts.py +0 -0
  35. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_discovery_nodes.py +0 -0
  36. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_enums.py +0 -0
  37. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_executor.py +0 -0
  38. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_functions.py +0 -0
  39. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_imports.py +0 -0
  40. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_metadata.py +0 -0
  41. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_models.py +0 -0
  42. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_multi_join_assignments.py +0 -0
  43. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_parse_engine.py +0 -0
  44. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_parsing.py +0 -0
  45. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_partial_handling.py +0 -0
  46. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_query_processing.py +0 -0
  47. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_query_render.py +0 -0
  48. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_select.py +0 -0
  49. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_show.py +0 -0
  50. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_statements.py +0 -0
  51. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_typing.py +0 -0
  52. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_undefined_concept.py +0 -0
  53. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/tests/test_where_clause.py +0 -0
  54. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/authoring/__init__.py +0 -0
  55. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/compiler.py +0 -0
  56. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/constants.py +0 -0
  57. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/__init__.py +0 -0
  58. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/constants.py +0 -0
  59. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/env_processor.py +0 -0
  60. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/ergonomics.py +0 -0
  61. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/exceptions.py +0 -0
  62. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/functions.py +0 -0
  63. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/graph_models.py +0 -0
  64. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/internal.py +0 -0
  65. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/__init__.py +0 -0
  66. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/build_environment.py +0 -0
  67. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/core.py +0 -0
  68. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/models/datasource.py +0 -0
  69. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/optimizations/__init__.py +0 -0
  70. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/optimizations/base_optimization.py +0 -0
  71. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/optimizations/inline_constant.py +0 -0
  72. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/optimizations/inline_datasource.py +0 -0
  73. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  74. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/__init__.py +0 -0
  75. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/graph_utils.py +0 -0
  76. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/__init__.py +0 -0
  77. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  78. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/common.py +0 -0
  79. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  80. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  81. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  82. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  83. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  84. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  85. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  86. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/select_node.py +0 -0
  87. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  88. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/union_node.py +0 -0
  89. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  90. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/node_generators/window_node.py +0 -0
  91. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/__init__.py +0 -0
  92. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/base_node.py +0 -0
  93. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/filter_node.py +0 -0
  94. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  95. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/union_node.py +0 -0
  96. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  97. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/nodes/window_node.py +0 -0
  98. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/processing/utility.py +0 -0
  99. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/query_processor.py +0 -0
  100. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/statements/__init__.py +0 -0
  101. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/statements/author.py +0 -0
  102. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/statements/build.py +0 -0
  103. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/statements/common.py +0 -0
  104. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/core/statements/execute.py +0 -0
  105. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/__init__.py +0 -0
  106. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/base.py +0 -0
  107. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/bigquery.py +0 -0
  108. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/common.py +0 -0
  109. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/config.py +0 -0
  110. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/dataframe.py +0 -0
  111. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/enums.py +0 -0
  112. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/postgres.py +0 -0
  113. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/presto.py +0 -0
  114. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/snowflake.py +0 -0
  115. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/dialect/sql_server.py +0 -0
  116. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/engine.py +0 -0
  117. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/executor.py +0 -0
  118. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/hooks/__init__.py +0 -0
  119. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/hooks/base_hook.py +0 -0
  120. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/hooks/graph_hook.py +0 -0
  121. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/hooks/query_debugger.py +0 -0
  122. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/metadata/__init__.py +0 -0
  123. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parser.py +0 -0
  124. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/__init__.py +0 -0
  125. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/config.py +0 -0
  126. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/exceptions.py +0 -0
  127. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/helpers.py +0 -0
  128. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/parsing/render.py +0 -0
  129. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/py.typed +0 -0
  130. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/render.py +0 -0
  131. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/scripts/__init__.py +0 -0
  132. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/scripts/trilogy.py +0 -0
  133. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/std/__init__.py +0 -0
  134. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/std/dashboard.preql +0 -0
  135. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/std/date.preql +0 -0
  136. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/std/display.preql +0 -0
  137. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/std/geography.preql +0 -0
  138. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/std/money.preql +0 -0
  139. {pytrilogy-0.0.3.31 → pytrilogy-0.0.3.33}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.31
3
+ Version: 0.0.3.33
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.31
3
+ Version: 0.0.3.33
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -13,6 +13,63 @@ from trilogy.core.models.core import DataType
13
13
  from trilogy.core.models.environment import Environment, LazyEnvironment
14
14
 
15
15
 
16
+ def test_concept_rehydration():
17
+ test = {
18
+ "name": "order_timestamp.month_start",
19
+ "datatype": "date",
20
+ "purpose": "property",
21
+ "derivation": "root",
22
+ "granularity": "multi_row",
23
+ "metadata": {
24
+ "description": "Auto-derived from a local.order_timestamp. The date truncated to the month.",
25
+ "line_number": None,
26
+ "concept_source": "auto_derived",
27
+ },
28
+ "lineage": {
29
+ "operator": "date_truncate",
30
+ "arg_count": 2,
31
+ "output_datatype": "date",
32
+ "output_purpose": "property",
33
+ "valid_inputs": [
34
+ ["datetime", "string", "timestamp", "date"],
35
+ ["date_part"],
36
+ ],
37
+ "arguments": [
38
+ {
39
+ "address": "local.order_timestamp",
40
+ "datatype": "timestamp",
41
+ "metadata": {
42
+ "description": None,
43
+ "line_number": None,
44
+ "concept_source": "manual",
45
+ },
46
+ },
47
+ "month",
48
+ ],
49
+ },
50
+ "namespace": "local",
51
+ "keys": ["local.order_timestamp"],
52
+ "grain": {"components": ["local.order_id"], "where_clause": None},
53
+ "modifiers": [],
54
+ "pseudonyms": [],
55
+ }
56
+ Concept.model_validate(test)
57
+ test["lineage"]["arguments"] = [
58
+ {
59
+ "address": "local.order_timestamp",
60
+ "datatype": "timestamp",
61
+ "metadata": {
62
+ "description": None,
63
+ "line_number": None,
64
+ "concept_source": "manual",
65
+ },
66
+ },
67
+ "monthz",
68
+ ]
69
+ with raises(TypeError):
70
+ Concept.model_validate(test)
71
+
72
+
16
73
  def test_environment_serialization(test_environment: Environment):
17
74
 
18
75
  path = test_environment.to_cache()
@@ -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.31"
7
+ __version__ = "0.0.3.33"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -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"
@@ -1,5 +1,5 @@
1
1
  from trilogy.constants import DEFAULT_NAMESPACE
2
- from trilogy.core.enums import ConceptSource, FunctionType, Purpose
2
+ from trilogy.core.enums import ConceptSource, DatePart, FunctionType, Purpose
3
3
  from trilogy.core.functions import AttrAccess, FunctionFactory
4
4
  from trilogy.core.models.author import Concept, Function, Metadata
5
5
  from trilogy.core.models.core import DataType, StructType, arg_to_datatype
@@ -64,6 +64,30 @@ def generate_date_concepts(concept: Concept, environment: Environment):
64
64
  if new_concept.name in environment.concepts:
65
65
  continue
66
66
  environment.add_concept(new_concept, add_derived=False)
67
+ for grain in [DatePart.MONTH, DatePart.YEAR]:
68
+ function = factory.create_function(
69
+ operator=FunctionType.DATE_TRUNCATE,
70
+ args=[concept, grain],
71
+ )
72
+ new_concept = Concept(
73
+ name=f"{concept.name}.{grain.value}_start",
74
+ datatype=DataType.DATE,
75
+ purpose=Purpose.PROPERTY,
76
+ lineage=function,
77
+ grain=concept.grain,
78
+ namespace=concept.namespace,
79
+ keys=set(
80
+ [concept.address],
81
+ ),
82
+ metadata=Metadata(
83
+ description=f"Auto-derived from {base_description}. The date truncated to the {grain.value}.",
84
+ line_number=base_line_number,
85
+ concept_source=ConceptSource.AUTO_DERIVED,
86
+ ),
87
+ )
88
+ if new_concept.name in environment.concepts:
89
+ continue
90
+ environment.add_concept(new_concept, add_derived=False)
67
91
 
68
92
 
69
93
  def generate_datetime_concepts(concept: Concept, environment: Environment):
@@ -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:
@@ -1658,8 +1667,13 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1658
1667
  # attempt to exit early to avoid checking all types
1659
1668
  break
1660
1669
  elif isinstance(arg, ptype):
1670
+ if isinstance(arg, str) and DataType.DATE_PART in valid_inputs[idx]:
1671
+ if arg not in [x.value for x in DatePart]:
1672
+ pass
1673
+ else:
1674
+ break
1661
1675
  raise TypeError(
1662
- f"Invalid {dtype} constant passed into {operator_name} {arg}, expecting one of {valid_inputs[idx]}"
1676
+ f'Invalid {dtype} constant passed into {operator_name} "{arg}", expecting one of {valid_inputs[idx]}'
1663
1677
  )
1664
1678
  return v
1665
1679
 
@@ -1782,6 +1796,10 @@ class FunctionCallWrapper(
1782
1796
  ],
1783
1797
  )
1784
1798
 
1799
+ def with_reference_replacement(self, source, target):
1800
+ raise NotImplementedError("Cannot reference replace")
1801
+ return self
1802
+
1785
1803
  def with_merge(
1786
1804
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1787
1805
  ) -> "FunctionCallWrapper":
@@ -2362,6 +2380,7 @@ FuncArgs = (
2362
2380
  | FilterItem
2363
2381
  | int
2364
2382
  | float
2383
+ | DatePart
2365
2384
  | str
2366
2385
  | date
2367
2386
  | datetime
@@ -2371,7 +2390,6 @@ FuncArgs = (
2371
2390
  | ListType
2372
2391
  | MapType
2373
2392
  | NumericType
2374
- | DatePart
2375
2393
  | list
2376
2394
  | ListWrapper[Any]
2377
2395
  )
@@ -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),
@@ -250,7 +250,7 @@ class Environment(BaseModel):
250
250
  imports=dict(self.imports),
251
251
  namespace=self.namespace,
252
252
  working_path=self.working_path,
253
- environment_config=self.config,
253
+ environment_config=self.config.model_copy(deep=True),
254
254
  version=self.version,
255
255
  cte_name_map=dict(self.cte_name_map),
256
256
  materialized_concepts=set(self.materialized_concepts),
@@ -637,7 +637,7 @@ class QueryDatasource(BaseModel):
637
637
  and CONFIG.validate_missing
638
638
  ):
639
639
  raise SyntaxError(
640
- f"On query datasource missing source map for {concept.address} on {key}, have {v}"
640
+ f"On query datasource missing source map for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
641
641
  )
642
642
  return v
643
643
 
@@ -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}")
@@ -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}
@@ -948,6 +951,11 @@ def _search_concepts(
948
951
  if complete == ValidationResult.COMPLETE:
949
952
  condition_required = True
950
953
  non_virtual = [c for c in completion_mandatory if c.address not in virtual]
954
+ non_virtual_output = [c for c in original_mandatory if c.address not in virtual]
955
+ non_virtual_different = len(completion_mandatory) != len(original_mandatory)
956
+ non_virtual_difference_values = set(
957
+ [x.address for x in completion_mandatory]
958
+ ).difference(set([x.address for x in original_mandatory]))
951
959
  if not conditions:
952
960
  condition_required = False
953
961
  non_virtual = [c for c in mandatory_list if c.address not in virtual]
@@ -966,7 +974,19 @@ def _search_concepts(
966
974
  )
967
975
  if len(stack) == 1:
968
976
  output: StrategyNode = stack[0]
969
- # _ = restrict_node_outputs_targets(output, mandatory_list, depth)
977
+ if non_virtual_different:
978
+ logger.info(
979
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values"
980
+ )
981
+ output.set_output_concepts(
982
+ [
983
+ x
984
+ for x in output.output_concepts
985
+ if x.address in non_virtual_output
986
+ ],
987
+ rebuild=False,
988
+ )
989
+
970
990
  logger.info(
971
991
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)}"
972
992
  )
@@ -995,6 +1015,30 @@ def _search_concepts(
995
1015
  logger.info(
996
1016
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
997
1017
  )
1018
+ if condition_required and conditions and non_virtual_different:
1019
+ logger.info(
1020
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Conditions {conditions} were injected, checking if we need a group to restore grain"
1021
+ )
1022
+ result = GroupNode.check_if_required(
1023
+ downstream_concepts=original_mandatory,
1024
+ parents=[output.resolve()],
1025
+ environment=environment,
1026
+ depth=depth,
1027
+ )
1028
+ logger.info(f"gcheck result is {result}")
1029
+ if result.required:
1030
+ logger.info(
1031
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Adding group node"
1032
+ )
1033
+ return GroupNode(
1034
+ output_concepts=original_mandatory,
1035
+ input_concepts=original_mandatory,
1036
+ environment=environment,
1037
+ parents=[output],
1038
+ partial_concepts=output.partial_concepts,
1039
+ preexisting_conditions=conditions.conditional,
1040
+ depth=depth,
1041
+ )
998
1042
  return output
999
1043
 
1000
1044
  # 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(
@@ -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"),
@@ -27,6 +27,7 @@ FUNCTION_MAP = {
27
27
  FunctionType.INDEX_ACCESS: lambda args: (f"{args[0]}[{args[1]}]"),
28
28
  # datetime is aliased
29
29
  FunctionType.CURRENT_DATETIME: lambda x: "cast(get_current_timestamp() as datetime)",
30
+ FunctionType.DATE: lambda x: f"cast({x[0]} as date)",
30
31
  FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc('{x[1]}', {x[0]})",
31
32
  FunctionType.DATE_ADD: lambda x: f"date_add({x[0]}, {x[2]} * INTERVAL 1 {x[1]})",
32
33
  FunctionType.DATE_PART: lambda x: f"date_part('{x[1]}', {x[0]})",