pytrilogy 0.0.3.43__tar.gz → 0.0.3.45__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 (141) hide show
  1. {pytrilogy-0.0.3.43/pytrilogy.egg-info → pytrilogy-0.0.3.45}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_models.py +0 -3
  4. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_parsing_failures.py +2 -1
  5. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/author.py +12 -14
  7. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/build.py +52 -47
  8. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/concept_strategies_v3.py +2 -0
  9. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/common.py +1 -1
  10. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/window_node.py +10 -4
  11. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/__init__.py +23 -19
  12. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/common.py +40 -25
  13. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/parse_engine.py +83 -13
  14. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/trilogy.lark +44 -34
  15. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/LICENSE.md +0 -0
  16. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/README.md +0 -0
  17. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/pyproject.toml +0 -0
  18. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/pytrilogy.egg-info/SOURCES.txt +0 -0
  19. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/pytrilogy.egg-info/dependency_links.txt +0 -0
  20. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/pytrilogy.egg-info/entry_points.txt +0 -0
  21. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/pytrilogy.egg-info/requires.txt +0 -0
  22. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/pytrilogy.egg-info/top_level.txt +0 -0
  23. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/setup.cfg +0 -0
  24. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/setup.py +0 -0
  25. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_datatypes.py +0 -0
  26. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_declarations.py +0 -0
  27. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_derived_concepts.py +0 -0
  28. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_discovery_nodes.py +0 -0
  29. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_enums.py +0 -0
  30. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_environment.py +0 -0
  31. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_executor.py +0 -0
  32. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_failure.py +0 -0
  33. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_functions.py +1 -1
  34. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_imports.py +0 -0
  35. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_metadata.py +0 -0
  36. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_multi_join_assignments.py +0 -0
  37. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_parse_engine.py +0 -0
  38. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_parsing.py +0 -0
  39. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_partial_handling.py +0 -0
  40. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_query_processing.py +0 -0
  41. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_query_render.py +0 -0
  42. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_select.py +0 -0
  43. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_show.py +0 -0
  44. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_statements.py +0 -0
  45. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_typing.py +0 -0
  46. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_undefined_concept.py +0 -0
  47. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_user_functions.py +0 -0
  48. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/tests/test_where_clause.py +0 -0
  49. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/authoring/__init__.py +0 -0
  50. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/compiler.py +0 -0
  51. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/constants.py +0 -0
  52. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/__init__.py +0 -0
  53. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/constants.py +0 -0
  54. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/enums.py +0 -0
  55. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/env_processor.py +0 -0
  56. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/environment_helpers.py +0 -0
  57. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/ergonomics.py +0 -0
  58. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/exceptions.py +0 -0
  59. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/functions.py +0 -0
  60. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/graph_models.py +0 -0
  61. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/internal.py +0 -0
  62. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/__init__.py +0 -0
  63. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/build_environment.py +0 -0
  64. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/core.py +0 -0
  65. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/datasource.py +0 -0
  66. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/environment.py +0 -0
  67. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/models/execute.py +0 -0
  68. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/optimization.py +0 -0
  69. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/optimizations/__init__.py +0 -0
  70. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/optimizations/base_optimization.py +0 -0
  71. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/optimizations/inline_constant.py +0 -0
  72. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/optimizations/inline_datasource.py +0 -0
  73. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  74. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/__init__.py +0 -0
  75. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/graph_utils.py +0 -0
  76. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/__init__.py +0 -0
  77. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  78. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  79. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/group_node.py +0 -0
  80. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  81. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  82. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  83. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  84. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  85. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  86. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  87. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/select_node.py +0 -0
  88. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  89. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/union_node.py +0 -0
  90. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  91. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/base_node.py +0 -0
  92. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/filter_node.py +0 -0
  93. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/group_node.py +0 -0
  94. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/merge_node.py +0 -0
  95. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  96. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/union_node.py +0 -0
  97. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  98. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/nodes/window_node.py +0 -0
  99. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/processing/utility.py +0 -0
  100. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/query_processor.py +0 -0
  101. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/statements/__init__.py +0 -0
  102. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/statements/author.py +0 -0
  103. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/statements/build.py +0 -0
  104. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/statements/common.py +0 -0
  105. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/core/statements/execute.py +0 -0
  106. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/__init__.py +0 -0
  107. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/base.py +0 -0
  108. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/bigquery.py +0 -0
  109. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/common.py +0 -0
  110. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/config.py +0 -0
  111. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/dataframe.py +0 -0
  112. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/duckdb.py +0 -0
  113. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/enums.py +0 -0
  114. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/postgres.py +0 -0
  115. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/presto.py +0 -0
  116. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/snowflake.py +0 -0
  117. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/dialect/sql_server.py +0 -0
  118. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/engine.py +0 -0
  119. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/executor.py +0 -0
  120. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/hooks/__init__.py +0 -0
  121. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/hooks/base_hook.py +0 -0
  122. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/hooks/graph_hook.py +0 -0
  123. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/hooks/query_debugger.py +0 -0
  124. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/metadata/__init__.py +0 -0
  125. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parser.py +0 -0
  126. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/__init__.py +0 -0
  127. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/config.py +0 -0
  128. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/exceptions.py +0 -0
  129. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/helpers.py +0 -0
  130. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/parsing/render.py +0 -0
  131. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/py.typed +0 -0
  132. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/render.py +0 -0
  133. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/scripts/__init__.py +0 -0
  134. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/scripts/trilogy.py +0 -0
  135. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/std/__init__.py +0 -0
  136. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/std/date.preql +0 -0
  137. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/std/display.preql +0 -0
  138. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/std/geography.preql +0 -0
  139. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/std/money.preql +0 -0
  140. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/std/report.preql +0 -0
  141. {pytrilogy-0.0.3.43 → pytrilogy-0.0.3.45}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.43
3
+ Version: 0.0.3.45
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.43
3
+ Version: 0.0.3.45
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -213,9 +213,6 @@ def test_comparison():
213
213
  with raises(SyntaxError):
214
214
  Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
215
215
 
216
- with raises(SyntaxError):
217
- Comparison(left=1, right=1, operator=ComparisonOperator.IN)
218
-
219
216
  # this should not error
220
217
  Comparison(left=1, right=[1, 2, 3], operator=ComparisonOperator.IN)
221
218
 
@@ -77,10 +77,11 @@ SELECT
77
77
  1+2->scalar
78
78
  ;
79
79
  """
80
- with raises(ParseError):
80
+ with raises(ParseError) as e:
81
81
  env, parsed = parse_text(
82
82
  x, parse_config=Parsing(strict_name_shadow_enforcement=True)
83
83
  )
84
+ assert "abc" in str(e)
84
85
  x = """
85
86
  key scalar int;
86
87
  property scalar.int_array list<int>;
@@ -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.43"
7
+ __version__ = "0.0.3.45"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -629,18 +629,8 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
629
629
  )
630
630
  elif self.operator in (ComparisonOperator.IN, ComparisonOperator.NOT_IN):
631
631
  right_type = arg_to_datatype(self.right)
632
- if not any(
633
- [
634
- isinstance(self.right, ConceptRef),
635
- right_type in (DataType.LIST,),
636
- isinstance(right_type, (ListType, ListWrapper, TupleWrapper)),
637
- ]
638
- ):
639
- raise SyntaxError(
640
- f"Cannot use {self.operator.value} with non-list, non-tuple, non-concept object {self.right} in {str(self)}"
641
- )
642
632
 
643
- elif isinstance(right_type, ListType) and not is_compatible_datatype(
633
+ if isinstance(right_type, ListType) and not is_compatible_datatype(
644
634
  arg_to_datatype(self.left), right_type.value_data_type
645
635
  ):
646
636
  raise SyntaxError(
@@ -1916,7 +1906,7 @@ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel)
1916
1906
 
1917
1907
 
1918
1908
  class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
1919
- content: ConceptRef
1909
+ content: FuncArgs
1920
1910
  where: "WhereClause"
1921
1911
 
1922
1912
  @field_validator("content", mode="before")
@@ -1932,13 +1922,21 @@ class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
1932
1922
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1933
1923
  ) -> "FilterItem":
1934
1924
  return FilterItem.model_construct(
1935
- content=self.content.with_merge(source, target, modifiers),
1925
+ content=(
1926
+ self.content.with_merge(source, target, modifiers)
1927
+ if isinstance(self.content, Mergeable)
1928
+ else self.content
1929
+ ),
1936
1930
  where=self.where.with_merge(source, target, modifiers),
1937
1931
  )
1938
1932
 
1939
1933
  def with_namespace(self, namespace: str) -> "FilterItem":
1940
1934
  return FilterItem.model_construct(
1941
- content=self.content.with_namespace(namespace),
1935
+ content=(
1936
+ self.content.with_namespace(namespace)
1937
+ if isinstance(self.content, Namespaced)
1938
+ else self.content
1939
+ ),
1942
1940
  where=self.where.with_namespace(namespace),
1943
1941
  )
1944
1942
 
@@ -1496,22 +1496,43 @@ class Factory:
1496
1496
  ):
1497
1497
  return base
1498
1498
 
1499
+ def instantiate_concept(
1500
+ self,
1501
+ arg: (
1502
+ AggregateWrapper
1503
+ | FunctionCallWrapper
1504
+ | WindowItem
1505
+ | FilterItem
1506
+ | Function
1507
+ | ListWrapper[Any]
1508
+ | MapWrapper[Any, Any]
1509
+ | int
1510
+ | float
1511
+ | str
1512
+ ),
1513
+ ) -> tuple[Concept, BuildConcept]:
1514
+ from trilogy.parsing.common import arbitrary_to_concept
1515
+
1516
+ new = arbitrary_to_concept(
1517
+ arg,
1518
+ environment=self.environment,
1519
+ )
1520
+ built = self.build(new)
1521
+ self.local_concepts[new.address] = built
1522
+ return new, built
1523
+
1499
1524
  @build.register
1500
1525
  def _(self, base: None) -> None:
1501
1526
  return base
1502
1527
 
1503
1528
  @build.register
1504
1529
  def _(self, base: Function) -> BuildFunction | BuildAggregateWrapper:
1505
- from trilogy.parsing.common import arbitrary_to_concept
1506
1530
 
1507
1531
  raw_args: list[Concept | FuncArgs] = []
1508
1532
  for arg in base.arguments:
1509
1533
  # to do proper discovery, we need to inject virtual intermediate ocncepts
1510
1534
  if isinstance(arg, (AggregateWrapper, FilterItem, WindowItem)):
1511
- narg = arbitrary_to_concept(
1512
- arg,
1513
- environment=self.environment,
1514
- )
1535
+ narg, _ = self.instantiate_concept(arg)
1515
1536
  raw_args.append(narg)
1516
1537
  else:
1517
1538
  raw_args.append(arg)
@@ -1532,24 +1553,16 @@ class Factory:
1532
1553
  for x in arguments:
1533
1554
  if isinstance(x, (ConceptRef, Concept)):
1534
1555
  final_args.append(x)
1535
- elif isinstance(x, (AggregateWrapper, FilterItem, WindowItem)):
1536
- newx = arbitrary_to_concept(
1537
- x,
1538
- environment=self.environment,
1539
- )
1540
- final_args.append(newx)
1541
1556
  else:
1542
1557
  # constants, etc, can be ignored for group
1543
1558
  continue
1544
- group_base = arbitrary_to_concept(
1559
+ _, rval = self.instantiate_concept(
1545
1560
  AggregateWrapper(
1546
1561
  function=group_base.lineage.function,
1547
1562
  by=final_args,
1548
- ),
1549
- environment=self.environment,
1563
+ )
1550
1564
  )
1551
1565
 
1552
- rval = self.build(group_base)
1553
1566
  return BuildFunction.model_construct(
1554
1567
  operator=base.operator,
1555
1568
  arguments=[rval, *[self.build(c) for c in raw_args[1:]]],
@@ -1580,20 +1593,13 @@ class Factory:
1580
1593
 
1581
1594
  @build.register
1582
1595
  def _(self, base: CaseWhen) -> BuildCaseWhen:
1583
- from trilogy.parsing.common import arbitrary_to_concept
1584
1596
 
1585
1597
  comparison = base.comparison
1586
1598
  if isinstance(comparison, (AggregateWrapper, FilterItem, WindowItem)):
1587
- comparison = arbitrary_to_concept(
1588
- comparison,
1589
- environment=self.environment,
1590
- )
1599
+ comparison, _ = self.instantiate_concept(comparison)
1591
1600
  expr: Concept | FuncArgs = base.expr
1592
1601
  if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
1593
- expr = arbitrary_to_concept(
1594
- expr,
1595
- environment=self.environment,
1596
- )
1602
+ expr, _ = self.instantiate_concept(expr)
1597
1603
  return BuildCaseWhen.model_construct(
1598
1604
  comparison=self.build(comparison),
1599
1605
  expr=self.build(expr),
@@ -1615,6 +1621,7 @@ class Factory:
1615
1621
  else:
1616
1622
  build_lineage = None
1617
1623
  derivation = Concept.calculate_derivation(build_lineage, base.purpose)
1624
+
1618
1625
  granularity = Concept.calculate_granularity(
1619
1626
  derivation, final_grain, build_lineage
1620
1627
  )
@@ -1675,16 +1682,12 @@ class Factory:
1675
1682
 
1676
1683
  @build.register
1677
1684
  def _(self, base: OrderItem) -> BuildOrderItem:
1678
- from trilogy.parsing.common import arbitrary_to_concept
1679
1685
 
1680
1686
  bexpr: Any
1681
1687
  if isinstance(base.expr, (AggregateWrapper, WindowItem, FilterItem)) or (
1682
1688
  isinstance(base.expr, Function) and base.expr.operator == FunctionType.GROUP
1683
1689
  ):
1684
- bexpr = arbitrary_to_concept(
1685
- base.expr,
1686
- environment=self.environment,
1687
- )
1690
+ bexpr, _ = self.instantiate_concept(base.expr)
1688
1691
  else:
1689
1692
  bexpr = base.expr
1690
1693
  return BuildOrderItem.model_construct(
@@ -1707,15 +1710,10 @@ class Factory:
1707
1710
 
1708
1711
  @build.register
1709
1712
  def _(self, base: WindowItem) -> BuildWindowItem:
1710
- # to do proper discovery, we need to inject virtual intermediate ocncepts
1711
- from trilogy.parsing.common import arbitrary_to_concept
1712
1713
 
1713
1714
  content: Concept | FuncArgs = base.content
1714
1715
  if isinstance(content, (AggregateWrapper, FilterItem, WindowItem)):
1715
- content = arbitrary_to_concept(
1716
- content,
1717
- environment=self.environment,
1718
- )
1716
+ content, _ = self.instantiate_concept(content)
1719
1717
  final_by = []
1720
1718
  for x in base.order_by:
1721
1719
  if (
@@ -1743,30 +1741,26 @@ class Factory:
1743
1741
 
1744
1742
  @build.register
1745
1743
  def _(self, base: SubselectComparison) -> BuildSubselectComparison:
1744
+ right: Any = base.right
1745
+ if isinstance(base.right, (AggregateWrapper, WindowItem, FilterItem, Function)):
1746
+ right_c, _ = self.instantiate_concept(base.right)
1747
+ right = right_c
1746
1748
  return BuildSubselectComparison.model_construct(
1747
- left=(self.build(base.left)),
1748
- right=(self.build(base.right)),
1749
+ left=self.build(base.left),
1750
+ right=self.build(right),
1749
1751
  operator=base.operator,
1750
1752
  )
1751
1753
 
1752
1754
  @build.register
1753
1755
  def _(self, base: Comparison) -> BuildComparison:
1754
- from trilogy.parsing.common import arbitrary_to_concept
1755
1756
 
1756
1757
  left = base.left
1757
1758
  if isinstance(left, (AggregateWrapper, WindowItem, FilterItem)):
1758
- left_c = arbitrary_to_concept(
1759
- left,
1760
- environment=self.environment,
1761
- )
1759
+ left_c, _ = self.instantiate_concept(left)
1762
1760
  left = left_c # type: ignore
1763
1761
  right = base.right
1764
1762
  if isinstance(right, (AggregateWrapper, WindowItem, FilterItem)):
1765
- right_c = arbitrary_to_concept(
1766
- right,
1767
- environment=self.environment,
1768
- )
1769
-
1763
+ right_c, _ = self.instantiate_concept(right)
1770
1764
  right = right_c # type: ignore
1771
1765
  return BuildComparison.model_construct(
1772
1766
  left=(self.build(left)),
@@ -1826,6 +1820,13 @@ class Factory:
1826
1820
 
1827
1821
  @build.register
1828
1822
  def _(self, base: FilterItem) -> BuildFilterItem:
1823
+ if isinstance(
1824
+ base.content, (Function, AggregateWrapper, WindowItem, FilterItem)
1825
+ ):
1826
+ _, built = self.instantiate_concept(base.content)
1827
+ return BuildFilterItem.model_construct(
1828
+ content=built, where=self.build(base.where)
1829
+ )
1829
1830
  return BuildFilterItem.model_construct(
1830
1831
  content=self.build(base.content), where=self.build(base.where)
1831
1832
  )
@@ -1969,6 +1970,10 @@ class Factory:
1969
1970
  new.datasources[k] = self.build(d)
1970
1971
  for k, a in base.alias_origin_lookup.items():
1971
1972
  new.alias_origin_lookup[k] = self.build(a)
1973
+ # add in anything that was built as a side-effect
1974
+ for bk, bv in self.local_concepts.items():
1975
+ if bk not in new.concepts:
1976
+ new.concepts[bk] = bv
1972
1977
  new.gen_concept_list_caches()
1973
1978
  return new
1974
1979
 
@@ -748,6 +748,7 @@ def search_concepts(
748
748
  accept_partial: bool = False,
749
749
  conditions: BuildWhereClause | None = None,
750
750
  ) -> StrategyNode | None:
751
+ logger.error(f"starting search for {mandatory_list}")
751
752
  hist = history.get_history(
752
753
  search=mandatory_list, accept_partial=accept_partial, conditions=conditions
753
754
  )
@@ -1094,6 +1095,7 @@ def _search_concepts(
1094
1095
  logger.error(
1095
1096
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Could not resolve concepts {[c.address for c in mandatory_list]}, network outcome was {complete}, missing {all_mandatory - found},"
1096
1097
  )
1098
+
1097
1099
  return None
1098
1100
 
1099
1101
 
@@ -178,7 +178,7 @@ def gen_enrichment_node(
178
178
  for x in extra_required
179
179
  ):
180
180
  log_lambda(
181
- f"{str(type(base_node).__name__)} returning property optimized enrichment node"
181
+ f"{str(type(base_node).__name__)} returning property optimized enrichment node for {extra_required[0].keys}"
182
182
  )
183
183
  return gen_property_enrichment_node(
184
184
  base_node,
@@ -32,7 +32,9 @@ def resolve_window_parent_concepts(
32
32
  if concept.lineage.order_by:
33
33
  for item in concept.lineage.order_by:
34
34
  base += item.concept_arguments
35
-
35
+ if concept.grain:
36
+ for gitem in concept.grain.components:
37
+ base.append(environment.concepts[gitem])
36
38
  return concept.lineage.content, unique(base, "address")
37
39
 
38
40
 
@@ -131,20 +133,24 @@ def gen_window_node(
131
133
  )
132
134
  _window_node.rebuild_cache()
133
135
  _window_node.resolve()
136
+
134
137
  window_node = StrategyNode(
135
138
  input_concepts=[concept] + additional_outputs + parent_concepts + targets,
136
139
  output_concepts=[concept] + additional_outputs + parent_concepts + targets,
137
140
  environment=environment,
138
141
  parents=[_window_node],
139
142
  preexisting_conditions=conditions.conditional if conditions else None,
140
- # hidden_concepts=[
141
- # x.address for x in parent_concepts if x.address not in local_optional
142
- # ],
143
+ grain=BuildGrain.from_concepts(
144
+ concepts=[concept] + additional_outputs + parent_concepts + targets,
145
+ environment=environment,
146
+ ),
143
147
  )
144
148
  if not non_equivalent_optional:
145
149
  logger.info(
146
150
  f"{padding(depth)}{LOGGER_PREFIX} no optional concepts, returning window node"
147
151
  )
152
+ # prune outputs if we don't need join keys
153
+ window_node.set_output_concepts([concept] + additional_outputs + targets)
148
154
  return window_node
149
155
 
150
156
  missing_optional = [
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, ConfigDict, Field
2
2
 
3
+ from trilogy.core.exceptions import UnresolvableQueryException
3
4
  from trilogy.core.models.build import BuildConcept, BuildWhereClause
4
5
  from trilogy.core.models.build_environment import BuildEnvironment
5
6
  from trilogy.core.models.environment import Environment
@@ -18,7 +19,7 @@ class History(BaseModel):
18
19
  base_environment: Environment
19
20
  history: dict[str, StrategyNode | None] = Field(default_factory=dict)
20
21
  select_history: dict[str, StrategyNode | None] = Field(default_factory=dict)
21
- started: set[str] = Field(default_factory=set)
22
+ started: dict[str, int] = Field(default_factory=dict)
22
23
  model_config = ConfigDict(arbitrary_types_allowed=True)
23
24
 
24
25
  def _concepts_to_lookup(
@@ -27,13 +28,10 @@ class History(BaseModel):
27
28
  accept_partial: bool,
28
29
  conditions: BuildWhereClause | None = None,
29
30
  ) -> str:
31
+ base = sorted([c.address for c in search])
30
32
  if conditions:
31
- return (
32
- "-".join([c.address for c in search])
33
- + str(accept_partial)
34
- + str(conditions)
35
- )
36
- return "-".join([c.address for c in search]) + str(accept_partial)
33
+ return "-".join(base) + str(accept_partial) + str(conditions)
34
+ return "-".join(base) + str(accept_partial)
37
35
 
38
36
  def search_to_history(
39
37
  self,
@@ -80,13 +78,19 @@ class History(BaseModel):
80
78
  accept_partial: bool = False,
81
79
  conditions: BuildWhereClause | None = None,
82
80
  ):
83
- self.started.add(
84
- self._concepts_to_lookup(
85
- search,
86
- accept_partial=accept_partial,
87
- conditions=conditions,
88
- )
81
+ key = self._concepts_to_lookup(
82
+ search,
83
+ accept_partial=accept_partial,
84
+ conditions=conditions,
89
85
  )
86
+ if key in self.started:
87
+ self.started[key] += 1
88
+ else:
89
+ self.started[key] = 1
90
+ if self.started[key] > 5:
91
+ raise UnresolvableQueryException(
92
+ f"Was unable to resolve datasources to serve this query from model; unresolvable set was {search}. You may be querying unrelated concepts."
93
+ )
90
94
 
91
95
  def log_end(
92
96
  self,
@@ -94,13 +98,13 @@ class History(BaseModel):
94
98
  accept_partial: bool = False,
95
99
  conditions: BuildWhereClause | None = None,
96
100
  ):
97
- self.started.discard(
98
- self._concepts_to_lookup(
99
- search,
100
- accept_partial=accept_partial,
101
- conditions=conditions,
102
- )
101
+ key = self._concepts_to_lookup(
102
+ search,
103
+ accept_partial=accept_partial,
104
+ conditions=conditions,
103
105
  )
106
+ if key in self.started:
107
+ del self.started[key]
104
108
 
105
109
  def check_started(
106
110
  self,
@@ -458,7 +458,25 @@ def filter_item_to_concept(
458
458
  metadata: Metadata | None = None,
459
459
  ) -> Concept:
460
460
  fmetadata = metadata or Metadata()
461
- cparent = environment.concepts[parent.content.address]
461
+ if isinstance(parent.content, ConceptRef):
462
+ cparent = environment.concepts[parent.content.address]
463
+ elif isinstance(
464
+ parent.content,
465
+ (
466
+ FilterItem,
467
+ AggregateWrapper,
468
+ FunctionCallWrapper,
469
+ WindowItem,
470
+ Function,
471
+ ListWrapper,
472
+ MapWrapper,
473
+ ),
474
+ ):
475
+ cparent = arbitrary_to_concept(parent.content, environment, namespace=namespace)
476
+ else:
477
+ raise NotImplementedError(
478
+ f"Filter item with non ref content {parent.content} not yet supported"
479
+ )
462
480
  modifiers = get_upstream_modifiers(
463
481
  cparent.concept_arguments, environment=environment
464
482
  )
@@ -494,24 +512,6 @@ def window_item_to_concept(
494
512
  metadata: Metadata | None = None,
495
513
  ) -> Concept:
496
514
  fmetadata = metadata or Metadata()
497
- # if isinstance(
498
- # parent.content,
499
- # (
500
- # AggregateWrapper
501
- # | FunctionCallWrapper
502
- # | WindowItem
503
- # | FilterItem
504
- # | Function
505
- # | ListWrapper
506
- # | MapWrapper
507
- # ),
508
- # ):
509
- # new_parent = arbitrary_to_concept(
510
- # parent.content, environment=environment, namespace=namespace
511
- # )
512
- # environment.add_concept(new_parent)
513
- # parent = parent.model_copy(update={"content": new_parent.reference})
514
-
515
515
  if not isinstance(parent.content, ConceptRef):
516
516
  raise NotImplementedError(
517
517
  f"Window function wiht non ref content {parent.content} not yet supported"
@@ -523,16 +523,26 @@ def window_item_to_concept(
523
523
  local_purpose, keys = get_purpose_and_keys(None, (bcontent,), environment)
524
524
  else:
525
525
  local_purpose = Purpose.PROPERTY
526
- keys = {
527
- bcontent.address,
528
- }
526
+ keys = Grain.from_concepts(
527
+ [bcontent.address] + [y.address for y in parent.over], environment
528
+ ).components
529
529
 
530
+ # when including the order by in discovery grain
530
531
  if parent.order_by:
531
532
  grain_components = parent.over + [bcontent.output]
532
533
  for item in parent.order_by:
533
- grain_components += item.concept_arguments
534
+ # confirm that it's not just an aggregate at the grain of the stuff we're already keying of of
535
+ # in which case we can ignore contributions
536
+ if (
537
+ isinstance(item.expr, AggregateWrapper)
538
+ and set([x.address for x in item.expr.by]) == keys
539
+ ):
540
+ continue
541
+ else:
542
+ grain_components += item.concept_arguments
534
543
  else:
535
544
  grain_components = parent.over + [bcontent.output]
545
+
536
546
  final_grain = Grain.from_concepts(grain_components, environment)
537
547
  modifiers = get_upstream_modifiers(bcontent.concept_arguments, environment)
538
548
  datatype = parent.content.datatype
@@ -651,7 +661,9 @@ def rowset_concept(
651
661
  orig_concept = environment.concepts[orig_address.address]
652
662
  name = orig_concept.name
653
663
  if isinstance(orig_concept.lineage, FilterItem):
654
- if orig_concept.lineage.where == rowset.select.where_clause:
664
+ if orig_concept.lineage.where == rowset.select.where_clause and isinstance(
665
+ orig_concept.lineage.content, (ConceptRef, Concept)
666
+ ):
655
667
  name = environment.concepts[orig_concept.lineage.content.address].name
656
668
  base_namespace = (
657
669
  f"{rowset.name}.{orig_concept.namespace}"
@@ -761,7 +773,10 @@ def arbitrary_to_concept(
761
773
  )
762
774
  elif isinstance(parent, FilterItem):
763
775
  if not name:
764
- name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{parent.content.name}_{string_to_hash(str(parent))}"
776
+ if isinstance(parent.content, ConceptRef):
777
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{parent.content.name}_{string_to_hash(str(parent))}"
778
+ else:
779
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{string_to_hash(str(parent))}"
765
780
  return filter_item_to_concept(
766
781
  parent,
767
782
  name,