pytrilogy 0.0.2.3__tar.gz → 0.0.2.5__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 (105) hide show
  1. {pytrilogy-0.0.2.3/pytrilogy.egg-info → pytrilogy-0.0.2.5}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_parsing.py +41 -0
  4. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_where_clause.py +55 -2
  5. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/enums.py +1 -0
  7. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/functions.py +9 -1
  8. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/models.py +28 -7
  9. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/inline_datasource.py +6 -2
  10. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/predicate_pushdown.py +3 -44
  11. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/concept_strategies_v3.py +1 -0
  12. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/common.py +3 -3
  13. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/filter_node.py +20 -16
  14. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/group_node.py +28 -2
  15. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/utility.py +46 -0
  16. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/query_processor.py +7 -2
  17. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/base.py +34 -8
  18. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/bigquery.py +3 -1
  19. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/duckdb.py +5 -5
  20. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/postgres.py +3 -1
  21. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/presto.py +3 -1
  22. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/snowflake.py +3 -1
  23. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/sql_server.py +3 -1
  24. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/common.py +1 -0
  25. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/parse_engine.py +74 -2
  26. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/trilogy.lark +7 -3
  27. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/LICENSE.md +0 -0
  28. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/README.md +0 -0
  29. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pyproject.toml +0 -0
  30. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/SOURCES.txt +0 -0
  31. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/dependency_links.txt +0 -0
  32. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/entry_points.txt +0 -0
  33. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/requires.txt +0 -0
  34. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/top_level.txt +0 -0
  35. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/setup.cfg +0 -0
  36. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/setup.py +0 -0
  37. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_datatypes.py +0 -0
  38. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_declarations.py +0 -0
  39. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_derived_concepts.py +0 -0
  40. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_discovery_nodes.py +0 -0
  41. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_environment.py +0 -0
  42. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_functions.py +0 -0
  43. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_imports.py +0 -0
  44. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_metadata.py +0 -0
  45. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_models.py +0 -0
  46. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_multi_join_assignments.py +0 -0
  47. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_select.py +0 -0
  50. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_statements.py +0 -0
  51. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_undefined_concept.py +0 -0
  52. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/compiler.py +0 -0
  53. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/constants.py +0 -0
  54. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/__init__.py +0 -0
  55. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/constants.py +0 -0
  56. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/env_processor.py +0 -0
  57. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/environment_helpers.py +0 -0
  58. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/ergonomics.py +0 -0
  59. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/exceptions.py +0 -0
  60. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/graph_models.py +0 -0
  61. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/internal.py +0 -0
  62. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimization.py +0 -0
  63. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/__init__.py +0 -0
  64. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/base_optimization.py +0 -0
  65. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/inline_constant.py +0 -0
  66. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/__init__.py +0 -0
  67. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/graph_utils.py +0 -0
  68. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/__init__.py +0 -0
  69. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  70. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/group_node.py +0 -0
  71. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  72. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  73. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  74. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  75. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/select_node.py +0 -0
  76. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  77. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/window_node.py +0 -0
  78. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/__init__.py +0 -0
  79. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/base_node.py +0 -0
  80. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/filter_node.py +0 -0
  81. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/merge_node.py +0 -0
  82. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  83. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  84. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/window_node.py +0 -0
  85. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/__init__.py +0 -0
  86. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/common.py +0 -0
  87. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/config.py +0 -0
  88. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/enums.py +0 -0
  89. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/engine.py +0 -0
  90. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/executor.py +0 -0
  91. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/__init__.py +0 -0
  92. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/base_hook.py +0 -0
  93. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/graph_hook.py +0 -0
  94. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/query_debugger.py +0 -0
  95. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/metadata/__init__.py +0 -0
  96. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parser.py +0 -0
  97. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/__init__.py +0 -0
  98. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/config.py +0 -0
  99. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/exceptions.py +0 -0
  100. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/helpers.py +0 -0
  101. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/render.py +0 -0
  102. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/py.typed +0 -0
  103. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/scripts/__init__.py +0 -0
  104. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/scripts/trilogy.py +0 -0
  105. {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.3
3
+ Version: 0.0.2.5
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.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.3
3
+ Version: 0.0.2.5
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -407,3 +407,44 @@ property id.labels map<string, int>;
407
407
  )
408
408
  assert env.concepts["labels"].datatype.key_type == DataType.STRING
409
409
  assert env.concepts["labels"].datatype.value_type == DataType.INTEGER
410
+
411
+
412
+ def test_map_string_access():
413
+ env, parsed = parse_text(
414
+ """
415
+ const labels <- { 'a': 1, 'b': 2, 'c': 3 };
416
+
417
+
418
+ select
419
+ labels['a'] as label_a,
420
+ ;
421
+
422
+ """
423
+ )
424
+ assert env.concepts["labels"].datatype.key_type == DataType.STRING
425
+ assert env.concepts["labels"].datatype.value_type == DataType.INTEGER
426
+
427
+
428
+ def test_struct_attr_access():
429
+
430
+ text = """
431
+ const labels <- struct(a=1, b=2, c=3);
432
+
433
+
434
+ select
435
+ labels,
436
+ labels.a as label_a
437
+ ;
438
+
439
+ """
440
+ env, parsed = parse_text(text)
441
+ assert env.concepts["labels"].datatype.fields_map["a"] == 1
442
+
443
+ assert env.concepts["labels.a"].concept_arguments[0].name == "labels"
444
+
445
+ results = Dialects.DUCK_DB.default_executor().execute_text(text)[0]
446
+
447
+ assert results.fetchall()[0] == (
448
+ {"a": 1, "b": 2, "c": 3},
449
+ 1,
450
+ )
@@ -3,6 +3,7 @@ from trilogy.core.models import SelectStatement, Grain, Parenthetical
3
3
  from trilogy.core.query_processor import process_query
4
4
  from trilogy.dialect.base import BaseDialect
5
5
  from trilogy.parser import parse
6
+ from trilogy.core.processing.utility import is_scalar_condition
6
7
 
7
8
 
8
9
  def test_select_where(test_environment):
@@ -44,6 +45,8 @@ where
44
45
 
45
46
 
46
47
  def test_select_where_agg(test_environment):
48
+ from trilogy.hooks.query_debugger import DebuggingHook
49
+
47
50
  declarations = """
48
51
  property my_favorite_order_revenue <- filter revenue where order_id in (1,2,3);
49
52
 
@@ -54,10 +57,15 @@ select
54
57
 
55
58
 
56
59
  """
57
- env, parsed = parse(declarations, environment=test_environment)
60
+ env, parsed = parse(
61
+ declarations,
62
+ environment=test_environment,
63
+ )
58
64
  select: SelectStatement = parsed[-1]
59
65
 
60
- BaseDialect().compile_statement(process_query(test_environment, select))
66
+ BaseDialect().compile_statement(
67
+ process_query(test_environment, select, hooks=[DebuggingHook()])
68
+ )
61
69
 
62
70
 
63
71
  def test_select_where_joins(test_environment):
@@ -205,3 +213,48 @@ where
205
213
 
206
214
  """
207
215
  env, parsed = parse(declarations, environment=test_environment)
216
+
217
+
218
+ def test_where_scalar(test_environment):
219
+ declarations = """
220
+ select
221
+ category_name
222
+ where
223
+ count(order_id) > 1
224
+ ;
225
+ """
226
+ env, parsed = parse(declarations, environment=test_environment)
227
+ select: SelectStatement = parsed[-1]
228
+
229
+ assert is_scalar_condition(select.where_clause.conditional) is False
230
+ _ = BaseDialect().compile_statement(process_query(test_environment, select))
231
+
232
+
233
+ def test_case_where(test_environment):
234
+ from trilogy.hooks.query_debugger import DebuggingHook
235
+
236
+ declarations = """property order_id_even_name <- CASE
237
+ when order_id %2 = 0 then 'even'
238
+ else 'odd'
239
+ END;
240
+
241
+ const test <- 1;
242
+
243
+ auto order_even_class_filter <- filter category_id where order_id_even_name = 'even' and 1= test;
244
+
245
+ select
246
+ category_id,
247
+ category_name
248
+ where
249
+ category_name like '%abc%' and category_id not in order_even_class_filter
250
+ and category_id = test
251
+ ;"""
252
+ env, parsed = parse(declarations, environment=test_environment)
253
+ select: SelectStatement = parsed[-1]
254
+
255
+ query = BaseDialect().compile_statement(
256
+ process_query(test_environment, select, hooks=[DebuggingHook()])
257
+ )
258
+
259
+ # check to make sure our subselect is well-formed
260
+ assert "`category_id` not in (select" in query, query
@@ -4,6 +4,6 @@ from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.3"
7
+ __version__ = "0.0.2.5"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -121,6 +121,7 @@ class FunctionType(Enum):
121
121
  INDEX_ACCESS = "index_access"
122
122
  MAP_ACCESS = "map_access"
123
123
  ATTR_ACCESS = "attr_access"
124
+ STRUCT = "struct"
124
125
 
125
126
  # TEXT AND MAYBE MORE
126
127
  SPLIT = "split"
@@ -251,11 +251,19 @@ def MapAccess(args: list[Concept]):
251
251
  )
252
252
 
253
253
 
254
+ def get_attr_datatype(
255
+ arg: Concept, lookup
256
+ ) -> DataType | ListType | StructType | MapType | NumericType:
257
+ if isinstance(arg.datatype, StructType):
258
+ return arg_to_datatype(arg.datatype.fields_map[lookup])
259
+ return arg.datatype
260
+
261
+
254
262
  def AttrAccess(args: list[Concept]):
255
263
  return Function(
256
264
  operator=FunctionType.ATTR_ACCESS,
257
265
  arguments=args,
258
- output_datatype=args[0].field_map[args[1]].datatype, # type: ignore
266
+ output_datatype=get_attr_datatype(args[0], args[1]), # type: ignore
259
267
  output_purpose=Purpose.PROPERTY,
260
268
  valid_inputs=[
261
269
  {DataType.STRUCT},
@@ -296,7 +296,7 @@ class MapType(BaseModel):
296
296
 
297
297
  class StructType(BaseModel):
298
298
  fields: List[ALL_TYPES]
299
- fields_map: Dict[str, Concept] = Field(default_factory=dict)
299
+ fields_map: Dict[str, Concept | int | float | str] = Field(default_factory=dict)
300
300
 
301
301
  @property
302
302
  def data_type(self):
@@ -419,6 +419,23 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
419
419
  def __hash__(self):
420
420
  return hash(str(self))
421
421
 
422
+ @property
423
+ def is_aggregate(self):
424
+ if (
425
+ self.lineage
426
+ and isinstance(self.lineage, Function)
427
+ and self.lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
428
+ ):
429
+ return True
430
+ if (
431
+ self.lineage
432
+ and isinstance(self.lineage, AggregateWrapper)
433
+ and self.lineage.function.operator
434
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
435
+ ):
436
+ return True
437
+ return False
438
+
422
439
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
423
440
  if self.address == source.address:
424
441
  new = target.with_grain(self.grain.with_merge(source, target, modifiers))
@@ -2416,7 +2433,13 @@ class CTE(BaseModel):
2416
2433
  # if we've entirely removed the need to join to someplace to get the concept
2417
2434
  # drop the join as well.
2418
2435
  for removed_cte in removed:
2419
- still_required = any([removed_cte in x for x in self.source_map.values()])
2436
+ still_required = any(
2437
+ [
2438
+ removed_cte in x
2439
+ for x in self.source_map.values()
2440
+ or self.existence_source_map.values()
2441
+ ]
2442
+ )
2420
2443
  if not still_required:
2421
2444
  self.joins = [
2422
2445
  join
@@ -2434,6 +2457,7 @@ class CTE(BaseModel):
2434
2457
  candidates = [x.name for x in self.parent_ctes]
2435
2458
  self.base_name_override = candidates[0] if candidates else None
2436
2459
  self.base_alias_override = candidates[0] if candidates else None
2460
+ return True
2437
2461
 
2438
2462
  def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
2439
2463
  qds_being_inlined = parent.source
@@ -3298,7 +3322,7 @@ class Comparison(
3298
3322
  and self.operator == other.operator
3299
3323
  )
3300
3324
 
3301
- def inline_constant(self, constant: Concept) -> "Comparison":
3325
+ def inline_constant(self, constant: Concept):
3302
3326
  assert isinstance(constant.lineage, Function)
3303
3327
  new_val = constant.lineage.arguments[0]
3304
3328
  if isinstance(self.left, ConstantInlineable):
@@ -3315,10 +3339,7 @@ class Comparison(
3315
3339
  else:
3316
3340
  new_right = self.right
3317
3341
 
3318
- if self.right == constant:
3319
- new_right = new_val
3320
-
3321
- return Comparison(
3342
+ return self.__class__(
3322
3343
  left=new_left,
3323
3344
  right=new_right,
3324
3345
  operator=self.operator,
@@ -43,10 +43,14 @@ class InlineDatasource(OptimizationRule):
43
43
  continue
44
44
  root_outputs = {x.address for x in root.output_concepts}
45
45
  cte_outputs = {x.address for x in cte.output_columns}
46
+ inherited = {x for x, v in cte.source_map.items() if v}
46
47
  # cte_inherited_outputs = {x.address for x in parent_cte.output_columns if parent_cte.source_map.get(x.address)}
47
48
  grain_components = {x.address for x in root.grain.components}
48
- if not cte_outputs.issubset(root_outputs):
49
- self.log(f"Not all {parent_cte.name} outputs are found on datasource")
49
+ if not inherited.issubset(root_outputs):
50
+ cte_missing = inherited - root_outputs
51
+ self.log(
52
+ f"Not all {parent_cte.name} require inputs are found on datasource, missing {cte_missing}"
53
+ )
50
54
  continue
51
55
  if not grain_components.issubset(cte_outputs):
52
56
  self.log("Not all datasource components in cte outputs, forcing group")
@@ -6,16 +6,9 @@ from trilogy.core.models import (
6
6
  SubselectComparison,
7
7
  Comparison,
8
8
  Parenthetical,
9
- Function,
10
- FilterItem,
11
- MagicConstants,
12
- Concept,
13
- WindowItem,
14
- AggregateWrapper,
15
- DataType,
16
9
  )
17
10
  from trilogy.core.optimizations.base_optimization import OptimizationRule
18
- from trilogy.core.enums import FunctionClass
11
+ from trilogy.core.processing.utility import is_scalar_condition
19
12
 
20
13
 
21
14
  def decompose_condition(
@@ -56,40 +49,6 @@ def is_child_of(a, comparison):
56
49
  return base
57
50
 
58
51
 
59
- def is_basic(
60
- element: (
61
- int
62
- | str
63
- | float
64
- | list
65
- | WindowItem
66
- | FilterItem
67
- | Concept
68
- | Comparison
69
- | Conditional
70
- | Parenthetical
71
- | Function
72
- | AggregateWrapper
73
- | MagicConstants
74
- | DataType
75
- ),
76
- ) -> bool:
77
- if isinstance(element, Parenthetical):
78
- return is_basic(element.content)
79
- elif isinstance(element, SubselectComparison):
80
- return True
81
- elif isinstance(element, Comparison):
82
- return is_basic(element.left) and is_basic(element.right)
83
- elif isinstance(element, Function):
84
- if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
85
- return False
86
- elif isinstance(element, AggregateWrapper):
87
- return is_basic(element.function)
88
- elif isinstance(element, Conditional):
89
- return is_basic(element.left) and is_basic(element.right)
90
- return True
91
-
92
-
93
52
  class PredicatePushdown(OptimizationRule):
94
53
 
95
54
  def __init__(self, *args, **kwargs) -> None:
@@ -187,13 +146,13 @@ class PredicatePushdown(OptimizationRule):
187
146
  )
188
147
  optimized = False
189
148
  for candidate in candidates:
190
- if not is_basic(candidate):
149
+ if not is_scalar_condition(candidate):
191
150
  self.debug(
192
151
  f"Skipping {candidate} as not a basic [no aggregate, etc] condition"
193
152
  )
194
153
  continue
195
154
  self.log(
196
- f"Checking candidate {candidate}, {type(candidate)}, {is_basic(candidate)}"
155
+ f"Checking candidate {candidate}, {type(candidate)}, scalar: {is_scalar_condition(candidate)}"
197
156
  )
198
157
  for parent_cte in cte.parent_ctes:
199
158
  local_pushdown = self._check_parent(
@@ -489,6 +489,7 @@ def _search_concepts(
489
489
 
490
490
  found: set[str] = set()
491
491
  skip: set[str] = set()
492
+ virtual: set[str] = set()
492
493
  stack: List[StrategyNode] = []
493
494
  complete = ValidationResult.INCOMPLETE
494
495
 
@@ -47,7 +47,7 @@ def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
47
47
 
48
48
  def resolve_filter_parent_concepts(
49
49
  concept: Concept,
50
- ) -> Tuple[Concept, List[Concept], List[Concept]]:
50
+ ) -> Tuple[Concept, List[Concept], List[Tuple[Concept, ...]]]:
51
51
  if not isinstance(concept.lineage, FilterItem):
52
52
  raise ValueError(
53
53
  f"Concept {concept} lineage is not filter item, is {type(concept.lineage)}"
@@ -58,7 +58,7 @@ def resolve_filter_parent_concepts(
58
58
  base_rows += concept.lineage.where.row_arguments
59
59
  # TODO: pass tuple groups through
60
60
  for ctuple in concept.lineage.where.existence_arguments:
61
- base_existence += list(ctuple)
61
+ base_existence.append(ctuple)
62
62
  if direct_parent.grain:
63
63
  base_rows += direct_parent.grain.components_copy
64
64
  if (
@@ -71,7 +71,7 @@ def resolve_filter_parent_concepts(
71
71
  return (
72
72
  concept.lineage.content,
73
73
  unique(base_rows, "address"),
74
- unique(base_existence, "address"),
74
+ base_existence,
75
75
  )
76
76
  return concept.lineage.content, unique(base_rows, "address"), []
77
77
 
@@ -74,27 +74,31 @@ def gen_filter_node(
74
74
  return parent
75
75
 
76
76
  core_parents.append(parent)
77
+
77
78
  if parent_existence_concepts:
78
- logger.info(
79
- f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in parent_existence_concepts]}"
80
- )
81
- parent_existence = source_concepts(
82
- mandatory_list=parent_existence_concepts,
83
- environment=environment,
84
- g=g,
85
- depth=depth + 1,
86
- history=history,
87
- )
88
- if not parent_existence:
79
+ for existence_tuple in parent_existence_concepts:
80
+ if not existence_tuple:
81
+ continue
89
82
  logger.info(
90
- f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
83
+ f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in existence_tuple]}"
91
84
  )
92
- return None
93
- core_parents.append(parent_existence)
94
-
85
+ parent_existence = source_concepts(
86
+ mandatory_list=list(existence_tuple),
87
+ environment=environment,
88
+ g=g,
89
+ depth=depth + 1,
90
+ history=history,
91
+ )
92
+ if not parent_existence:
93
+ logger.info(
94
+ f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
95
+ )
96
+ return None
97
+ core_parents.append(parent_existence)
98
+ flattened_existence = [x for y in parent_existence_concepts for x in y]
95
99
  filter_node = FilterNode(
96
100
  input_concepts=unique(
97
- [immediate_parent] + parent_row_concepts + parent_existence_concepts,
101
+ [immediate_parent] + parent_row_concepts + flattened_existence,
98
102
  "address",
99
103
  ),
100
104
  output_concepts=[concept, immediate_parent] + parent_row_concepts,
@@ -19,7 +19,7 @@ from trilogy.core.processing.nodes.base_node import (
19
19
  concept_list_to_grain,
20
20
  )
21
21
  from trilogy.utility import unique
22
-
22
+ from trilogy.core.processing.utility import is_scalar_condition
23
23
 
24
24
  LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
25
25
 
@@ -111,7 +111,8 @@ class GroupNode(StrategyNode):
111
111
  f" {parent.grain}"
112
112
  )
113
113
  source_type = SourceType.GROUP
114
- return QueryDatasource(
114
+
115
+ base = QueryDatasource(
115
116
  input_concepts=self.input_concepts,
116
117
  output_concepts=self.output_concepts,
117
118
  datasources=parent_sources,
@@ -134,6 +135,31 @@ class GroupNode(StrategyNode):
134
135
  partial_concepts=self.partial_concepts,
135
136
  condition=self.conditions,
136
137
  )
138
+ # if there is a condition on a group node and it's not scalar
139
+ # inject an additional CTE
140
+ if self.conditions:
141
+ logger.info("CONDITIONS")
142
+ logger.info(str(self.conditions))
143
+ logger.info(is_scalar_condition(self.conditions))
144
+ if self.conditions and not is_scalar_condition(self.conditions):
145
+ base.condition = None
146
+ base.output_concepts = self.output_concepts + self.conditions.row_arguments
147
+ return QueryDatasource(
148
+ input_concepts=base.output_concepts,
149
+ output_concepts=self.output_concepts,
150
+ datasources=[base],
151
+ source_type=SourceType.SELECT,
152
+ source_map=resolve_concept_map(
153
+ [base],
154
+ targets=self.output_concepts,
155
+ inherited_inputs=base.output_concepts,
156
+ ),
157
+ joins=[],
158
+ grain=grain,
159
+ partial_concepts=self.partial_concepts,
160
+ condition=self.conditions,
161
+ )
162
+ return base
137
163
 
138
164
  def copy(self) -> "GroupNode":
139
165
  return GroupNode(
@@ -8,6 +8,16 @@ from trilogy.core.models import (
8
8
  QueryDatasource,
9
9
  LooseConceptList,
10
10
  Environment,
11
+ Conditional,
12
+ SubselectComparison,
13
+ Comparison,
14
+ Parenthetical,
15
+ Function,
16
+ FilterItem,
17
+ MagicConstants,
18
+ WindowItem,
19
+ AggregateWrapper,
20
+ DataType,
11
21
  )
12
22
 
13
23
  from trilogy.core.enums import Purpose, Granularity
@@ -18,6 +28,8 @@ from collections import defaultdict
18
28
  from logging import Logger
19
29
  from pydantic import BaseModel
20
30
 
31
+ from trilogy.core.enums import FunctionClass
32
+
21
33
 
22
34
  class NodeType(Enum):
23
35
  CONCEPT = 1
@@ -332,3 +344,37 @@ def get_disconnected_components(
332
344
  x for x in sub_graphs if calculate_graph_relevance(graph, x, all_concepts) > 0
333
345
  ]
334
346
  return len(sub_graphs), sub_graphs
347
+
348
+
349
+ def is_scalar_condition(
350
+ element: (
351
+ int
352
+ | str
353
+ | float
354
+ | list
355
+ | WindowItem
356
+ | FilterItem
357
+ | Concept
358
+ | Comparison
359
+ | Conditional
360
+ | Parenthetical
361
+ | Function
362
+ | AggregateWrapper
363
+ | MagicConstants
364
+ | DataType
365
+ ),
366
+ ) -> bool:
367
+ if isinstance(element, Parenthetical):
368
+ return is_scalar_condition(element.content)
369
+ elif isinstance(element, SubselectComparison):
370
+ return True
371
+ elif isinstance(element, Comparison):
372
+ return is_scalar_condition(element.left) and is_scalar_condition(element.right)
373
+ elif isinstance(element, Function):
374
+ if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
375
+ return False
376
+ elif isinstance(element, AggregateWrapper):
377
+ return is_scalar_condition(element.function)
378
+ elif isinstance(element, Conditional):
379
+ return is_scalar_condition(element.left) and is_scalar_condition(element.right)
380
+ return True
@@ -350,7 +350,11 @@ def get_query_datasources(
350
350
 
351
351
  search_concepts: list[Concept] = statement.output_components
352
352
  nest_where = statement.where_clause_category == SelectFiltering.IMPLICIT
353
- if nest_where and statement.where_clause:
353
+
354
+ # if all are aggregates, we've pushed the filtering inside the aggregates anyway
355
+ all_aggregate = all([x.is_aggregate for x in search_concepts])
356
+
357
+ if nest_where and statement.where_clause and not all_aggregate:
354
358
  search_concepts = unique(
355
359
  statement.where_clause.row_arguments + search_concepts, "address"
356
360
  )
@@ -363,7 +367,8 @@ def get_query_datasources(
363
367
  )
364
368
  ds: GroupNode | SelectNode
365
369
  if nest_where and statement.where_clause:
366
- ods.conditions = statement.where_clause.conditional
370
+ if not all_aggregate:
371
+ ods.conditions = statement.where_clause.conditional
367
372
  ods.output_concepts = search_concepts
368
373
  # ods.hidden_concepts = where_delta
369
374
  ods.rebuild_cache()