pytrilogy 0.0.3.48__tar.gz → 0.0.3.51__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.48/pytrilogy.egg-info → pytrilogy-0.0.3.51}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_functions.py +4 -0
  4. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_parse_engine.py +23 -1
  5. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/enums.py +3 -0
  7. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/functions.py +22 -0
  8. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/author.py +6 -2
  9. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/build.py +15 -3
  10. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/execute.py +4 -2
  11. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/concept_strategies_v3.py +1 -1
  12. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/common.py +3 -4
  13. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/filter_node.py +142 -91
  14. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/group_node.py +3 -4
  15. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/base_node.py +4 -1
  16. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/statements/author.py +0 -2
  17. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/base.py +7 -2
  18. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/bigquery.py +2 -0
  19. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/duckdb.py +2 -0
  20. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/common.py +25 -15
  21. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/parse_engine.py +35 -7
  22. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/trilogy.lark +10 -4
  23. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/LICENSE.md +0 -0
  24. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/README.md +0 -0
  25. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/pyproject.toml +0 -0
  26. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/pytrilogy.egg-info/SOURCES.txt +0 -0
  27. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/pytrilogy.egg-info/dependency_links.txt +0 -0
  28. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/pytrilogy.egg-info/entry_points.txt +0 -0
  29. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/pytrilogy.egg-info/requires.txt +0 -0
  30. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/pytrilogy.egg-info/top_level.txt +0 -0
  31. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/setup.cfg +0 -0
  32. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/setup.py +0 -0
  33. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_datatypes.py +0 -0
  34. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_declarations.py +0 -0
  35. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_derived_concepts.py +0 -0
  36. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_discovery_nodes.py +0 -0
  37. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_enums.py +0 -0
  38. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_environment.py +0 -0
  39. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_executor.py +0 -0
  40. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_failure.py +0 -0
  41. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_imports.py +0 -0
  42. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_metadata.py +0 -0
  43. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_models.py +0 -0
  44. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_multi_join_assignments.py +0 -0
  45. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_parsing.py +0 -0
  46. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_parsing_failures.py +0 -0
  47. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_query_render.py +0 -0
  50. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_select.py +0 -0
  51. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_show.py +0 -0
  52. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_statements.py +0 -0
  53. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_typing.py +0 -0
  54. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_undefined_concept.py +0 -0
  55. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_user_functions.py +0 -0
  56. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/tests/test_where_clause.py +0 -0
  57. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/authoring/__init__.py +0 -0
  58. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/compiler.py +0 -0
  59. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/constants.py +0 -0
  60. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/__init__.py +0 -0
  61. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/constants.py +0 -0
  62. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/env_processor.py +0 -0
  63. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/environment_helpers.py +0 -0
  64. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/ergonomics.py +0 -0
  65. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/exceptions.py +0 -0
  66. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/graph_models.py +0 -0
  67. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/internal.py +0 -0
  68. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/__init__.py +0 -0
  69. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/build_environment.py +0 -0
  70. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/core.py +0 -0
  71. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/datasource.py +0 -0
  72. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/models/environment.py +0 -0
  73. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/optimization.py +0 -0
  74. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/optimizations/__init__.py +0 -0
  75. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/optimizations/base_optimization.py +0 -0
  76. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/optimizations/inline_datasource.py +0 -0
  77. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  78. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/__init__.py +0 -0
  79. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/graph_utils.py +0 -0
  80. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/__init__.py +0 -0
  81. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  82. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  83. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  84. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  85. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  86. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  87. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  88. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  89. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/select_node.py +0 -0
  90. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  91. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/union_node.py +0 -0
  92. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  93. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/node_generators/window_node.py +0 -0
  94. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/__init__.py +0 -0
  95. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/filter_node.py +0 -0
  96. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/group_node.py +0 -0
  97. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/merge_node.py +0 -0
  98. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  99. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/union_node.py +0 -0
  100. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  101. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/nodes/window_node.py +0 -0
  102. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/processing/utility.py +0 -0
  103. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/query_processor.py +0 -0
  104. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/statements/__init__.py +0 -0
  105. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/statements/build.py +0 -0
  106. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/statements/common.py +0 -0
  107. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/core/statements/execute.py +0 -0
  108. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/__init__.py +0 -0
  109. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/common.py +0 -0
  110. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/config.py +0 -0
  111. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/dataframe.py +0 -0
  112. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/enums.py +0 -0
  113. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/postgres.py +0 -0
  114. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/presto.py +0 -0
  115. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/snowflake.py +0 -0
  116. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/dialect/sql_server.py +0 -0
  117. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/engine.py +0 -0
  118. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/executor.py +0 -0
  119. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/hooks/__init__.py +0 -0
  120. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/hooks/base_hook.py +0 -0
  121. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/hooks/graph_hook.py +0 -0
  122. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/hooks/query_debugger.py +0 -0
  123. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/metadata/__init__.py +0 -0
  124. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parser.py +0 -0
  125. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/__init__.py +0 -0
  126. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/config.py +0 -0
  127. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/exceptions.py +0 -0
  128. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/helpers.py +0 -0
  129. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/parsing/render.py +0 -0
  130. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/py.typed +0 -0
  131. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/render.py +0 -0
  132. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/scripts/__init__.py +0 -0
  133. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/scripts/trilogy.py +0 -0
  134. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/std/__init__.py +0 -0
  135. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/std/date.preql +0 -0
  136. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/std/display.preql +0 -0
  137. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/std/geography.preql +0 -0
  138. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/std/money.preql +0 -0
  139. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/std/report.preql +0 -0
  140. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.51}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.48
3
+ Version: 0.0.3.51
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.48
3
+ Version: 0.0.3.51
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -214,6 +214,8 @@ def test_math_functions(test_environment):
214
214
  property order_id.order_nested <- revenue * 2/2;
215
215
  property order_id.rounded <- round(revenue + 2.01,2);
216
216
  property order_id.rounded_default <- round(revenue + 2.01);
217
+ property order_id.floor <- floor(revenue + 2.01);
218
+ property order_id.ceil <- ceil(revenue + 2.01);
217
219
  constant random <- random(1);
218
220
  select
219
221
  order_id,
@@ -224,6 +226,8 @@ def test_math_functions(test_environment):
224
226
  order_add,
225
227
  rounded,
226
228
  rounded_default,
229
+ floor,
230
+ ceil,
227
231
  random,
228
232
  ;"""
229
233
  env, parsed = parse(declarations, environment=test_environment)
@@ -2,7 +2,12 @@ from pytest import raises
2
2
 
3
3
  from trilogy.core.exceptions import UndefinedConceptException
4
4
  from trilogy.core.models.environment import Environment
5
- from trilogy.parsing.parse_engine import PARSER, ParseToObjects, unpack_visit_error
5
+ from trilogy.parsing.parse_engine import (
6
+ PARSER,
7
+ InvalidSyntaxException,
8
+ ParseToObjects,
9
+ unpack_visit_error,
10
+ )
6
11
 
7
12
  TEXT = """
8
13
  const a <- 1;
@@ -29,3 +34,20 @@ def test_parser():
29
34
  with raises(UndefinedConceptException):
30
35
  unpack_visit_error(e)
31
36
  assert failed
37
+
38
+
39
+ TEXT2 = """
40
+ const a <- 1;
41
+
42
+ select
43
+ a,
44
+ FROM a
45
+ ;
46
+ """
47
+
48
+
49
+ def test_from_error():
50
+ env = Environment()
51
+
52
+ with raises(InvalidSyntaxException):
53
+ env.parse(TEXT2)
@@ -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.48"
7
+ __version__ = "0.0.3.51"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -131,6 +131,7 @@ class FunctionType(Enum):
131
131
  CONSTANT = "constant"
132
132
  COALESCE = "coalesce"
133
133
  IS_NULL = "isnull"
134
+ NULLIF = "nullif"
134
135
  BOOL = "bool"
135
136
 
136
137
  # COMPLEX
@@ -156,6 +157,8 @@ class FunctionType(Enum):
156
157
  ABS = "abs"
157
158
  SQRT = "sqrt"
158
159
  RANDOM = "random"
160
+ FLOOR = "floor"
161
+ CEIL = "ceil"
159
162
 
160
163
  # Aggregates
161
164
  ## group is not a real aggregate - it just means group by this + some other set of fields
@@ -279,6 +279,12 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
279
279
  output_purpose=Purpose.PROPERTY,
280
280
  arg_count=1,
281
281
  ),
282
+ FunctionType.NULLIF: FunctionConfig(
283
+ valid_inputs={*DataType},
284
+ output_purpose=Purpose.PROPERTY,
285
+ output_type_function=lambda args: get_output_type_at_index(args, 0),
286
+ arg_count=2,
287
+ ),
282
288
  FunctionType.COALESCE: FunctionConfig(
283
289
  valid_inputs={*DataType},
284
290
  output_purpose=Purpose.PROPERTY,
@@ -637,6 +643,22 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
637
643
  output_type_function=lambda args: get_output_type_at_index(args, 0),
638
644
  arg_count=2,
639
645
  ),
646
+ FunctionType.FLOOR: FunctionConfig(
647
+ valid_inputs=[
648
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
649
+ ],
650
+ output_purpose=Purpose.PROPERTY,
651
+ output_type=DataType.INTEGER,
652
+ arg_count=1,
653
+ ),
654
+ FunctionType.CEIL: FunctionConfig(
655
+ valid_inputs=[
656
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
657
+ ],
658
+ output_purpose=Purpose.PROPERTY,
659
+ output_type=DataType.INTEGER,
660
+ arg_count=1,
661
+ ),
640
662
  FunctionType.CUSTOM: FunctionConfig(
641
663
  output_purpose=Purpose.PROPERTY,
642
664
  arg_count=InfiniteFunctionArgs,
@@ -1945,11 +1945,15 @@ class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
1945
1945
 
1946
1946
  @property
1947
1947
  def output_datatype(self):
1948
- return self.content.datatype
1948
+ return arg_to_datatype(self.content)
1949
1949
 
1950
1950
  @property
1951
1951
  def concept_arguments(self):
1952
- return [self.content] + self.where.concept_arguments
1952
+ if isinstance(self.content, ConceptRef):
1953
+ return [self.content] + self.where.concept_arguments
1954
+ elif isinstance(self.content, ConceptArgs):
1955
+ return self.content.concept_arguments + self.where.concept_arguments
1956
+ return self.where.concept_arguments
1953
1957
 
1954
1958
 
1955
1959
  class RowsetLineage(Namespaced, Mergeable, BaseModel):
@@ -1173,7 +1173,7 @@ class BuildAggregateWrapper(BuildConceptArgs, DataTyped, BaseModel):
1173
1173
 
1174
1174
 
1175
1175
  class BuildFilterItem(BuildConceptArgs, BaseModel):
1176
- content: BuildConcept
1176
+ content: "BuildExpr"
1177
1177
  where: BuildWhereClause
1178
1178
 
1179
1179
  def __str__(self):
@@ -1181,15 +1181,27 @@ class BuildFilterItem(BuildConceptArgs, BaseModel):
1181
1181
 
1182
1182
  @property
1183
1183
  def output_datatype(self):
1184
- return self.content.datatype
1184
+ return arg_to_datatype(self.content)
1185
1185
 
1186
1186
  @property
1187
1187
  def output_purpose(self):
1188
1188
  return self.content.purpose
1189
1189
 
1190
+ @property
1191
+ def content_concept_arguments(self):
1192
+ if isinstance(self.content, BuildConcept):
1193
+ return [self.content]
1194
+ elif isinstance(self.content, BuildConceptArgs):
1195
+ return self.content.concept_arguments
1196
+ return []
1197
+
1190
1198
  @property
1191
1199
  def concept_arguments(self):
1192
- return [self.content] + self.where.concept_arguments
1200
+ if isinstance(self.content, BuildConcept):
1201
+ return [self.content] + self.where.concept_arguments
1202
+ elif isinstance(self.content, BuildConceptArgs):
1203
+ return self.content.concept_arguments + self.where.concept_arguments
1204
+ return self.where.concept_arguments
1193
1205
 
1194
1206
 
1195
1207
  class BuildRowsetLineage(BuildConceptArgs, BaseModel):
@@ -695,7 +695,7 @@ class QueryDatasource(BaseModel):
695
695
  "can only merge two datasources if the force_group flag is the same"
696
696
  )
697
697
  logger.debug(
698
- f"{LOGGER_PREFIX} merging {self.name} with"
698
+ f"[Query Datasource] merging {self.name} with"
699
699
  f" {[c.address for c in self.output_concepts]} concepts and"
700
700
  f" {other.name} with {[c.address for c in other.output_concepts]} concepts"
701
701
  )
@@ -759,7 +759,9 @@ class QueryDatasource(BaseModel):
759
759
  hidden_concepts=hidden,
760
760
  ordering=self.ordering,
761
761
  )
762
-
762
+ logger.debug(
763
+ f"[Query Datasource] merged with {[c.address for c in qds.output_concepts]} concepts"
764
+ )
763
765
  return qds
764
766
 
765
767
  @property
@@ -319,7 +319,7 @@ def generate_node(
319
319
  ]
320
320
 
321
321
  logger.info(
322
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating aggregate node with {[x.address for x in agg_optional]}"
322
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating aggregate node with {[x for x in agg_optional]}"
323
323
  )
324
324
  return gen_group_node(
325
325
  concept,
@@ -67,14 +67,14 @@ def resolve_condition_parent_concepts(
67
67
  def resolve_filter_parent_concepts(
68
68
  concept: BuildConcept,
69
69
  environment: BuildEnvironment,
70
- ) -> Tuple[BuildConcept, List[BuildConcept], List[Tuple[BuildConcept, ...]]]:
70
+ ) -> Tuple[List[BuildConcept], List[Tuple[BuildConcept, ...]]]:
71
71
  if not isinstance(concept.lineage, (BuildFilterItem,)):
72
72
  raise ValueError(
73
73
  f"Concept {concept} lineage is not filter item, is {type(concept.lineage)}"
74
74
  )
75
75
  direct_parent = concept.lineage.content
76
76
  base_existence = []
77
- base_rows = [direct_parent]
77
+ base_rows = [direct_parent] if isinstance(direct_parent, BuildConcept) else []
78
78
  condition_rows, condition_existence = resolve_condition_parent_concepts(
79
79
  concept.lineage.where
80
80
  )
@@ -90,11 +90,10 @@ def resolve_filter_parent_concepts(
90
90
 
91
91
  if concept.lineage.where.existence_arguments:
92
92
  return (
93
- concept.lineage.content,
94
93
  unique(base_rows, "address"),
95
94
  base_existence,
96
95
  )
97
- return concept.lineage.content, unique(base_rows, "address"), []
96
+ return unique(base_rows, "address"), []
98
97
 
99
98
 
100
99
  def gen_property_enrichment_node(
@@ -25,71 +25,140 @@ LOGGER_PREFIX = "[GEN_FILTER_NODE]"
25
25
  FILTER_TYPES = (BuildFilterItem,)
26
26
 
27
27
 
28
- def gen_filter_node(
29
- concept: BuildConcept,
28
+ def pushdown_filter_to_parent(
30
29
  local_optional: List[BuildConcept],
31
- environment: BuildEnvironment,
32
- g,
30
+ conditions: BuildWhereClause | None,
31
+ filter_where: BuildWhereClause,
32
+ same_filter_optional: list[BuildConcept],
33
33
  depth: int,
34
- source_concepts,
35
- history: History | None = None,
34
+ ) -> bool:
35
+ optimized_pushdown = False
36
+ if not is_scalar_condition(filter_where.conditional):
37
+ optimized_pushdown = False
38
+ elif not local_optional:
39
+ optimized_pushdown = True
40
+ elif conditions and conditions == filter_where:
41
+ logger.info(
42
+ f"{padding(depth)}{LOGGER_PREFIX} query conditions are the same as filter conditions, can optimize across all concepts"
43
+ )
44
+ optimized_pushdown = True
45
+ elif same_filter_optional == local_optional:
46
+ logger.info(
47
+ f"{padding(depth)}{LOGGER_PREFIX} all optional concepts are included in the filter, can optimize across all concepts"
48
+ )
49
+ optimized_pushdown = True
50
+
51
+ return optimized_pushdown
52
+
53
+
54
+ def build_parent_concepts(
55
+ concept: BuildConcept,
56
+ environment: BuildEnvironment,
57
+ local_optional: List[BuildConcept],
36
58
  conditions: BuildWhereClause | None = None,
37
- ) -> StrategyNode | None:
38
- immediate_parent, parent_row_concepts, parent_existence_concepts = (
39
- resolve_filter_parent_concepts(concept, environment)
59
+ depth: int = 0,
60
+ ):
61
+ parent_row_concepts, parent_existence_concepts = resolve_filter_parent_concepts(
62
+ concept, environment
40
63
  )
41
64
  if not isinstance(concept.lineage, FILTER_TYPES):
42
65
  raise SyntaxError('Filter node must have a filter type lineage"')
43
- where = concept.lineage.where
66
+ filter_where = concept.lineage.where
44
67
 
45
- optional_included: list[BuildConcept] = []
68
+ same_filter_optional: list[BuildConcept] = []
46
69
 
47
70
  for x in local_optional:
48
71
  if isinstance(x.lineage, FILTER_TYPES):
49
- if concept.lineage.where == where:
72
+ if concept.lineage.where == filter_where:
50
73
  logger.info(
51
- f"{padding(depth)}{LOGGER_PREFIX} fetching {x.lineage.content.address} as optional parent from optional {x} with same filter conditions "
74
+ f"{padding(depth)}{LOGGER_PREFIX} fetching parents for peer {x} with same filter conditions"
52
75
  )
53
- if x.lineage.content.address not in parent_row_concepts:
54
- parent_row_concepts.append(x.lineage.content)
55
- optional_included.append(x)
76
+
77
+ for arg in x.lineage.content_concept_arguments:
78
+ if arg.address not in parent_row_concepts:
79
+ parent_row_concepts.append(arg)
80
+ same_filter_optional.append(x)
56
81
  continue
57
- if conditions and conditions == where:
58
- optional_included.append(x)
82
+ elif conditions and conditions == filter_where:
83
+ same_filter_optional.append(x)
59
84
 
60
85
  # sometimes, it's okay to include other local optional above the filter
61
86
  # in case it is, prep our list
62
87
  extra_row_level_optional: list[BuildConcept] = []
88
+
63
89
  for x in local_optional:
64
- if x.address in optional_included:
90
+ if x.address in same_filter_optional:
65
91
  continue
66
92
  extra_row_level_optional.append(x)
93
+ is_optimized_pushdown = pushdown_filter_to_parent(
94
+ local_optional, conditions, filter_where, same_filter_optional, depth
95
+ )
96
+ if not is_optimized_pushdown:
97
+ parent_row_concepts += extra_row_level_optional
98
+ return (
99
+ parent_row_concepts,
100
+ parent_existence_concepts,
101
+ same_filter_optional,
102
+ is_optimized_pushdown,
103
+ )
67
104
 
68
- # this flag controls whether we materialize the filter as a where on the prior CTE
69
- # or do the filtering inline as a case statement
70
- optimized_pushdown = False
71
- if not is_scalar_condition(where.conditional):
72
- optimized_pushdown = False
73
- elif not local_optional:
74
- optimized_pushdown = True
75
- elif conditions and conditions == where:
105
+
106
+ def add_existence_sources(
107
+ core_parent_nodes: list[StrategyNode],
108
+ parent_existence_concepts: list[tuple[BuildConcept, ...]],
109
+ source_concepts,
110
+ environment,
111
+ g,
112
+ depth,
113
+ history,
114
+ ):
115
+ for existence_tuple in parent_existence_concepts:
116
+ if not existence_tuple:
117
+ continue
76
118
  logger.info(
77
- f"{padding(depth)}{LOGGER_PREFIX} query conditions are the same as filter conditions, can optimize across all concepts"
119
+ f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in existence_tuple]}"
78
120
  )
79
- optimized_pushdown = True
80
- elif optional_included == local_optional:
81
- logger.info(
82
- f"{padding(depth)}{LOGGER_PREFIX} all optional concepts are included in the filter, can optimize across all concepts"
121
+ parent_existence = source_concepts(
122
+ mandatory_list=list(existence_tuple),
123
+ environment=environment,
124
+ g=g,
125
+ depth=depth + 1,
126
+ history=history,
83
127
  )
84
- optimized_pushdown = True
85
- logger.info(
86
- f"{padding(depth)}{LOGGER_PREFIX} filter `{concept}` condition `{concept.lineage.where}` derived from {immediate_parent.address} row parents {[x.address for x in parent_row_concepts]} and {[[y.address] for x in parent_existence_concepts for y in x]} existence parents"
128
+ if not parent_existence:
129
+ logger.info(
130
+ f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
131
+ )
132
+ return None
133
+ core_parent_nodes.append(parent_existence)
134
+
135
+
136
+ def gen_filter_node(
137
+ concept: BuildConcept,
138
+ local_optional: List[BuildConcept],
139
+ environment: BuildEnvironment,
140
+ g,
141
+ depth: int,
142
+ source_concepts,
143
+ history: History | None = None,
144
+ conditions: BuildWhereClause | None = None,
145
+ ) -> StrategyNode | None:
146
+ if not isinstance(concept.lineage, FILTER_TYPES):
147
+ raise SyntaxError('Filter node must have a filter type lineage"')
148
+ where = concept.lineage.where
149
+
150
+ (
151
+ parent_row_concepts,
152
+ parent_existence_concepts,
153
+ same_filter_optional,
154
+ optimized_pushdown,
155
+ ) = build_parent_concepts(
156
+ concept,
157
+ environment=environment,
158
+ local_optional=local_optional,
159
+ conditions=conditions,
160
+ depth=depth,
87
161
  )
88
- # we'll populate this with the row parent
89
- # and the existence parent(s)
90
- core_parents = []
91
- if not optimized_pushdown:
92
- parent_row_concepts += extra_row_level_optional
93
162
 
94
163
  row_parent: StrategyNode = source_concepts(
95
164
  mandatory_list=parent_row_concepts,
@@ -100,27 +169,19 @@ def gen_filter_node(
100
169
  conditions=conditions,
101
170
  )
102
171
 
172
+ core_parent_nodes: list[StrategyNode] = []
103
173
  flattened_existence = [x for y in parent_existence_concepts for x in y]
104
174
  if parent_existence_concepts:
105
- for existence_tuple in parent_existence_concepts:
106
- if not existence_tuple:
107
- continue
108
- logger.info(
109
- f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in existence_tuple]}"
110
- )
111
- parent_existence = source_concepts(
112
- mandatory_list=list(existence_tuple),
113
- environment=environment,
114
- g=g,
115
- depth=depth + 1,
116
- history=history,
117
- )
118
- if not parent_existence:
119
- logger.info(
120
- f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
121
- )
122
- return None
123
- core_parents.append(parent_existence)
175
+ add_existence_sources(
176
+ core_parent_nodes,
177
+ parent_existence_concepts,
178
+ source_concepts,
179
+ environment,
180
+ g,
181
+ depth,
182
+ history,
183
+ )
184
+
124
185
  if not row_parent:
125
186
  logger.info(
126
187
  f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
@@ -129,7 +190,7 @@ def gen_filter_node(
129
190
 
130
191
  if optimized_pushdown:
131
192
  logger.info(
132
- f"{padding(depth)}{LOGGER_PREFIX} returning optimized filter node with pushdown to parent with condition {where.conditional}"
193
+ f"{padding(depth)}{LOGGER_PREFIX} returning optimized filter node with pushdown to parent with condition {where.conditional} across {[concept] + same_filter_optional + row_parent.output_concepts} "
133
194
  )
134
195
  if isinstance(row_parent, SelectNode):
135
196
  logger.info(
@@ -137,7 +198,9 @@ def gen_filter_node(
137
198
  )
138
199
  parent = StrategyNode(
139
200
  input_concepts=row_parent.output_concepts,
140
- output_concepts=[concept] + row_parent.output_concepts,
201
+ output_concepts=[concept]
202
+ + same_filter_optional
203
+ + row_parent.output_concepts,
141
204
  environment=row_parent.environment,
142
205
  parents=[row_parent],
143
206
  depth=row_parent.depth,
@@ -146,46 +209,34 @@ def gen_filter_node(
146
209
  )
147
210
  else:
148
211
  parent = row_parent
149
-
150
- expected_output = [concept] + [
151
- x
152
- for x in local_optional
153
- if x.address in [y for y in parent.output_concepts]
154
- or x.address in [y for y in optional_included]
155
- ]
156
- parent.add_parents(core_parents)
212
+ parent.add_output_concepts([concept] + same_filter_optional)
213
+ parent.add_parents(core_parent_nodes)
157
214
  parent.add_condition(where.conditional)
158
- parent.add_existence_concepts(flattened_existence, False).set_output_concepts(
159
- expected_output, False
160
- )
215
+ parent.add_existence_concepts(flattened_existence, False)
161
216
  parent.grain = BuildGrain.from_concepts(
162
- (
163
- [environment.concepts[k] for k in immediate_parent.keys]
164
- if immediate_parent.keys
165
- else [immediate_parent]
166
- )
167
- + [
168
- x
169
- for x in local_optional
170
- if x.address in [y.address for y in parent.output_concepts]
171
- ],
217
+ parent.output_concepts,
172
218
  environment=environment,
173
219
  )
174
220
  parent.rebuild_cache()
175
221
  filter_node = parent
176
222
  else:
177
- core_parents.append(row_parent)
178
-
223
+ core_parent_nodes.append(row_parent)
224
+ filters = [concept] + same_filter_optional
225
+ parents_for_grain = [
226
+ x.lineage.content
227
+ for x in filters
228
+ if isinstance(x.lineage.content, BuildConcept)
229
+ ]
179
230
  filter_node = FilterNode(
180
231
  input_concepts=unique(
181
- [immediate_parent] + parent_row_concepts + flattened_existence,
232
+ parent_row_concepts + flattened_existence,
182
233
  "address",
183
234
  ),
184
- output_concepts=[concept, immediate_parent] + parent_row_concepts,
235
+ output_concepts=[concept] + same_filter_optional + parent_row_concepts,
185
236
  environment=environment,
186
- parents=core_parents,
237
+ parents=core_parent_nodes,
187
238
  grain=BuildGrain.from_concepts(
188
- [immediate_parent] + parent_row_concepts,
239
+ parents_for_grain + parent_row_concepts, environment=environment
189
240
  ),
190
241
  preexisting_conditions=conditions.conditional if conditions else None,
191
242
  )
@@ -211,7 +262,8 @@ def gen_filter_node(
211
262
  )
212
263
  enrich_node: StrategyNode = source_concepts( # this fetches the parent + join keys
213
264
  # to then connect to the rest of the query
214
- mandatory_list=[immediate_parent] + parent_row_concepts + local_optional,
265
+ mandatory_list=parent_row_concepts
266
+ + [x for x in local_optional if x.address not in filter_node.output_concepts],
215
267
  environment=environment,
216
268
  g=g,
217
269
  depth=depth + 1,
@@ -227,14 +279,13 @@ def gen_filter_node(
227
279
  f"{padding(depth)}{LOGGER_PREFIX} returning filter node and enrich node with {enrich_node.output_concepts} and {enrich_node.input_concepts}"
228
280
  )
229
281
  return MergeNode(
230
- input_concepts=[concept, immediate_parent] + local_optional,
282
+ input_concepts=filter_node.output_concepts + enrich_node.output_concepts,
231
283
  output_concepts=[
232
284
  concept,
233
285
  ]
234
286
  + local_optional,
235
287
  environment=environment,
236
288
  parents=[
237
- # this node fetches only what we need to filter
238
289
  filter_node,
239
290
  enrich_node,
240
291
  ],
@@ -51,6 +51,7 @@ def gen_group_node(
51
51
  ):
52
52
  grain_components = [environment.concepts[c] for c in concept.grain.components]
53
53
  parent_concepts += grain_components
54
+ build_grain_parents = BuildGrain.from_concepts(parent_concepts)
54
55
  output_concepts += grain_components
55
56
  for possible_agg in local_optional:
56
57
 
@@ -76,9 +77,7 @@ def gen_group_node(
76
77
  logger.info(
77
78
  f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
78
79
  )
79
- elif BuildGrain.from_concepts(agg_parents) == BuildGrain.from_concepts(
80
- parent_concepts
81
- ):
80
+ elif BuildGrain.from_concepts(agg_parents) == build_grain_parents:
82
81
  extra = [x for x in agg_parents if x.address not in parent_concepts]
83
82
  parent_concepts += extra
84
83
  output_concepts.append(possible_agg)
@@ -87,7 +86,7 @@ def gen_group_node(
87
86
  )
88
87
  else:
89
88
  logger.info(
90
- f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; mismatched grain {BuildGrain.from_concepts(agg_parents)} vs {BuildGrain.from_concepts(parent_concepts)}"
89
+ f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; mismatched parent grain {BuildGrain.from_concepts(agg_parents)} vs local parent {BuildGrain.from_concepts(parent_concepts)}"
91
90
  )
92
91
  if parent_concepts:
93
92
  logger.info(
@@ -194,15 +194,18 @@ class StrategyNode:
194
194
  if not self.parents:
195
195
  return
196
196
  non_hidden = set()
197
+ hidden = set()
197
198
  for x in self.parents:
198
199
  for z in x.usable_outputs:
199
200
  non_hidden.add(z.address)
200
201
  for psd in z.pseudonyms:
201
202
  non_hidden.add(psd)
203
+ for z in x.hidden_concepts:
204
+ hidden.add(z)
202
205
  if not all([x.address in non_hidden for x in self.input_concepts]):
203
206
  missing = [x for x in self.input_concepts if x.address not in non_hidden]
204
207
  raise ValueError(
205
- f"Invalid input concepts; {missing} are missing non-hidden parent nodes"
208
+ f"Invalid input concepts; {missing} are missing non-hidden parent nodes; have {non_hidden} and hidden {hidden}"
206
209
  )
207
210
 
208
211
  def add_parents(self, parents: list["StrategyNode"]):
@@ -162,9 +162,7 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
162
162
  output.local_concepts[x.content.address] = environment.concepts[
163
163
  x.content.address
164
164
  ]
165
-
166
165
  output.grain = output.calculate_grain(environment, output.local_concepts)
167
-
168
166
  output.validate_syntax(environment)
169
167
  return output
170
168