pytrilogy 0.0.2.7__tar.gz → 0.0.2.9__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.7/pytrilogy.egg-info → pytrilogy-0.0.2.9}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/__init__.py +1 -1
  4. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/constants.py +1 -0
  5. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/enums.py +1 -0
  6. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/models.py +154 -56
  7. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/optimization.py +44 -5
  8. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/optimizations/inline_datasource.py +14 -8
  9. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/optimizations/predicate_pushdown.py +73 -44
  10. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/concept_strategies_v3.py +69 -28
  11. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/common.py +42 -16
  12. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/filter_node.py +89 -48
  13. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/group_node.py +3 -1
  14. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/rowset_node.py +13 -54
  15. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/select_node.py +10 -13
  16. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/unnest_node.py +5 -3
  17. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/window_node.py +23 -2
  18. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/__init__.py +34 -6
  19. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/base_node.py +67 -13
  20. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/filter_node.py +3 -0
  21. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/group_node.py +3 -0
  22. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/merge_node.py +1 -11
  23. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/select_node_v2.py +1 -0
  24. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/utility.py +29 -10
  25. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/query_processor.py +47 -20
  26. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/base.py +47 -14
  27. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/common.py +15 -3
  28. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/presto.py +2 -1
  29. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/parse_engine.py +20 -1
  30. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/trilogy.lark +3 -1
  31. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/LICENSE.md +0 -0
  32. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/README.md +0 -0
  33. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/pyproject.toml +0 -0
  34. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/pytrilogy.egg-info/SOURCES.txt +0 -0
  35. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/pytrilogy.egg-info/dependency_links.txt +0 -0
  36. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/pytrilogy.egg-info/entry_points.txt +0 -0
  37. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/pytrilogy.egg-info/requires.txt +0 -0
  38. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/pytrilogy.egg-info/top_level.txt +0 -0
  39. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/setup.cfg +0 -0
  40. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/setup.py +0 -0
  41. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_datatypes.py +0 -0
  42. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_declarations.py +0 -0
  43. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_derived_concepts.py +0 -0
  44. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_discovery_nodes.py +0 -0
  45. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_environment.py +0 -0
  46. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_functions.py +0 -0
  47. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_imports.py +0 -0
  48. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_metadata.py +0 -0
  49. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_models.py +0 -0
  50. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_multi_join_assignments.py +0 -0
  51. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_parsing.py +0 -0
  52. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_partial_handling.py +0 -0
  53. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_query_processing.py +0 -0
  54. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_select.py +0 -0
  55. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_statements.py +0 -0
  56. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_undefined_concept.py +0 -0
  57. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/tests/test_where_clause.py +0 -0
  58. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/compiler.py +0 -0
  59. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/__init__.py +0 -0
  60. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/constants.py +0 -0
  61. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/env_processor.py +0 -0
  62. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/environment_helpers.py +0 -0
  63. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/ergonomics.py +0 -0
  64. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/exceptions.py +0 -0
  65. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/functions.py +0 -0
  66. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/graph_models.py +0 -0
  67. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/internal.py +0 -0
  68. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/optimizations/__init__.py +0 -0
  69. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/optimizations/base_optimization.py +0 -0
  70. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/optimizations/inline_constant.py +0 -0
  71. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/__init__.py +0 -0
  72. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/graph_utils.py +0 -0
  73. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/__init__.py +0 -0
  74. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  75. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  76. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  77. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  78. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  79. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/core/processing/nodes/window_node.py +0 -0
  80. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/__init__.py +0 -0
  81. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/bigquery.py +0 -0
  82. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/config.py +0 -0
  83. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/duckdb.py +0 -0
  84. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/enums.py +0 -0
  85. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/postgres.py +0 -0
  86. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/snowflake.py +0 -0
  87. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/dialect/sql_server.py +0 -0
  88. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/engine.py +0 -0
  89. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/executor.py +0 -0
  90. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/hooks/__init__.py +0 -0
  91. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/hooks/base_hook.py +0 -0
  92. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/hooks/graph_hook.py +0 -0
  93. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/hooks/query_debugger.py +0 -0
  94. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/metadata/__init__.py +0 -0
  95. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parser.py +0 -0
  96. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/__init__.py +0 -0
  97. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/common.py +0 -0
  98. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/config.py +0 -0
  99. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/exceptions.py +0 -0
  100. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/helpers.py +0 -0
  101. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/parsing/render.py +0 -0
  102. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/py.typed +0 -0
  103. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/scripts/__init__.py +0 -0
  104. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/scripts/trilogy.py +0 -0
  105. {pytrilogy-0.0.2.7 → pytrilogy-0.0.2.9}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.7
3
+ Version: 0.0.2.9
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.7
3
+ Version: 0.0.2.9
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -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.7"
7
+ __version__ = "0.0.2.9"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -24,6 +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
28
  direct_return: bool = True
28
29
 
29
30
 
@@ -123,6 +123,7 @@ class FunctionType(Enum):
123
123
  MAP_ACCESS = "map_access"
124
124
  ATTR_ACCESS = "attr_access"
125
125
  STRUCT = "struct"
126
+ ARRAY = "array"
126
127
 
127
128
  # TEXT AND MAYBE MORE
128
129
  SPLIT = "split"
@@ -154,7 +154,10 @@ class ConceptArgs(ABC):
154
154
  class SelectContext(ABC):
155
155
 
156
156
  def with_select_context(
157
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
157
+ self,
158
+ grain: Grain,
159
+ conditional: Conditional | Comparison | Parenthetical | None,
160
+ environment: Environment | None = None,
158
161
  ):
159
162
  raise NotImplementedError
160
163
 
@@ -166,6 +169,7 @@ class ConstantInlineable(ABC):
166
169
 
167
170
  class SelectTypeMixin(BaseModel):
168
171
  where_clause: Union["WhereClause", None] = Field(default=None)
172
+ having_clause: Union["HavingClause", None] = Field(default=None)
169
173
 
170
174
  @property
171
175
  def output_components(self) -> List[Concept]:
@@ -595,13 +599,16 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
595
599
  self,
596
600
  grain: Optional["Grain"] = None,
597
601
  conditional: Conditional | Comparison | Parenthetical | None = None,
602
+ environment: Environment | None = None,
598
603
  ) -> "Concept":
599
604
  if not all([isinstance(x, Concept) for x in self.keys or []]):
600
605
  raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
601
606
  new_grain = grain or self.grain
602
607
  new_lineage = self.lineage
603
608
  if isinstance(self.lineage, SelectContext):
604
- new_lineage = self.lineage.with_select_context(new_grain, conditional)
609
+ new_lineage = self.lineage.with_select_context(
610
+ new_grain, conditional, environment=environment
611
+ )
605
612
  return self.__class__(
606
613
  name=self.name,
607
614
  datatype=self.datatype,
@@ -788,7 +795,9 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
788
795
  return Granularity.MULTI_ROW
789
796
 
790
797
  def with_filter(
791
- self, condition: "Conditional | Comparison | Parenthetical"
798
+ self,
799
+ condition: "Conditional | Comparison | Parenthetical",
800
+ environment: Environment | None = None,
792
801
  ) -> "Concept":
793
802
  from trilogy.utility import string_to_hash
794
803
 
@@ -805,12 +814,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
805
814
  modifiers=self.modifiers,
806
815
  pseudonyms=self.pseudonyms,
807
816
  )
817
+ if environment:
818
+ environment.add_concept(new)
808
819
  return new
809
820
 
810
821
 
811
822
  class Grain(Mergeable, BaseModel):
812
823
  nested: bool = False
813
824
  components: List[Concept] = Field(default_factory=list, validate_default=True)
825
+ where_clause: Optional[WhereClause] = Field(default=None)
814
826
 
815
827
  @field_validator("components")
816
828
  def component_validator(cls, v, info: ValidationInfo):
@@ -836,10 +848,12 @@ class Grain(Mergeable, BaseModel):
836
848
 
837
849
  def __str__(self):
838
850
  if self.abstract:
839
- return (
840
- "Grain<Abstract" + ",".join([c.address for c in self.components]) + ">"
841
- )
842
- return "Grain<" + ",".join([c.address for c in self.components]) + ">"
851
+ base = "Grain<Abstract>"
852
+ else:
853
+ base = "Grain<" + ",".join([c.address for c in self.components]) + ">"
854
+ if self.where_clause:
855
+ base += f"|{str(self.where_clause)}"
856
+ return base
843
857
 
844
858
  def with_namespace(self, namespace: str) -> "Grain":
845
859
  return Grain(
@@ -1046,12 +1060,15 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1046
1060
  return self.output_datatype
1047
1061
 
1048
1062
  def with_select_context(
1049
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1063
+ self,
1064
+ grain: Grain,
1065
+ conditional: Conditional | Comparison | Parenthetical | None,
1066
+ environment: Environment | None = None,
1050
1067
  ) -> Function:
1051
1068
  if self.operator in FunctionClass.AGGREGATE_FUNCTIONS.value and conditional:
1052
1069
  base = [
1053
1070
  (
1054
- c.with_select_context(grain, conditional)
1071
+ c.with_select_context(grain, conditional, environment)
1055
1072
  if isinstance(
1056
1073
  c,
1057
1074
  SelectContext,
@@ -1061,7 +1078,7 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1061
1078
  for c in self.arguments
1062
1079
  ]
1063
1080
  final = [
1064
- c.with_filter(conditional) if isinstance(c, Concept) else c
1081
+ c.with_filter(conditional, environment) if isinstance(c, Concept) else c
1065
1082
  for c in base
1066
1083
  ]
1067
1084
  return Function(
@@ -1077,7 +1094,7 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1077
1094
  operator=self.operator,
1078
1095
  arguments=[
1079
1096
  (
1080
- c.with_select_context(grain, conditional)
1097
+ c.with_select_context(grain, conditional, environment)
1081
1098
  if isinstance(
1082
1099
  c,
1083
1100
  SelectContext,
@@ -1293,13 +1310,22 @@ class WindowItem(Mergeable, Namespaced, SelectContext, BaseModel):
1293
1310
  )
1294
1311
 
1295
1312
  def with_select_context(
1296
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1313
+ self,
1314
+ grain: Grain,
1315
+ conditional: Conditional | Comparison | Parenthetical | None,
1316
+ environment: Environment | None = None,
1297
1317
  ) -> "WindowItem":
1298
1318
  return WindowItem(
1299
1319
  type=self.type,
1300
- content=self.content.with_select_context(grain, conditional),
1301
- over=[x.with_select_context(grain, conditional) for x in self.over],
1302
- order_by=[x.with_select_context(grain, conditional) for x in self.order_by],
1320
+ content=self.content.with_select_context(grain, conditional, environment),
1321
+ over=[
1322
+ x.with_select_context(grain, conditional, environment)
1323
+ for x in self.over
1324
+ ],
1325
+ order_by=[
1326
+ x.with_select_context(grain, conditional, environment)
1327
+ for x in self.order_by
1328
+ ],
1303
1329
  )
1304
1330
 
1305
1331
  @property
@@ -1368,11 +1394,14 @@ class FilterItem(Namespaced, SelectContext, BaseModel):
1368
1394
  )
1369
1395
 
1370
1396
  def with_select_context(
1371
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1397
+ self,
1398
+ grain: Grain,
1399
+ conditional: Conditional | Comparison | Parenthetical | None,
1400
+ environment: Environment | None = None,
1372
1401
  ) -> FilterItem:
1373
1402
  return FilterItem(
1374
- content=self.content.with_select_context(grain, conditional),
1375
- where=self.where.with_select_context(grain, conditional),
1403
+ content=self.content.with_select_context(grain, conditional, environment),
1404
+ where=self.where.with_select_context(grain, conditional, environment),
1376
1405
  )
1377
1406
 
1378
1407
  @property
@@ -1452,9 +1481,17 @@ class OrderItem(Mergeable, SelectContext, Namespaced, BaseModel):
1452
1481
  return OrderItem(expr=self.expr.with_namespace(namespace), order=self.order)
1453
1482
 
1454
1483
  def with_select_context(
1455
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1484
+ self,
1485
+ grain: Grain,
1486
+ conditional: Conditional | Comparison | Parenthetical | None,
1487
+ environment: Environment | None = None,
1456
1488
  ) -> "OrderItem":
1457
- return OrderItem(expr=self.expr.with_grain(grain), order=self.order)
1489
+ return OrderItem(
1490
+ expr=self.expr.with_select_context(
1491
+ grain, conditional=conditional, environment=environment
1492
+ ),
1493
+ order=self.order,
1494
+ )
1458
1495
 
1459
1496
  def with_merge(
1460
1497
  self, source: Concept, target: Concept, modifiers: List[Modifier]
@@ -1643,7 +1680,9 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1643
1680
  )
1644
1681
  ):
1645
1682
  output.append(item)
1646
- return Grain(components=unique(output, "address"))
1683
+ return Grain(
1684
+ components=unique(output, "address"), where_clause=self.where_clause
1685
+ )
1647
1686
 
1648
1687
  def with_namespace(self, namespace: str) -> "SelectStatement":
1649
1688
  return SelectStatement(
@@ -2066,6 +2105,7 @@ class Datasource(Namespaced, BaseModel):
2066
2105
  class UnnestJoin(BaseModel):
2067
2106
  concept: Concept
2068
2107
  alias: str = "unnest"
2108
+ rendering_required: bool = True
2069
2109
 
2070
2110
  def __hash__(self):
2071
2111
  return (self.alias + self.concept.address).__hash__()
@@ -2228,17 +2268,14 @@ class QueryDatasource(BaseModel):
2228
2268
  @classmethod
2229
2269
  def validate_source_map(cls, v, info: ValidationInfo):
2230
2270
  values = info.data
2231
- expected = {c.address for c in values["output_concepts"]}.union(
2232
- c.address for c in values["input_concepts"]
2233
- )
2234
- seen = set()
2235
- for k, _ in v.items():
2236
- seen.add(k)
2237
- for x in expected:
2238
- if x not in seen and CONFIG.validate_missing:
2239
- raise SyntaxError(
2240
- f"source map missing {x} on (expected {expected}, have {seen})"
2241
- )
2271
+ for key in ("input_concepts", "output_concepts"):
2272
+ if not values.get(key):
2273
+ continue
2274
+ for concept in values[key]:
2275
+ if concept.address not in v and CONFIG.validate_missing:
2276
+ raise SyntaxError(
2277
+ f"Missing source map for {concept.address} on {key}, have {v}"
2278
+ )
2242
2279
  return v
2243
2280
 
2244
2281
  def __str__(self):
@@ -2460,10 +2497,17 @@ class CTE(BaseModel):
2460
2497
  for join in self.joins
2461
2498
  if not isinstance(join, Join)
2462
2499
  or (
2463
- join.right_cte.name != removed_cte
2464
- and join.left_cte.name != removed_cte
2500
+ isinstance(join, Join)
2501
+ and (
2502
+ join.right_cte.name != removed_cte
2503
+ and join.left_cte.name != removed_cte
2504
+ )
2465
2505
  )
2466
2506
  ]
2507
+ for join in self.joins:
2508
+ if isinstance(join, UnnestJoin) and join.concept == concept:
2509
+ join.rendering_required = False
2510
+
2467
2511
  self.parent_ctes = [
2468
2512
  x for x in self.parent_ctes if x.name != removed_cte
2469
2513
  ]
@@ -2841,6 +2885,7 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
2841
2885
  self,
2842
2886
  grain: Optional["Grain"] = None,
2843
2887
  conditional: Conditional | Comparison | Parenthetical | None = None,
2888
+ environment: Environment | None = None,
2844
2889
  ) -> "UndefinedConcept":
2845
2890
  if not all([isinstance(x, Concept) for x in self.keys or []]):
2846
2891
  raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
@@ -2848,7 +2893,9 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
2848
2893
  if self.lineage:
2849
2894
  new_lineage = self.lineage
2850
2895
  if isinstance(self.lineage, SelectContext):
2851
- new_lineage = self.lineage.with_select_context(new_grain, conditional)
2896
+ new_lineage = self.lineage.with_select_context(
2897
+ new_grain, conditional, environment
2898
+ )
2852
2899
  else:
2853
2900
  new_lineage = None
2854
2901
  return self.__class__(
@@ -3291,7 +3338,9 @@ class LazyEnvironment(Environment):
3291
3338
  ) or name.startswith("_"):
3292
3339
  return super().__getattribute__(name)
3293
3340
  if not self.loaded:
3294
- print(f"lazily evaluating load path {self.load_path} to access {name}")
3341
+ logger.info(
3342
+ f"lazily evaluating load path {self.load_path} to access {name}"
3343
+ )
3295
3344
  from trilogy import parse
3296
3345
 
3297
3346
  env = Environment(working_path=str(self.working_path))
@@ -3433,16 +3482,23 @@ class Comparison(
3433
3482
  )
3434
3483
 
3435
3484
  def with_select_context(
3436
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3485
+ self,
3486
+ grain: Grain,
3487
+ conditional: Conditional | Comparison | Parenthetical | None,
3488
+ environment: Environment | None = None,
3437
3489
  ):
3438
3490
  return self.__class__(
3439
3491
  left=(
3440
- self.left.with_select_context(grain, conditional)
3492
+ self.left.with_select_context(grain, conditional, environment)
3441
3493
  if isinstance(self.left, SelectContext)
3442
3494
  else self.left
3443
3495
  ),
3444
3496
  # the right side does NOT need to inherit select grain
3445
- right=self.right,
3497
+ right=(
3498
+ self.right.with_select_context(grain, conditional, environment)
3499
+ if isinstance(self.right, SelectContext)
3500
+ else self.right
3501
+ ),
3446
3502
  operator=self.operator,
3447
3503
  )
3448
3504
 
@@ -3526,12 +3582,15 @@ class SubselectComparison(Comparison):
3526
3582
  return [tuple(get_concept_arguments(self.right))]
3527
3583
 
3528
3584
  def with_select_context(
3529
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3585
+ self,
3586
+ grain: Grain,
3587
+ conditional: Conditional | Comparison | Parenthetical | None,
3588
+ environment: Environment | None = None,
3530
3589
  ):
3531
- # there's no need to pass the select grain through to a subselect comparison
3590
+ # there's no need to pass the select grain through to a subselect comparison on the right
3532
3591
  return self.__class__(
3533
3592
  left=(
3534
- self.left.with_select_context(grain, conditional)
3593
+ self.left.with_select_context(grain, conditional, environment)
3535
3594
  if isinstance(self.left, SelectContext)
3536
3595
  else self.left
3537
3596
  ),
@@ -3562,12 +3621,17 @@ class CaseWhen(Namespaced, SelectContext, BaseModel):
3562
3621
  )
3563
3622
 
3564
3623
  def with_select_context(
3565
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3624
+ self,
3625
+ grain: Grain,
3626
+ conditional: Conditional | Comparison | Parenthetical | None,
3627
+ environment: Environment | None = None,
3566
3628
  ) -> CaseWhen:
3567
3629
  return CaseWhen(
3568
- comparison=self.comparison.with_select_context(grain, conditional),
3630
+ comparison=self.comparison.with_select_context(
3631
+ grain, conditional, environment
3632
+ ),
3569
3633
  expr=(
3570
- (self.expr.with_select_context(grain, conditional))
3634
+ (self.expr.with_select_context(grain, conditional, environment))
3571
3635
  if isinstance(self.expr, SelectContext)
3572
3636
  else self.expr
3573
3637
  ),
@@ -3584,12 +3648,15 @@ class CaseElse(Namespaced, SelectContext, BaseModel):
3584
3648
  return get_concept_arguments(self.expr)
3585
3649
 
3586
3650
  def with_select_context(
3587
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3651
+ self,
3652
+ grain: Grain,
3653
+ conditional: Conditional | Comparison | Parenthetical | None,
3654
+ environment: Environment | None = None,
3588
3655
  ) -> CaseElse:
3589
3656
  return CaseElse(
3590
3657
  discriminant=self.discriminant,
3591
3658
  expr=(
3592
- self.expr.with_select_context(grain, conditional)
3659
+ self.expr.with_select_context(grain, conditional, environment)
3593
3660
  if isinstance(
3594
3661
  self.expr,
3595
3662
  SelectContext,
@@ -3729,16 +3796,19 @@ class Conditional(
3729
3796
  )
3730
3797
 
3731
3798
  def with_select_context(
3732
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3799
+ self,
3800
+ grain: Grain,
3801
+ conditional: Conditional | Comparison | Parenthetical | None,
3802
+ environment: Environment | None = None,
3733
3803
  ):
3734
3804
  return Conditional(
3735
3805
  left=(
3736
- self.left.with_select_context(grain, conditional)
3806
+ self.left.with_select_context(grain, conditional, environment)
3737
3807
  if isinstance(self.left, SelectContext)
3738
3808
  else self.left
3739
3809
  ),
3740
3810
  right=(
3741
- self.right.with_select_context(grain, conditional)
3811
+ self.right.with_select_context(grain, conditional, environment)
3742
3812
  if isinstance(self.right, SelectContext)
3743
3813
  else self.right
3744
3814
  ),
@@ -3847,13 +3917,16 @@ class AggregateWrapper(Mergeable, Namespaced, SelectContext, BaseModel):
3847
3917
  )
3848
3918
 
3849
3919
  def with_select_context(
3850
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3920
+ self,
3921
+ grain: Grain,
3922
+ conditional: Conditional | Comparison | Parenthetical | None,
3923
+ environment: Environment | None = None,
3851
3924
  ) -> AggregateWrapper:
3852
3925
  if not self.by:
3853
3926
  by = grain.components_copy
3854
3927
  else:
3855
3928
  by = self.by
3856
- parent = self.function.with_select_context(grain, conditional)
3929
+ parent = self.function.with_select_context(grain, conditional, environment)
3857
3930
  return AggregateWrapper(function=parent, by=by)
3858
3931
 
3859
3932
 
@@ -3885,10 +3958,15 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
3885
3958
  return WhereClause(conditional=self.conditional.with_namespace(namespace))
3886
3959
 
3887
3960
  def with_select_context(
3888
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3961
+ self,
3962
+ grain: Grain,
3963
+ conditional: Conditional | Comparison | Parenthetical | None,
3964
+ environment: Environment | None = None,
3889
3965
  ) -> WhereClause:
3890
3966
  return WhereClause(
3891
- conditional=self.conditional.with_select_context(grain, conditional)
3967
+ conditional=self.conditional.with_select_context(
3968
+ grain, conditional, environment
3969
+ )
3892
3970
  )
3893
3971
 
3894
3972
  @property
@@ -3901,6 +3979,22 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
3901
3979
  output += item.grain.components if item.grain else []
3902
3980
  return Grain(components=list(set(output)))
3903
3981
 
3982
+ @property
3983
+ def components(self):
3984
+ from trilogy.core.processing.utility import decompose_condition
3985
+
3986
+ return decompose_condition(self.conditional)
3987
+
3988
+ @property
3989
+ def is_scalar(self):
3990
+ from trilogy.core.processing.utility import is_scalar_condition
3991
+
3992
+ return is_scalar_condition(self.conditional)
3993
+
3994
+
3995
+ class HavingClause(WhereClause):
3996
+ pass
3997
+
3904
3998
 
3905
3999
  class MaterializedDataset(BaseModel):
3906
4000
  address: Address
@@ -3920,6 +4014,7 @@ class ProcessedQuery(BaseModel):
3920
4014
  hidden_columns: List[Concept] = Field(default_factory=list)
3921
4015
  limit: Optional[int] = None
3922
4016
  where_clause: Optional[WhereClause] = None
4017
+ having_clause: Optional[HavingClause] = None
3923
4018
  order_by: Optional[OrderBy] = None
3924
4019
 
3925
4020
 
@@ -4122,11 +4217,14 @@ class Parenthetical(
4122
4217
  )
4123
4218
 
4124
4219
  def with_select_context(
4125
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
4220
+ self,
4221
+ grain: Grain,
4222
+ conditional: Conditional | Comparison | Parenthetical | None,
4223
+ environment: Environment | None = None,
4126
4224
  ):
4127
4225
  return Parenthetical(
4128
4226
  content=(
4129
- self.content.with_select_context(grain, conditional)
4227
+ self.content.with_select_context(grain, conditional, environment)
4130
4228
  if isinstance(self.content, SelectContext)
4131
4229
  else self.content
4132
4230
  )
@@ -17,19 +17,58 @@ from trilogy.core.optimizations import (
17
17
  MAX_OPTIMIZATION_LOOPS = 100
18
18
 
19
19
 
20
+ # other optimizations may make a CTE a pure passthrough
21
+ # remove those
22
+ # def is_locally_irrelevant(cte: CTE) -> CTE | bool:
23
+ # if not len(cte.parent_ctes) == 1:
24
+ # return False
25
+ # parent = cte.parent_ctes[0]
26
+ # if not parent.output_columns == cte.output_columns:
27
+ # return False
28
+ # if cte.condition is not None:
29
+ # return False
30
+ # if cte.group_to_grain:
31
+ # return False
32
+ # if len(cte.joins)>1:
33
+ # return False
34
+ # return parent
35
+
36
+
20
37
  def filter_irrelevant_ctes(
21
38
  input: list[CTE],
22
39
  root_cte: CTE,
23
40
  ):
24
41
  relevant_ctes = set()
25
42
 
26
- def recurse(cte: CTE):
43
+ def recurse(cte: CTE, inverse_map: dict[str, list[CTE]]):
44
+ # TODO: revisit this
45
+ # if parent := is_locally_irrelevant(cte):
46
+ # logger.info(
47
+ # f"[Optimization][Irrelevent CTE filtering] Removing redundant CTE {cte.name} and replacing with {parent.name}"
48
+ # )
49
+ # for child in inverse_map.get(cte.name, []):
50
+ # child.parent_ctes = [
51
+ # x for x in child.parent_ctes if x.name != cte.name
52
+ # ] + [parent]
53
+ # for x in child.source_map:
54
+ # if cte.name in child.source_map[x]:
55
+ # child.source_map[x].remove(cte.name)
56
+ # child.source_map[x].append(parent.name)
57
+ # for x2 in child.existence_source_map:
58
+ # if cte.name in child.existence_source_map[x2]:
59
+ # child.existence_source_map[x2].remove(cte.name)
60
+ # child.existence_source_map[x2].append(parent.name)
61
+ # else:
27
62
  relevant_ctes.add(cte.name)
28
63
  for cte in cte.parent_ctes:
29
- recurse(cte)
30
-
31
- recurse(root_cte)
32
- return [cte for cte in input if cte.name in relevant_ctes]
64
+ recurse(cte, inverse_map)
65
+
66
+ inverse_map = gen_inverse_map(input)
67
+ recurse(root_cte, inverse_map)
68
+ final = [cte for cte in input if cte.name in relevant_ctes]
69
+ if len(final) == len(input):
70
+ return input
71
+ return filter_irrelevant_ctes(final, root_cte)
33
72
 
34
73
 
35
74
  def gen_inverse_map(input: list[CTE]) -> dict[str, list[CTE]]:
@@ -5,6 +5,7 @@ from trilogy.core.models import (
5
5
 
6
6
  from trilogy.core.optimizations.base_optimization import OptimizationRule
7
7
  from collections import defaultdict
8
+ from trilogy.constants import CONFIG
8
9
 
9
10
 
10
11
  class InlineDatasource(OptimizationRule):
@@ -18,28 +19,28 @@ class InlineDatasource(OptimizationRule):
18
19
  if not cte.parent_ctes:
19
20
  return False
20
21
 
21
- self.log(
22
+ self.debug(
22
23
  f"Checking {cte.name} for consolidating inline tables with {len(cte.parent_ctes)} parents"
23
24
  )
24
25
  to_inline: list[CTE] = []
25
26
  force_group = False
26
27
  for parent_cte in cte.parent_ctes:
27
28
  if not parent_cte.is_root_datasource:
28
- self.log(f"parent {parent_cte.name} is not root")
29
+ self.debug(f"parent {parent_cte.name} is not root")
29
30
  continue
30
31
  if parent_cte.parent_ctes:
31
- self.log(f"parent {parent_cte.name} has parents")
32
+ self.debug(f"parent {parent_cte.name} has parents")
32
33
  continue
33
34
  if parent_cte.condition:
34
- self.log(f"parent {parent_cte.name} has condition, cannot be inlined")
35
+ self.debug(f"parent {parent_cte.name} has condition, cannot be inlined")
35
36
  continue
36
37
  raw_root = parent_cte.source.datasources[0]
37
38
  if not isinstance(raw_root, Datasource):
38
- self.log(f"parent {parent_cte.name} is not datasource")
39
+ self.debug(f"Parent {parent_cte.name} is not datasource")
39
40
  continue
40
41
  root: Datasource = raw_root
41
42
  if not root.can_be_inlined:
42
- self.log(f"parent {parent_cte.name} datasource is not inlineable")
43
+ self.debug(f"Parent {parent_cte.name} datasource is not inlineable")
43
44
  continue
44
45
  root_outputs = {x.address for x in root.output_concepts}
45
46
  inherited = {
@@ -52,7 +53,9 @@ class InlineDatasource(OptimizationRule):
52
53
  )
53
54
  continue
54
55
  if not root.grain.issubset(parent_cte.grain):
55
- self.log(f"Not all {parent_cte.name} is at wrong grain to inline")
56
+ self.log(
57
+ f"{parent_cte.name} is at wrong grain to inline ({root.grain} vs {parent_cte.grain})"
58
+ )
56
59
  continue
57
60
  to_inline.append(parent_cte)
58
61
 
@@ -62,7 +65,10 @@ class InlineDatasource(OptimizationRule):
62
65
  self.candidates[cte.name].add(replaceable.name)
63
66
  self.count[replaceable.source.name] += 1
64
67
  return True
65
- if self.count[replaceable.source.name] > 1:
68
+ if (
69
+ self.count[replaceable.source.name]
70
+ > CONFIG.optimizations.constant_inline_cutoff
71
+ ):
66
72
  self.log(
67
73
  f"Skipping inlining raw datasource {replaceable.source.name} ({replaceable.name}) due to multiple references"
68
74
  )