pytrilogy 0.0.3.33__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.33/pytrilogy.egg-info → pytrilogy-0.0.3.34}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_query_processing.py +3 -1
  4. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/authoring/__init__.py +6 -0
  6. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/author.py +1 -1
  7. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/build_environment.py +6 -13
  8. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/environment.py +2 -1
  9. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/execute.py +8 -0
  10. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/concept_strategies_v3.py +17 -1
  11. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_merge_node.py +2 -0
  12. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/utility.py +11 -5
  13. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/statements/author.py +5 -0
  14. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/parse_engine.py +24 -0
  15. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/render.py +6 -0
  16. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/trilogy.lark +3 -0
  17. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/LICENSE.md +0 -0
  18. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/README.md +0 -0
  19. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/pyproject.toml +0 -0
  20. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/SOURCES.txt +0 -0
  21. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/dependency_links.txt +0 -0
  22. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/entry_points.txt +0 -0
  23. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/requires.txt +0 -0
  24. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/pytrilogy.egg-info/top_level.txt +0 -0
  25. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/setup.cfg +0 -0
  26. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/setup.py +0 -0
  27. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_datatypes.py +0 -0
  28. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_declarations.py +0 -0
  29. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_derived_concepts.py +0 -0
  30. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_discovery_nodes.py +0 -0
  31. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_enums.py +0 -0
  32. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_environment.py +0 -0
  33. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_executor.py +0 -0
  34. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_functions.py +0 -0
  35. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_imports.py +0 -0
  36. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_metadata.py +0 -0
  37. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_models.py +0 -0
  38. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_multi_join_assignments.py +0 -0
  39. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_parse_engine.py +0 -0
  40. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_parsing.py +0 -0
  41. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_partial_handling.py +0 -0
  42. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_query_render.py +0 -0
  43. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_select.py +0 -0
  44. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_show.py +0 -0
  45. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_statements.py +0 -0
  46. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_typing.py +0 -0
  47. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_undefined_concept.py +0 -0
  48. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_user_functions.py +0 -0
  49. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/tests/test_where_clause.py +0 -0
  50. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/compiler.py +0 -0
  51. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/constants.py +0 -0
  52. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/__init__.py +0 -0
  53. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/constants.py +0 -0
  54. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/enums.py +0 -0
  55. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/env_processor.py +0 -0
  56. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/environment_helpers.py +0 -0
  57. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/ergonomics.py +0 -0
  58. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/exceptions.py +0 -0
  59. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/functions.py +0 -0
  60. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/graph_models.py +0 -0
  61. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/internal.py +0 -0
  62. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/__init__.py +0 -0
  63. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/build.py +0 -0
  64. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/core.py +0 -0
  65. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/models/datasource.py +0 -0
  66. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/optimization.py +0 -0
  67. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/__init__.py +0 -0
  68. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/base_optimization.py +0 -0
  69. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/inline_constant.py +0 -0
  70. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/inline_datasource.py +0 -0
  71. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  72. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/__init__.py +0 -0
  73. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/graph_utils.py +0 -0
  74. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/__init__.py +0 -0
  75. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  76. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/common.py +0 -0
  77. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  78. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/group_node.py +0 -0
  79. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  80. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  81. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  82. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  83. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  84. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  85. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/select_node.py +0 -0
  86. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  87. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/union_node.py +0 -0
  88. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  89. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/node_generators/window_node.py +0 -0
  90. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/__init__.py +0 -0
  91. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/base_node.py +0 -0
  92. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/filter_node.py +0 -0
  93. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/group_node.py +0 -0
  94. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/merge_node.py +0 -0
  95. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  96. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/union_node.py +0 -0
  97. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  98. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/processing/nodes/window_node.py +0 -0
  99. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/query_processor.py +0 -0
  100. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/statements/__init__.py +0 -0
  101. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/statements/build.py +0 -0
  102. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/statements/common.py +0 -0
  103. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/core/statements/execute.py +0 -0
  104. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/__init__.py +0 -0
  105. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/base.py +0 -0
  106. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/bigquery.py +0 -0
  107. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/common.py +0 -0
  108. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/config.py +0 -0
  109. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/dataframe.py +0 -0
  110. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/duckdb.py +0 -0
  111. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/enums.py +0 -0
  112. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/postgres.py +0 -0
  113. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/presto.py +0 -0
  114. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/snowflake.py +0 -0
  115. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/dialect/sql_server.py +0 -0
  116. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/engine.py +0 -0
  117. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/executor.py +0 -0
  118. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/hooks/__init__.py +0 -0
  119. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/hooks/base_hook.py +0 -0
  120. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/hooks/graph_hook.py +0 -0
  121. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/hooks/query_debugger.py +0 -0
  122. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/metadata/__init__.py +0 -0
  123. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parser.py +0 -0
  124. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/__init__.py +0 -0
  125. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/common.py +0 -0
  126. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/config.py +0 -0
  127. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/exceptions.py +0 -0
  128. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/parsing/helpers.py +0 -0
  129. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/py.typed +0 -0
  130. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/render.py +0 -0
  131. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/scripts/__init__.py +0 -0
  132. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/scripts/trilogy.py +0 -0
  133. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/std/__init__.py +0 -0
  134. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/std/dashboard.preql +0 -0
  135. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/std/date.preql +0 -0
  136. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/std/display.preql +0 -0
  137. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/std/geography.preql +0 -0
  138. {pytrilogy-0.0.3.33 → pytrilogy-0.0.3.34}/trilogy/std/money.preql +0 -0
  139. {pytrilogy-0.0.3.33 → 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.33
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.33
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"
@@ -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.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
  ]
@@ -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] = []
@@ -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,
@@ -384,6 +384,11 @@ class MergeStatementV2(HasUUID, BaseModel):
384
384
  modifiers: List[Modifier] = Field(default_factory=list)
385
385
 
386
386
 
387
+ class KeyMergeStatement(HasUUID, BaseModel):
388
+ keys: set[str]
389
+ target: ConceptRef
390
+
391
+
387
392
  class ImportStatement(HasUUID, BaseModel):
388
393
  # import abc.def as bar
389
394
  # the bit after 'as', eg bar
@@ -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
File without changes
File without changes
File without changes
File without changes