pytrilogy 0.0.3.33__tar.gz → 0.0.3.35__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 (140) hide show
  1. {pytrilogy-0.0.3.33/pytrilogy.egg-info → pytrilogy-0.0.3.35}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/pytrilogy.egg-info/SOURCES.txt +2 -2
  4. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_query_processing.py +3 -1
  5. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/authoring/__init__.py +6 -0
  7. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/functions.py +17 -4
  8. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/author.py +21 -5
  9. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/build.py +35 -4
  10. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/build_environment.py +6 -13
  11. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/environment.py +2 -1
  12. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/execute.py +8 -0
  13. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/concept_strategies_v3.py +17 -1
  14. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/select_merge_node.py +2 -0
  15. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/utility.py +11 -5
  16. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/statements/author.py +5 -1
  17. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/common.py +1 -1
  18. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/parse_engine.py +24 -0
  19. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/render.py +6 -0
  20. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/trilogy.lark +3 -0
  21. pytrilogy-0.0.3.35/trilogy/std/report.preql +5 -0
  22. pytrilogy-0.0.3.33/trilogy/std/dashboard.preql +0 -5
  23. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/LICENSE.md +0 -0
  24. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/README.md +0 -0
  25. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/pyproject.toml +0 -0
  26. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/pytrilogy.egg-info/dependency_links.txt +0 -0
  27. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/pytrilogy.egg-info/entry_points.txt +0 -0
  28. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/pytrilogy.egg-info/requires.txt +0 -0
  29. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/pytrilogy.egg-info/top_level.txt +0 -0
  30. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/setup.cfg +0 -0
  31. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/setup.py +0 -0
  32. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_datatypes.py +0 -0
  33. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_declarations.py +0 -0
  34. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_derived_concepts.py +0 -0
  35. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_discovery_nodes.py +0 -0
  36. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_enums.py +0 -0
  37. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_environment.py +0 -0
  38. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_executor.py +0 -0
  39. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_functions.py +0 -0
  40. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_imports.py +0 -0
  41. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_metadata.py +0 -0
  42. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_models.py +0 -0
  43. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_multi_join_assignments.py +0 -0
  44. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_parse_engine.py +0 -0
  45. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_parsing.py +0 -0
  46. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_partial_handling.py +0 -0
  47. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_query_render.py +0 -0
  48. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_select.py +0 -0
  49. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_show.py +0 -0
  50. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_statements.py +0 -0
  51. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_typing.py +0 -0
  52. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_undefined_concept.py +0 -0
  53. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_user_functions.py +0 -0
  54. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/tests/test_where_clause.py +0 -0
  55. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/compiler.py +0 -0
  56. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/constants.py +0 -0
  57. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/__init__.py +0 -0
  58. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/constants.py +0 -0
  59. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/enums.py +0 -0
  60. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/env_processor.py +0 -0
  61. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/environment_helpers.py +0 -0
  62. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/ergonomics.py +0 -0
  63. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/exceptions.py +0 -0
  64. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/graph_models.py +0 -0
  65. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/internal.py +0 -0
  66. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/__init__.py +0 -0
  67. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/core.py +0 -0
  68. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/models/datasource.py +0 -0
  69. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/optimization.py +0 -0
  70. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/optimizations/__init__.py +0 -0
  71. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/optimizations/base_optimization.py +0 -0
  72. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/optimizations/inline_constant.py +0 -0
  73. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/optimizations/inline_datasource.py +0 -0
  74. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  75. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/__init__.py +0 -0
  76. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/graph_utils.py +0 -0
  77. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/__init__.py +0 -0
  78. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  79. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/common.py +0 -0
  80. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  81. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/group_node.py +0 -0
  82. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  83. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  84. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  85. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  86. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  87. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  88. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/select_node.py +0 -0
  89. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  90. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/union_node.py +0 -0
  91. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  92. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/node_generators/window_node.py +0 -0
  93. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/__init__.py +0 -0
  94. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/base_node.py +0 -0
  95. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/filter_node.py +0 -0
  96. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/group_node.py +0 -0
  97. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/merge_node.py +0 -0
  98. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  99. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/union_node.py +0 -0
  100. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  101. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/processing/nodes/window_node.py +0 -0
  102. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/query_processor.py +0 -0
  103. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/statements/__init__.py +0 -0
  104. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/statements/build.py +0 -0
  105. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/statements/common.py +0 -0
  106. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/core/statements/execute.py +0 -0
  107. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/__init__.py +0 -0
  108. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/base.py +0 -0
  109. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/bigquery.py +0 -0
  110. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/common.py +0 -0
  111. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/config.py +0 -0
  112. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/dataframe.py +0 -0
  113. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/duckdb.py +0 -0
  114. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/enums.py +0 -0
  115. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/postgres.py +0 -0
  116. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/presto.py +0 -0
  117. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/snowflake.py +0 -0
  118. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/dialect/sql_server.py +0 -0
  119. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/engine.py +0 -0
  120. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/executor.py +0 -0
  121. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/hooks/__init__.py +0 -0
  122. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/hooks/base_hook.py +0 -0
  123. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/hooks/graph_hook.py +0 -0
  124. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/hooks/query_debugger.py +0 -0
  125. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/metadata/__init__.py +0 -0
  126. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parser.py +0 -0
  127. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/__init__.py +0 -0
  128. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/config.py +0 -0
  129. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/exceptions.py +0 -0
  130. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/parsing/helpers.py +0 -0
  131. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/py.typed +0 -0
  132. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/render.py +0 -0
  133. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/scripts/__init__.py +0 -0
  134. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/scripts/trilogy.py +0 -0
  135. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/std/__init__.py +0 -0
  136. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/std/date.preql +0 -0
  137. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/std/display.preql +0 -0
  138. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/std/geography.preql +0 -0
  139. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/std/money.preql +0 -0
  140. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.35}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.33
3
+ Version: 0.0.3.35
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.33
3
+ Version: 0.0.3.35
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -130,8 +130,8 @@ trilogy/parsing/trilogy.lark
130
130
  trilogy/scripts/__init__.py
131
131
  trilogy/scripts/trilogy.py
132
132
  trilogy/std/__init__.py
133
- trilogy/std/dashboard.preql
134
133
  trilogy/std/date.preql
135
134
  trilogy/std/display.preql
136
135
  trilogy/std/geography.preql
137
- trilogy/std/money.preql
136
+ trilogy/std/money.preql
137
+ trilogy/std/report.preql
@@ -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"
@@ -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.33"
7
+ __version__ = "0.0.3.35"
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
  ]
@@ -111,11 +111,14 @@ def validate_case_output(
111
111
  if output_datatype != DataType.NULL:
112
112
  datatypes.add(output_datatype.data_type)
113
113
  mapz[str(arg.expr)] = output_datatype
114
- if not len(datatypes) == 1:
114
+ known = [x for x in datatypes if x != DataType.UNKNOWN]
115
+ if len(known) == 0:
116
+ return DataType.UNKNOWN
117
+ if not len(known) == 1:
115
118
  raise SyntaxError(
116
119
  f"All case expressions must have the same output datatype, got {datatypes} from {mapz}"
117
120
  )
118
- return datatypes.pop()
121
+ return known.pop()
119
122
 
120
123
 
121
124
  def create_struct_output(
@@ -672,12 +675,22 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
672
675
  arg_count=1,
673
676
  ),
674
677
  FunctionType.SUM: FunctionConfig(
675
- valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
678
+ valid_inputs={
679
+ DataType.INTEGER,
680
+ DataType.FLOAT,
681
+ DataType.NUMBER,
682
+ DataType.NUMERIC,
683
+ },
676
684
  output_purpose=Purpose.METRIC,
677
685
  arg_count=1,
678
686
  ),
679
687
  FunctionType.AVG: FunctionConfig(
680
- valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
688
+ valid_inputs={
689
+ DataType.INTEGER,
690
+ DataType.FLOAT,
691
+ DataType.NUMBER,
692
+ DataType.NUMERIC,
693
+ },
681
694
  output_purpose=Purpose.METRIC,
682
695
  arg_count=1,
683
696
  ),
@@ -107,6 +107,10 @@ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
107
107
  ) = DataType.UNKNOWN
108
108
  metadata: Optional["Metadata"] = None
109
109
 
110
+ @property
111
+ def reference(self):
112
+ return self
113
+
110
114
  @property
111
115
  def line_no(self) -> int | None:
112
116
  if self.metadata:
@@ -1089,7 +1093,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1089
1093
  pseudonyms=self.pseudonyms,
1090
1094
  )
1091
1095
 
1092
- @property
1096
+ @cached_property
1093
1097
  def sources(self) -> List["ConceptRef"]:
1094
1098
  if self.lineage:
1095
1099
  output: List[ConceptRef] = []
@@ -1426,7 +1430,7 @@ def get_basic_type(
1426
1430
  return type
1427
1431
 
1428
1432
 
1429
- class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
1433
+ class CaseWhen(Namespaced, DataTyped, ConceptArgs, Mergeable, BaseModel):
1430
1434
  comparison: Conditional | SubselectComparison | Comparison
1431
1435
  expr: "Expr"
1432
1436
 
@@ -1436,6 +1440,10 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
1436
1440
  return v.reference
1437
1441
  return v
1438
1442
 
1443
+ @property
1444
+ def output_datatype(self):
1445
+ return arg_to_datatype(self.expr)
1446
+
1439
1447
  def __str__(self):
1440
1448
  return self.__repr__()
1441
1449
 
@@ -1488,7 +1496,7 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
1488
1496
  )
1489
1497
 
1490
1498
 
1491
- class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1499
+ class CaseElse(Namespaced, ConceptArgs, DataTyped, Mergeable, BaseModel):
1492
1500
  expr: "Expr"
1493
1501
  # this ensures that it's easily differentiable from CaseWhen
1494
1502
  discriminant: ComparisonOperator = ComparisonOperator.ELSE
@@ -1499,6 +1507,10 @@ class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1499
1507
  def __repr__(self):
1500
1508
  return f"ELSE {str(self.expr)}"
1501
1509
 
1510
+ @property
1511
+ def output_datatype(self):
1512
+ return arg_to_datatype(self.expr)
1513
+
1502
1514
  @field_validator("expr", mode="before")
1503
1515
  def enforce_expr(cls, v):
1504
1516
  if isinstance(v, Concept):
@@ -1837,7 +1849,7 @@ class FunctionCallWrapper(
1837
1849
 
1838
1850
  class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
1839
1851
  function: Function
1840
- by: List[ConceptRef] = Field(default_factory=list)
1852
+ by: List[ConceptRef | Concept] = Field(default_factory=list)
1841
1853
 
1842
1854
  def __init__(self, **kwargs):
1843
1855
  super().__init__(**kwargs)
@@ -1863,7 +1875,7 @@ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel)
1863
1875
 
1864
1876
  @property
1865
1877
  def concept_arguments(self) -> List[ConceptRef]:
1866
- return self.function.concept_arguments + self.by
1878
+ return self.function.concept_arguments + [x.reference for x in self.by]
1867
1879
 
1868
1880
  @property
1869
1881
  def output_datatype(self):
@@ -2392,4 +2404,8 @@ FuncArgs = (
2392
2404
  | NumericType
2393
2405
  | list
2394
2406
  | ListWrapper[Any]
2407
+ | TupleWrapper[Any]
2408
+ | Comparison
2409
+ | Conditional
2410
+ | MagicConstants
2395
2411
  )
@@ -1538,9 +1538,23 @@ class Factory:
1538
1538
 
1539
1539
  @build.register
1540
1540
  def _(self, base: CaseWhen) -> BuildCaseWhen:
1541
+ from trilogy.parsing.common import arbitrary_to_concept
1542
+
1543
+ comparison = base.comparison
1544
+ if isinstance(comparison, (AggregateWrapper, FilterItem, WindowItem)):
1545
+ comparison = arbitrary_to_concept(
1546
+ comparison,
1547
+ environment=self.environment,
1548
+ )
1549
+ expr: Concept | FuncArgs = base.expr
1550
+ if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
1551
+ expr = arbitrary_to_concept(
1552
+ expr,
1553
+ environment=self.environment,
1554
+ )
1541
1555
  return BuildCaseWhen.model_construct(
1542
- comparison=self.build(base.comparison),
1543
- expr=(self.build(base.expr)),
1556
+ comparison=self.build(comparison),
1557
+ expr=self.build(expr),
1544
1558
  )
1545
1559
 
1546
1560
  @build.register
@@ -1647,10 +1661,27 @@ class Factory:
1647
1661
  @build.register
1648
1662
  def _(self, base: WindowItem) -> BuildWindowItem:
1649
1663
  # to do proper discovery, we need to inject virtual intermediate ocncepts
1664
+ from trilogy.parsing.common import arbitrary_to_concept
1665
+
1666
+ content: Concept | FuncArgs = base.content
1667
+ if isinstance(content, (AggregateWrapper, FilterItem, WindowItem)):
1668
+ content = arbitrary_to_concept(
1669
+ content,
1670
+ environment=self.environment,
1671
+ )
1672
+ final_by = []
1673
+ for x in base.order_by:
1674
+ if (
1675
+ isinstance(x.expr, AggregateWrapper)
1676
+ and not x.expr.by
1677
+ and isinstance(content, (ConceptRef, Concept))
1678
+ ):
1679
+ x.expr.by = [content]
1680
+ final_by.append(x)
1650
1681
  return BuildWindowItem.model_construct(
1651
1682
  type=base.type,
1652
- content=self.build(base.content),
1653
- order_by=[self.build(x) for x in base.order_by],
1683
+ content=self.build(content),
1684
+ order_by=[self.build(x) for x in final_by],
1654
1685
  over=[self.build(x) for x in base.over],
1655
1686
  index=base.index,
1656
1687
  )
@@ -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
@@ -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
  )
@@ -860,7 +860,7 @@ def _search_concepts(
860
860
  and priority_concept.address not in conditions.row_arguments
861
861
  ):
862
862
  logger.info(
863
- 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"
864
864
  )
865
865
  local_conditions = conditions
866
866
 
@@ -936,7 +936,23 @@ def _search_concepts(
936
936
  if complete == ValidationResult.COMPLETE and (
937
937
  not accept_partial or (accept_partial and not partial)
938
938
  ):
939
+ logger.info(
940
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} breaking loop, complete"
941
+ )
939
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
+ )
940
956
  # if we have attempted on root node, we've tried them all.
941
957
  # inject in another search with filter concepts
942
958
  if priority_concept.derivation == Derivation.ROOT:
@@ -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
 
@@ -81,7 +81,7 @@ class JoinOrderOutput:
81
81
 
82
82
 
83
83
  def resolve_join_order_v2(
84
- g: nx.Graph, partials: dict[str, list[str]]
84
+ g: nx.Graph, partials: dict[str, list[str]], nullables: dict[str, list[str]]
85
85
  ) -> list[JoinOrderOutput]:
86
86
  datasources = [x for x in g.nodes if x.startswith("ds~")]
87
87
  concepts = [x for x in g.nodes if x.startswith("c~")]
@@ -108,7 +108,7 @@ def resolve_join_order_v2(
108
108
  root = next_pivots[0]
109
109
  pivots = [x for x in pivots if x != root]
110
110
  else:
111
- root = pivots.pop()
111
+ root = pivots.pop(0)
112
112
 
113
113
  # sort so less partials is last and eligible lefts are
114
114
  def score_key(x: str) -> tuple[int, int, str]:
@@ -119,6 +119,8 @@ def resolve_join_order_v2(
119
119
  # if it has the concept as a partial, lower weight
120
120
  if root in partials.get(x, []):
121
121
  base -= 1
122
+ if root in nullables.get(x, []):
123
+ base -= 1
122
124
  return (base, len(x), x)
123
125
 
124
126
  # get remainig un-joined datasets
@@ -159,9 +161,11 @@ def resolve_join_order_v2(
159
161
  )
160
162
  right_is_partial = any(key in partials.get(right, []) for key in common)
161
163
  # we don't care if left is nullable for join type (just keys), but if we did
162
- # ex: left_is_nullable = any(key in partials.get(left_candidate, [])
164
+ # left_is_nullable = any(
165
+ # key in nullables.get(left_candidate, []) for key in common
166
+ # )
163
167
  right_is_nullable = any(
164
- key in partials.get(right, []) for key in common
168
+ key in nullables.get(right, []) for key in common
165
169
  )
166
170
  if left_is_partial:
167
171
  join_type = JoinType.FULL
@@ -356,6 +360,7 @@ def get_node_joins(
356
360
  ) -> List[BaseJoin]:
357
361
  graph = nx.Graph()
358
362
  partials: dict[str, list[str]] = {}
363
+ nullables: dict[str, list[str]] = {}
359
364
  ds_node_map: dict[str, QueryDatasource | BuildDatasource] = {}
360
365
  concept_map: dict[str, BuildConcept] = {}
361
366
  for datasource in datasources:
@@ -363,6 +368,7 @@ def get_node_joins(
363
368
  ds_node_map[ds_node] = datasource
364
369
  graph.add_node(ds_node, type=NodeType.NODE)
365
370
  partials[ds_node] = [f"c~{c.address}" for c in datasource.partial_concepts]
371
+ nullables[ds_node] = [f"c~{c.address}" for c in datasource.nullable_concepts]
366
372
  for concept in datasource.output_concepts:
367
373
  if concept.address in datasource.hidden_concepts:
368
374
  continue
@@ -374,7 +380,7 @@ def get_node_joins(
374
380
  environment=environment,
375
381
  )
376
382
 
377
- joins = resolve_join_order_v2(graph, partials=partials)
383
+ joins = resolve_join_order_v2(graph, partials=partials, nullables=nullables)
378
384
  return [
379
385
  BaseJoin(
380
386
  left_datasource=ds_node_map[j.left] if j.left else None,
@@ -135,7 +135,6 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
135
135
  )
136
136
 
137
137
  output.grain = output.calculate_grain(environment)
138
-
139
138
  for x in selection:
140
139
  if x.is_undefined and environment.concepts.fail_on_missing:
141
140
  environment.concepts.raise_undefined(
@@ -384,6 +383,11 @@ class MergeStatementV2(HasUUID, BaseModel):
384
383
  modifiers: List[Modifier] = Field(default_factory=list)
385
384
 
386
385
 
386
+ class KeyMergeStatement(HasUUID, BaseModel):
387
+ keys: set[str]
388
+ target: ConceptRef
389
+
390
+
387
391
  class ImportStatement(HasUUID, BaseModel):
388
392
  # import abc.def as bar
389
393
  # the bit after 'as', eg bar
@@ -270,7 +270,7 @@ def _get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
270
270
  elif isinstance(arg, AggregateWrapper) and not arg.by:
271
271
  return [], True
272
272
  elif isinstance(arg, AggregateWrapper) and arg.by:
273
- return arg.by, True
273
+ return [x.reference for x in arg.by], True
274
274
  elif isinstance(arg, FunctionCallWrapper):
275
275
  return get_relevant_parent_concepts(arg.content)
276
276
  return get_concept_arguments(arg), False
@@ -116,6 +116,7 @@ from trilogy.core.statements.author import (
116
116
  CopyStatement,
117
117
  FunctionDeclaration,
118
118
  ImportStatement,
119
+ KeyMergeStatement,
119
120
  Limit,
120
121
  MergeStatementV2,
121
122
  MultiSelectStatement,
@@ -890,6 +891,29 @@ class ParseToObjects(Transformer):
890
891
  def over_list(self, args):
891
892
  return [x for x in args]
892
893
 
894
+ @v_args(meta=True)
895
+ def key_merge_statement(self, meta: Meta, args) -> KeyMergeStatement | None:
896
+ key_inputs = args[:-1]
897
+ target = args[-1]
898
+ keys = [self.environment.concepts[grain] for grain in key_inputs]
899
+ target_c = self.environment.concepts[target]
900
+ new = KeyMergeStatement(
901
+ keys=set([x.address for x in keys]),
902
+ target=target_c.reference,
903
+ )
904
+ internal = Concept(
905
+ name="_" + target_c.address.replace(".", "_"),
906
+ namespace=self.environment.namespace,
907
+ purpose=Purpose.PROPERTY,
908
+ keys=set([x.address for x in keys]),
909
+ datatype=target_c.datatype,
910
+ grain=Grain(components={x.address for x in keys}),
911
+ )
912
+ self.environment.add_concept(internal)
913
+ # always a full merge
914
+ self.environment.merge_concept(target_c, internal, [])
915
+ return new
916
+
893
917
  @v_args(meta=True)
894
918
  def merge_statement(self, meta: Meta, args) -> MergeStatementV2 | None:
895
919
  modifiers = []
@@ -51,6 +51,7 @@ from trilogy.core.statements.author import (
51
51
  CopyStatement,
52
52
  FunctionDeclaration,
53
53
  ImportStatement,
54
+ KeyMergeStatement,
54
55
  MergeStatementV2,
55
56
  MultiSelectStatement,
56
57
  PersistStatement,
@@ -525,6 +526,11 @@ class Renderer:
525
526
  return f"MERGE {self.to_string(arg.sources[0])} into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{self.to_string(arg.targets[arg.sources[0].address])};"
526
527
  return f"MERGE {arg.source_wildcard}.* into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{arg.target_wildcard}.*;"
527
528
 
529
+ @to_string.register
530
+ def _(self, arg: KeyMergeStatement):
531
+ keys = ", ".join(sorted(list(arg.keys)))
532
+ return f"MERGE PROPERTY <{keys}> from {arg.target.address};"
533
+
528
534
  @to_string.register
529
535
  def _(self, arg: Modifier):
530
536
  if arg == Modifier.PARTIAL:
@@ -10,6 +10,7 @@
10
10
  | rowset_derivation_statement
11
11
  | import_statement
12
12
  | copy_statement
13
+ | key_merge_statement
13
14
  | merge_statement
14
15
  | rawsql_statement
15
16
 
@@ -76,6 +77,8 @@
76
77
 
77
78
  align_clause: align_item ("AND"i align_item)* "AND"i?
78
79
 
80
+ key_merge_statement: "merge"i "property"i "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "from"i IDENTIFIER
81
+
79
82
  merge_statement: "merge"i WILDCARD_IDENTIFIER "into"i SHORTHAND_MODIFIER? WILDCARD_IDENTIFIER
80
83
 
81
84
  // raw sql statement
@@ -0,0 +1,5 @@
1
+
2
+
3
+
4
+ def top_x_by_metric(val, metric, x=10, default='other') -> case when rank val by metric desc <x+1 then val else default end; # Get the top X of a value by a certain metric, bucket the rest in other
5
+
@@ -1,5 +0,0 @@
1
-
2
-
3
-
4
- # def top_x_by_metric(val, metric, x=10) -> case when rank val by metric order by metric desc <x then val else 'Other' end;
5
-
File without changes
File without changes
File without changes
File without changes