pytrilogy 0.0.2.9__tar.gz → 0.0.2.11__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.9/pytrilogy.egg-info → pytrilogy-0.0.2.11}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_models.py +18 -0
  4. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_parsing.py +24 -0
  5. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/constants.py +1 -1
  7. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/models.py +43 -10
  8. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/concept_strategies_v3.py +2 -0
  9. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/filter_node.py +27 -5
  10. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/group_node.py +2 -0
  11. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/node_merge_node.py +6 -1
  12. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/group_node.py +1 -5
  13. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/utility.py +18 -5
  14. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/query_processor.py +12 -4
  15. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/base.py +18 -15
  16. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/duckdb.py +1 -1
  17. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/parse_engine.py +60 -15
  18. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/trilogy.lark +5 -1
  19. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/LICENSE.md +0 -0
  20. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/README.md +0 -0
  21. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/pyproject.toml +0 -0
  22. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/pytrilogy.egg-info/SOURCES.txt +0 -0
  23. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/pytrilogy.egg-info/dependency_links.txt +0 -0
  24. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/pytrilogy.egg-info/entry_points.txt +0 -0
  25. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/pytrilogy.egg-info/requires.txt +0 -0
  26. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/pytrilogy.egg-info/top_level.txt +0 -0
  27. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/setup.cfg +0 -0
  28. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/setup.py +0 -0
  29. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_datatypes.py +0 -0
  30. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_declarations.py +0 -0
  31. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_derived_concepts.py +0 -0
  32. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_discovery_nodes.py +0 -0
  33. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_environment.py +0 -0
  34. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_functions.py +0 -0
  35. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_imports.py +0 -0
  36. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_metadata.py +0 -0
  37. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_multi_join_assignments.py +0 -0
  38. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_partial_handling.py +0 -0
  39. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_query_processing.py +0 -0
  40. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_select.py +0 -0
  41. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_statements.py +0 -0
  42. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_undefined_concept.py +0 -0
  43. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/tests/test_where_clause.py +0 -0
  44. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/compiler.py +0 -0
  45. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/__init__.py +0 -0
  46. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/constants.py +0 -0
  47. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/enums.py +0 -0
  48. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/env_processor.py +0 -0
  49. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/environment_helpers.py +0 -0
  50. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/ergonomics.py +0 -0
  51. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/exceptions.py +0 -0
  52. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/functions.py +0 -0
  53. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/graph_models.py +0 -0
  54. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/internal.py +0 -0
  55. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/optimization.py +0 -0
  56. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/optimizations/__init__.py +0 -0
  57. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/optimizations/base_optimization.py +0 -0
  58. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/optimizations/inline_constant.py +0 -0
  59. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/optimizations/inline_datasource.py +0 -0
  60. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  61. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/__init__.py +0 -0
  62. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/graph_utils.py +0 -0
  63. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/__init__.py +0 -0
  64. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  65. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/common.py +0 -0
  66. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  67. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  68. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  69. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/select_node.py +0 -0
  70. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  71. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/node_generators/window_node.py +0 -0
  72. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/__init__.py +0 -0
  73. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/base_node.py +0 -0
  74. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/filter_node.py +0 -0
  75. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/merge_node.py +0 -0
  76. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  77. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  78. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/core/processing/nodes/window_node.py +0 -0
  79. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/__init__.py +0 -0
  80. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/bigquery.py +0 -0
  81. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/common.py +0 -0
  82. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/config.py +0 -0
  83. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/enums.py +0 -0
  84. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/postgres.py +0 -0
  85. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/presto.py +0 -0
  86. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/snowflake.py +0 -0
  87. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/dialect/sql_server.py +0 -0
  88. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/engine.py +0 -0
  89. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/executor.py +0 -0
  90. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/hooks/__init__.py +0 -0
  91. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/hooks/base_hook.py +0 -0
  92. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/hooks/graph_hook.py +0 -0
  93. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/hooks/query_debugger.py +0 -0
  94. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/metadata/__init__.py +0 -0
  95. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parser.py +0 -0
  96. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/__init__.py +0 -0
  97. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/common.py +0 -0
  98. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/config.py +0 -0
  99. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/exceptions.py +0 -0
  100. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/helpers.py +0 -0
  101. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/parsing/render.py +0 -0
  102. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/py.typed +0 -0
  103. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/scripts/__init__.py +0 -0
  104. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/scripts/trilogy.py +0 -0
  105. {pytrilogy-0.0.2.9 → pytrilogy-0.0.2.11}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.9
3
+ Version: 0.0.2.11
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.9
3
+ Version: 0.0.2.11
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -12,6 +12,7 @@ from trilogy.core.models import (
12
12
  Comparison,
13
13
  Join,
14
14
  JoinKey,
15
+ Concept,
15
16
  )
16
17
 
17
18
 
@@ -69,6 +70,23 @@ def test_concept(test_environment, test_environment_graph):
69
70
  )
70
71
 
71
72
 
73
+ def test_concept_filter(test_environment, test_environment_graph):
74
+ test_concept: Concept = list(test_environment.concepts.values())[0]
75
+ new = test_concept.with_filter(
76
+ Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
77
+ )
78
+ new2 = test_concept.with_filter(
79
+ Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
80
+ )
81
+
82
+ assert new.name == new2.name != test_concept.name
83
+
84
+ new3 = new.with_filter(
85
+ Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
86
+ )
87
+ assert new3 == new
88
+
89
+
72
90
  def test_conditional(test_environment, test_environment_graph):
73
91
  test_concept = list(test_environment.concepts.values())[-1]
74
92
 
@@ -492,3 +492,27 @@ select x;
492
492
  results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
493
493
 
494
494
  assert "abcdef as test" in results, results
495
+
496
+
497
+ def test_filter_concise():
498
+
499
+ text = """
500
+ key x int;
501
+ key y int;
502
+
503
+ datasource test (
504
+ x:x,
505
+ y:y)
506
+ grain(x)
507
+ address `abc:def`
508
+ ;
509
+
510
+ auto filtered_test <- x ? y > 10;
511
+
512
+ select filtered_test;
513
+ """
514
+ env, parsed = parse_text(text)
515
+
516
+ results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
517
+
518
+ assert "filtered_test" in results, results
@@ -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.9"
7
+ __version__ = "0.0.2.11"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -24,7 +24,7 @@ class Optimizations:
24
24
  predicate_pushdown: bool = True
25
25
  datasource_inlining: bool = True
26
26
  constant_inlining: bool = True
27
- constant_inline_cutoff: int = 2
27
+ constant_inline_cutoff: int = 3
28
28
  direct_return: bool = True
29
29
 
30
30
 
@@ -70,7 +70,7 @@ from trilogy.utility import unique
70
70
  from collections import UserList, UserDict
71
71
  from functools import cached_property
72
72
  from abc import ABC
73
-
73
+ from collections import defaultdict
74
74
 
75
75
  LOGGER_PREFIX = "[MODELS]"
76
76
 
@@ -801,15 +801,18 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
801
801
  ) -> "Concept":
802
802
  from trilogy.utility import string_to_hash
803
803
 
804
- name = string_to_hash(self.name + str(condition))
804
+ if self.lineage and isinstance(self.lineage, FilterItem):
805
+ if self.lineage.where.conditional == condition:
806
+ return self
807
+ hash = string_to_hash(self.name + str(condition))
805
808
  new = Concept(
806
- name=f"{self.name}_{name}",
809
+ name=f"{self.name}_filter_{hash}",
807
810
  datatype=self.datatype,
808
811
  purpose=self.purpose,
809
812
  metadata=self.metadata,
810
813
  lineage=FilterItem(content=self, where=WhereClause(conditional=condition)),
811
- keys=None,
812
- grain=(self.grain if self.purpose == Purpose.PROPERTY else Grain()),
814
+ keys=(self.keys if self.purpose == Purpose.PROPERTY else None),
815
+ grain=self.grain if self.grain else Grain(components=[]),
813
816
  namespace=self.namespace,
814
817
  modifiers=self.modifiers,
815
818
  pseudonyms=self.pseudonyms,
@@ -842,6 +845,16 @@ class Grain(Mergeable, BaseModel):
842
845
  v2 = sorted(final, key=lambda x: x.name)
843
846
  return v2
844
847
 
848
+ def with_filter(
849
+ self,
850
+ condition: "Conditional | Comparison | Parenthetical",
851
+ environment: Environment | None = None,
852
+ ) -> "Grain":
853
+ return Grain(
854
+ components=[c.with_filter(condition, environment) for c in self.components],
855
+ nested=self.nested,
856
+ )
857
+
845
858
  @property
846
859
  def components_copy(self) -> List[Concept]:
847
860
  return [*self.components]
@@ -1680,6 +1693,9 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1680
1693
  )
1681
1694
  ):
1682
1695
  output.append(item)
1696
+ # TODO: explore implicit filtering more
1697
+ # if self.where_clause.conditional and self.where_clause_category == SelectFiltering.IMPLICIT:
1698
+ # output =[x.with_filter(self.where_clause.conditional) for x in output]
1683
1699
  return Grain(
1684
1700
  components=unique(output, "address"), where_clause=self.where_clause
1685
1701
  )
@@ -2027,7 +2043,7 @@ class Datasource(Namespaced, BaseModel):
2027
2043
  return self.__repr__()
2028
2044
 
2029
2045
  def __hash__(self):
2030
- return (self.namespace + self.identifier).__hash__()
2046
+ return self.full_name.__hash__()
2031
2047
 
2032
2048
  def with_namespace(self, namespace: str):
2033
2049
  new_namespace = (
@@ -2212,9 +2228,9 @@ class BaseJoin(BaseModel):
2212
2228
  class QueryDatasource(BaseModel):
2213
2229
  input_concepts: List[Concept]
2214
2230
  output_concepts: List[Concept]
2231
+ datasources: List[Union[Datasource, "QueryDatasource"]]
2215
2232
  source_map: Dict[str, Set[Union[Datasource, "QueryDatasource", "UnnestJoin"]]]
2216
2233
 
2217
- datasources: List[Union[Datasource, "QueryDatasource"]]
2218
2234
  grain: Grain
2219
2235
  joins: List[BaseJoin | UnnestJoin]
2220
2236
  limit: Optional[int] = None
@@ -2266,7 +2282,7 @@ class QueryDatasource(BaseModel):
2266
2282
 
2267
2283
  @field_validator("source_map")
2268
2284
  @classmethod
2269
- def validate_source_map(cls, v, info: ValidationInfo):
2285
+ def validate_source_map(cls, v: dict, info: ValidationInfo):
2270
2286
  values = info.data
2271
2287
  for key in ("input_concepts", "output_concepts"):
2272
2288
  if not values.get(key):
@@ -2344,11 +2360,23 @@ class QueryDatasource(BaseModel):
2344
2360
  )
2345
2361
 
2346
2362
  merged_datasources = {}
2363
+
2347
2364
  for ds in [*self.datasources, *other.datasources]:
2348
2365
  if ds.full_name in merged_datasources:
2349
2366
  merged_datasources[ds.full_name] = merged_datasources[ds.full_name] + ds
2350
2367
  else:
2351
2368
  merged_datasources[ds.full_name] = ds
2369
+
2370
+ final_source_map = defaultdict(set)
2371
+ for key in self.source_map:
2372
+ final_source_map[key] = self.source_map[key].union(
2373
+ other.source_map.get(key, set())
2374
+ )
2375
+ for key in other.source_map:
2376
+ if key not in final_source_map:
2377
+ final_source_map[key] = other.source_map[key]
2378
+ for k, v in final_source_map.items():
2379
+ final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2352
2380
  qds = QueryDatasource(
2353
2381
  input_concepts=unique(
2354
2382
  self.input_concepts + other.input_concepts, "address"
@@ -2356,7 +2384,7 @@ class QueryDatasource(BaseModel):
2356
2384
  output_concepts=unique(
2357
2385
  self.output_concepts + other.output_concepts, "address"
2358
2386
  ),
2359
- source_map={**self.source_map, **other.source_map},
2387
+ source_map=final_source_map,
2360
2388
  datasources=list(merged_datasources.values()),
2361
2389
  grain=self.grain,
2362
2390
  joins=unique(self.joins + other.joins, "unique_id"),
@@ -4062,8 +4090,13 @@ class RowsetDerivationStatement(Namespaced, BaseModel):
4062
4090
  output: list[Concept] = []
4063
4091
  orig: dict[str, Concept] = {}
4064
4092
  for orig_concept in self.select.output_components:
4093
+ name = orig_concept.name
4094
+ if isinstance(orig_concept.lineage, FilterItem):
4095
+ if orig_concept.lineage.where == self.select.where_clause:
4096
+ name = orig_concept.lineage.content.name
4097
+
4065
4098
  new_concept = Concept(
4066
- name=orig_concept.name,
4099
+ name=name,
4067
4100
  datatype=orig_concept.datatype,
4068
4101
  purpose=orig_concept.purpose,
4069
4102
  lineage=RowsetItem(
@@ -191,6 +191,7 @@ def generate_candidates_restrictive(
191
191
  ):
192
192
  combos.append(local_candidates)
193
193
  combos.append(grain_check)
194
+ # combos.append(local_candidates)
194
195
  # append the empty set for sourcing concept by itself last
195
196
  combos.append([])
196
197
  return combos
@@ -645,6 +646,7 @@ def _search_concepts(
645
646
  depth=depth,
646
647
  source_concepts=search_concepts,
647
648
  history=history,
649
+ search_conditions=conditions,
648
650
  )
649
651
 
650
652
  if expanded:
@@ -17,6 +17,7 @@ from trilogy.core.processing.node_generators.common import (
17
17
  from trilogy.constants import logger
18
18
  from trilogy.core.processing.utility import padding, unique
19
19
  from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
20
+ from trilogy.core.processing.utility import is_scalar_condition
20
21
 
21
22
  LOGGER_PREFIX = "[GEN_FILTER_NODE]"
22
23
 
@@ -38,6 +39,15 @@ def gen_filter_node(
38
39
  raise SyntaxError('Filter node must have a lineage of type "FilterItem"')
39
40
  where = concept.lineage.where
40
41
 
42
+ optional_included: list[Concept] = []
43
+ for x in local_optional:
44
+ if isinstance(x.lineage, FilterItem):
45
+ if concept.lineage.where == where:
46
+ logger.info(
47
+ f"{padding(depth)}{LOGGER_PREFIX} fetching {x.lineage.content.address} as optional parent with same filter conditions "
48
+ )
49
+ parent_row_concepts.append(x.lineage.content)
50
+ optional_included.append(x)
41
51
  logger.info(
42
52
  f"{padding(depth)}{LOGGER_PREFIX} filter {concept.address} 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"
43
53
  )
@@ -48,6 +58,7 @@ def gen_filter_node(
48
58
  g=g,
49
59
  depth=depth + 1,
50
60
  history=history,
61
+ conditions=conditions,
51
62
  )
52
63
 
53
64
  flattened_existence = [x for y in parent_existence_concepts for x in y]
@@ -78,16 +89,25 @@ def gen_filter_node(
78
89
  return None
79
90
 
80
91
  optimized_pushdown = False
81
- if not local_optional:
92
+ if not is_scalar_condition(where.conditional):
93
+ optimized_pushdown = False
94
+ elif not local_optional:
82
95
  optimized_pushdown = True
83
96
  elif conditions and conditions == where:
84
97
  logger.info(
85
98
  f"{padding(depth)}{LOGGER_PREFIX} query conditions are the same as filter conditions, can optimize across all concepts"
86
99
  )
87
100
  optimized_pushdown = True
88
-
101
+ elif optional_included == local_optional:
102
+ logger.info(
103
+ f"{padding(depth)}{LOGGER_PREFIX} all optional concepts are included in the filter, can optimize across all concepts"
104
+ )
105
+ optimized_pushdown = True
89
106
  if optimized_pushdown:
90
107
  if isinstance(row_parent, SelectNode):
108
+ logger.info(
109
+ f"{padding(depth)}{LOGGER_PREFIX} nesting select node in strategy node"
110
+ )
91
111
  parent = StrategyNode(
92
112
  input_concepts=row_parent.output_concepts,
93
113
  output_concepts=[concept] + row_parent.output_concepts,
@@ -111,6 +131,7 @@ def gen_filter_node(
111
131
  x
112
132
  for x in local_optional
113
133
  if x.address in [y.address for y in parent.output_concepts]
134
+ or x.address in [y.address for y in optional_included]
114
135
  ]
115
136
  parent.add_parents(core_parents)
116
137
  parent.add_condition(where.conditional)
@@ -170,6 +191,7 @@ def gen_filter_node(
170
191
  ] + outputs
171
192
  filter_node.rebuild_cache()
172
193
  return filter_node
194
+
173
195
  enrich_node = source_concepts( # this fetches the parent + join keys
174
196
  # to then connect to the rest of the query
175
197
  mandatory_list=[immediate_parent] + parent_row_concepts + local_optional,
@@ -177,10 +199,11 @@ def gen_filter_node(
177
199
  g=g,
178
200
  depth=depth + 1,
179
201
  history=history,
202
+ conditions=conditions,
180
203
  )
181
204
  if not enrich_node:
182
205
  return filter_node
183
- x = MergeNode(
206
+ return MergeNode(
184
207
  input_concepts=[concept, immediate_parent] + local_optional,
185
208
  output_concepts=[
186
209
  concept,
@@ -201,8 +224,7 @@ def gen_filter_node(
201
224
  [immediate_parent] + parent_row_concepts
202
225
  ),
203
226
  join_type=JoinType.LEFT_OUTER,
204
- filter_to_mutual=False,
227
+ filter_to_mutual=True,
205
228
  )
206
229
  ],
207
230
  )
208
- return x
@@ -91,4 +91,6 @@ def gen_group_node(
91
91
  depth=depth,
92
92
  source_concepts=source_concepts,
93
93
  log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
94
+ history=history,
95
+ conditions=conditions,
94
96
  )
@@ -1,6 +1,6 @@
1
1
  from typing import List, Optional
2
2
 
3
- from trilogy.core.models import Concept, Environment, Conditional
3
+ from trilogy.core.models import Concept, Environment, Conditional, WhereClause
4
4
  from trilogy.core.processing.nodes import MergeNode, History, StrategyNode
5
5
  import networkx as nx
6
6
  from trilogy.core.graph_models import concept_to_node
@@ -260,6 +260,7 @@ def subgraphs_to_merge_node(
260
260
  source_concepts,
261
261
  history,
262
262
  conditions,
263
+ search_conditions: WhereClause | None = None,
263
264
  enable_early_exit: bool = True,
264
265
  ):
265
266
  parents: List[StrategyNode] = []
@@ -277,6 +278,7 @@ def subgraphs_to_merge_node(
277
278
  g=g,
278
279
  depth=depth + 1,
279
280
  history=history,
281
+ conditions=search_conditions,
280
282
  )
281
283
  if not parent:
282
284
  logger.info(
@@ -315,6 +317,7 @@ def gen_merge_node(
315
317
  accept_partial: bool = False,
316
318
  history: History | None = None,
317
319
  conditions: Conditional | None = None,
320
+ search_conditions: WhereClause | None = None,
318
321
  ) -> Optional[MergeNode]:
319
322
 
320
323
  for filter_downstream in [True, False]:
@@ -339,6 +342,7 @@ def gen_merge_node(
339
342
  source_concepts=source_concepts,
340
343
  history=history,
341
344
  conditions=conditions,
345
+ search_conditions=search_conditions,
342
346
  )
343
347
  # one concept handling may need to be kicked to alias
344
348
  if len(all_concepts) == 1:
@@ -354,6 +358,7 @@ def gen_merge_node(
354
358
  history=history,
355
359
  conditions=conditions,
356
360
  enable_early_exit=False,
361
+ search_conditions=search_conditions,
357
362
  )
358
363
  if test:
359
364
  return test
@@ -130,7 +130,7 @@ class GroupNode(StrategyNode):
130
130
  if self.conditions
131
131
  else self.output_concepts
132
132
  ),
133
- inherited_inputs=self.input_concepts,
133
+ inherited_inputs=self.input_concepts + self.existence_concepts,
134
134
  ),
135
135
  joins=[],
136
136
  grain=grain,
@@ -139,10 +139,6 @@ class GroupNode(StrategyNode):
139
139
  )
140
140
  # if there is a condition on a group node and it's not scalar
141
141
  # inject an additional CTE
142
- if self.conditions:
143
- logger.info("CONDITIONS")
144
- logger.info(str(self.conditions))
145
- logger.info(is_scalar_condition(self.conditions))
146
142
  if self.conditions and not is_scalar_condition(self.conditions):
147
143
  base.condition = None
148
144
  base.output_concepts = self.output_concepts + self.conditions.row_arguments
@@ -355,27 +355,40 @@ def is_scalar_condition(
355
355
  | MagicConstants
356
356
  | DataType
357
357
  ),
358
+ materialized: set[str] | None = None,
358
359
  ) -> bool:
359
360
  if isinstance(element, Parenthetical):
360
- return is_scalar_condition(element.content)
361
+ return is_scalar_condition(element.content, materialized)
361
362
  elif isinstance(element, SubselectComparison):
362
363
  return True
363
364
  elif isinstance(element, Comparison):
364
- return is_scalar_condition(element.left) and is_scalar_condition(element.right)
365
+ return is_scalar_condition(element.left, materialized) and is_scalar_condition(
366
+ element.right, materialized
367
+ )
365
368
  elif isinstance(element, Function):
366
369
  if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
367
370
  return False
371
+ elif isinstance(element, Concept):
372
+ if materialized and element.address in materialized:
373
+ return True
374
+ if element.lineage and isinstance(element.lineage, AggregateWrapper):
375
+ return is_scalar_condition(element.lineage, materialized)
376
+ return True
368
377
  elif isinstance(element, AggregateWrapper):
369
- return is_scalar_condition(element.function)
378
+ return is_scalar_condition(element.function, materialized)
370
379
  elif isinstance(element, Conditional):
371
- return is_scalar_condition(element.left) and is_scalar_condition(element.right)
380
+ return is_scalar_condition(element.left, materialized) and is_scalar_condition(
381
+ element.right, materialized
382
+ )
372
383
  return True
373
384
 
374
385
 
375
386
  def decompose_condition(
376
- conditional: Conditional,
387
+ conditional: Conditional | Comparison | Parenthetical,
377
388
  ) -> list[SubselectComparison | Comparison | Conditional | Parenthetical]:
378
389
  chunks: list[SubselectComparison | Comparison | Conditional | Parenthetical] = []
390
+ if not isinstance(conditional, Conditional):
391
+ return [conditional]
379
392
  if conditional.operator == BooleanOperator.AND:
380
393
  if not (
381
394
  isinstance(
@@ -145,7 +145,9 @@ def generate_source_map(
145
145
  names = set([x.name for x in ev])
146
146
  ematches = [cte.name for cte in all_new_ctes if cte.source.name in names]
147
147
  existence_source_map[ek] = ematches
148
- return {k: [] if not v else v for k, v in source_map.items()}, existence_source_map
148
+ return {
149
+ k: [] if not v else list(set(v)) for k, v in source_map.items()
150
+ }, existence_source_map
149
151
 
150
152
 
151
153
  def datasource_to_query_datasource(datasource: Datasource) -> QueryDatasource:
@@ -191,6 +193,8 @@ def resolve_cte_base_name_and_alias_v2(
191
193
  raw_joins: List[Join | InstantiatedUnnestJoin],
192
194
  ) -> Tuple[str | None, str | None]:
193
195
  joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
196
+ # INFO trilogy:query_processor.py:263 Finished building source map for civet with 3 parents, have {'local.relevant_customers': ['fowl', 'fowl'],
197
+ # 'customer.demographics.gender': ['mandrill'], 'customer.id': ['mandrill'], 'customer.demographics.id': ['mandrill'], 'customer.id_9268029262289908': [], 'customer.demographics.gender_1513806568509111': []}, query_datasource had non-empty keys ['local.relevant_customers', 'customer.demographics.gender', 'customer.id', 'customer.demographics.id'] and existence had non-empty keys []
194
198
  if (
195
199
  len(source.datasources) == 1
196
200
  and isinstance(source.datasources[0], Datasource)
@@ -212,12 +216,16 @@ def resolve_cte_base_name_and_alias_v2(
212
216
 
213
217
  counts: dict[str, int] = defaultdict(lambda: 0)
214
218
  output_addresses = [x.address for x in source.output_concepts]
219
+ input_address = [x.address for x in source.input_concepts]
215
220
  for k, v in source_map.items():
216
221
  for vx in v:
217
222
  if k in output_addresses:
218
223
  counts[vx] = counts[vx] + 1
219
- else:
220
- counts[vx] = counts[vx]
224
+
225
+ if k in input_address:
226
+ counts[vx] = counts[vx] + 1
227
+
228
+ counts[vx] = counts[vx]
221
229
  if counts:
222
230
  return max(counts, key=counts.get), max(counts, key=counts.get) # type: ignore
223
231
  return None, None
@@ -261,7 +269,7 @@ def datasource_to_ctes(
261
269
 
262
270
  human_id = generate_cte_name(query_datasource.full_name, name_map)
263
271
  logger.info(
264
- f"Finished building source map for {human_id} with {len(parents)} parents, have {source_map}, query_datasource had non-empty keys {[k for k, v in query_datasource.source_map.items() if v]} "
272
+ f"Finished building source map for {human_id} with {len(parents)} parents, have {source_map}, query_datasource had non-empty keys {[k for k, v in query_datasource.source_map.items() if v]} and existence had non-empty keys {[k for k, v in query_datasource.existence_source_map.items() if v]} "
265
273
  )
266
274
  final_joins = [
267
275
  x
@@ -2,7 +2,7 @@ from typing import List, Union, Optional, Dict, Any, Sequence, Callable
2
2
 
3
3
  from jinja2 import Template
4
4
 
5
- from trilogy.core.processing.utility import is_scalar_condition
5
+ from trilogy.core.processing.utility import is_scalar_condition, decompose_condition
6
6
  from trilogy.constants import CONFIG, logger, MagicConstants
7
7
  from trilogy.core.internal import DEFAULT_CONCEPTS
8
8
  from trilogy.core.enums import (
@@ -281,10 +281,7 @@ class BaseDialect:
281
281
  rval = f"{self.WINDOW_FUNCTION_MAP[c.lineage.type](concept = self.render_concept_sql(c.lineage.content, cte=cte, alias=False), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
282
282
  elif isinstance(c.lineage, FilterItem):
283
283
  # for cases when we've optimized this
284
- if (
285
- len(cte.output_columns) == 1
286
- and cte.condition == c.lineage.where.conditional
287
- ):
284
+ if cte.condition == c.lineage.where.conditional:
288
285
  rval = self.render_expr(c.lineage.content, cte=cte)
289
286
  else:
290
287
  rval = f"CASE WHEN {self.render_expr(c.lineage.where.conditional, cte=cte)} THEN {self.render_concept_sql(c.lineage.content, cte=cte, alias=False)} ELSE NULL END"
@@ -538,6 +535,20 @@ class BaseDialect:
538
535
  final_joins = []
539
536
  else:
540
537
  final_joins = cte.joins or []
538
+ where: Conditional | Parenthetical | Comparison | None = None
539
+ having: Conditional | Parenthetical | Comparison | None = None
540
+ materialized = {x for x, v in cte.source_map.items() if v}
541
+ if cte.condition:
542
+ if is_scalar_condition(cte.condition, materialized=materialized):
543
+ where = cte.condition
544
+ else:
545
+ components = decompose_condition(cte.condition)
546
+ for x in components:
547
+ if is_scalar_condition(x, materialized=materialized):
548
+ where = where + x if where else x
549
+ else:
550
+ having = having + x if having else x
551
+
541
552
  return CompiledCTE(
542
553
  name=cte.name,
543
554
  statement=self.SQL_TEMPLATE.render(
@@ -561,16 +572,8 @@ class BaseDialect:
561
572
  ]
562
573
  if j
563
574
  ],
564
- where=(
565
- self.render_expr(cte.condition, cte)
566
- if cte.condition and is_scalar_condition(cte.condition)
567
- else None
568
- ),
569
- having=(
570
- self.render_expr(cte.condition, cte)
571
- if cte.condition and not is_scalar_condition(cte.condition)
572
- else None
573
- ),
575
+ where=(self.render_expr(where, cte) if where else None),
576
+ having=(self.render_expr(having, cte) if having else None),
574
577
  order_by=(
575
578
  [self.render_order_item(i, cte) for i in cte.order_by.items]
576
579
  if cte.order_by
@@ -36,7 +36,7 @@ FUNCTION_MAP = {
36
36
  # we may return a static value
37
37
  FUNCTION_GRAIN_MATCH_MAP = {
38
38
  **FUNCTION_MAP,
39
- FunctionType.COUNT: lambda args: "1",
39
+ FunctionType.COUNT: lambda args: f"{args[0]}",
40
40
  FunctionType.SUM: lambda args: f"{args[0]}",
41
41
  FunctionType.AVG: lambda args: f"{args[0]}",
42
42
  }
@@ -983,6 +983,7 @@ class ParseToObjects(Transformer):
983
983
  order_by=order_by,
984
984
  meta=Metadata(line_number=meta.line),
985
985
  )
986
+
986
987
  for item in select_items:
987
988
  # we don't know the grain of an aggregate at assignment time
988
989
  # so rebuild at this point in the tree
@@ -1000,21 +1001,43 @@ class ParseToObjects(Transformer):
1000
1001
  )
1001
1002
  self.environment.add_concept(new_concept, meta=meta)
1002
1003
  item.content.output = new_concept
1004
+ # TODO: revisit if we can push down every filter
1005
+ # else:
1006
+ # item.content = (
1007
+ # item.content.with_filter(
1008
+ # output.where_clause.conditional, environment=self.environment
1009
+ # )
1010
+ # if output.where_clause
1011
+ # and output.where_clause_category == SelectFiltering.IMPLICIT
1012
+ # else item.content
1013
+ # )
1014
+
1003
1015
  if order_by:
1004
1016
  for orderitem in order_by.items:
1005
- if (
1006
- isinstance(orderitem.expr, Concept)
1007
- and orderitem.expr.purpose == Purpose.METRIC
1008
- ):
1009
- orderitem.expr = orderitem.expr.with_select_context(
1010
- output.grain,
1011
- conditional=(
1012
- output.where_clause.conditional
1013
- if output.where_clause
1014
- and output.where_clause_category == SelectFiltering.IMPLICIT
1015
- else None
1016
- ),
1017
- )
1017
+ if isinstance(orderitem.expr, Concept):
1018
+ if orderitem.expr.purpose == Purpose.METRIC:
1019
+ orderitem.expr = orderitem.expr.with_select_context(
1020
+ output.grain,
1021
+ conditional=(
1022
+ output.where_clause.conditional
1023
+ if output.where_clause
1024
+ and output.where_clause_category
1025
+ == SelectFiltering.IMPLICIT
1026
+ else None
1027
+ ),
1028
+ environment=self.environment,
1029
+ )
1030
+ # TODO :push down every filter
1031
+ # else:
1032
+ # orderitem.expr = (
1033
+ # orderitem.expr.with_filter(
1034
+ # output.where_clause.conditional,
1035
+ # environment=self.environment,
1036
+ # )
1037
+ # if output.where_clause
1038
+ # and output.where_clause_category == SelectFiltering.IMPLICIT
1039
+ # else orderitem.expr
1040
+ # )
1018
1041
  return output
1019
1042
 
1020
1043
  @v_args(meta=True)
@@ -1116,7 +1139,25 @@ class ParseToObjects(Transformer):
1116
1139
  def comparison(self, args) -> Comparison:
1117
1140
  if args[1] == ComparisonOperator.IN:
1118
1141
  raise SyntaxError
1119
- return Comparison(left=args[0], right=args[2], operator=args[1])
1142
+ if isinstance(args[0], AggregateWrapper):
1143
+ left = arbitrary_to_concept(
1144
+ args[0],
1145
+ namespace=self.environment.namespace,
1146
+ name=f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(args[0]))}",
1147
+ )
1148
+ self.environment.add_concept(left)
1149
+ else:
1150
+ left = args[0]
1151
+ if isinstance(args[2], AggregateWrapper):
1152
+ right = arbitrary_to_concept(
1153
+ args[2],
1154
+ namespace=self.environment.namespace,
1155
+ name=f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(args[2]))}",
1156
+ )
1157
+ self.environment.add_concept(right)
1158
+ else:
1159
+ right = args[2]
1160
+ return Comparison(left=left, right=right, operator=args[1])
1120
1161
 
1121
1162
  def between_comparison(self, args) -> Conditional:
1122
1163
  left_bound = args[1]
@@ -1219,7 +1260,11 @@ class ParseToObjects(Transformer):
1219
1260
 
1220
1261
  def filter_item(self, args) -> FilterItem:
1221
1262
  where: WhereClause
1222
- string_concept, where = args
1263
+ string_concept, raw = args
1264
+ if isinstance(raw, WhereClause):
1265
+ where = raw
1266
+ else:
1267
+ where = WhereClause(conditional=raw)
1223
1268
  concept = self.environment.concepts[string_concept]
1224
1269
  return FilterItem(content=concept, where=where)
1225
1270
 
@@ -83,7 +83,11 @@
83
83
  raw_function: "bind" "sql" IDENTIFIER "(" function_binding_list ")" "->" data_type "as"i MULTILINE_STRING
84
84
 
85
85
  // user_id where state = Mexico
86
- filter_item: "filter"i IDENTIFIER where
86
+ _filter_alt: IDENTIFIER "?" conditional
87
+ _filter_base: "filter"i IDENTIFIER where
88
+ filter_item: _filter_base | _filter_alt
89
+
90
+
87
91
 
88
92
  // rank/lag/lead
89
93
  WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i) /[\s]+/
File without changes
File without changes
File without changes
File without changes
File without changes