pytrilogy 0.0.2.13__tar.gz → 0.0.2.14__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 (106) hide show
  1. {pytrilogy-0.0.2.13/pytrilogy.egg-info → pytrilogy-0.0.2.14}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. pytrilogy-0.0.2.14/tests/test_datatypes.py +58 -0
  4. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_models.py +3 -5
  5. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_parsing.py +2 -2
  6. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_partial_handling.py +1 -1
  7. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_where_clause.py +1 -1
  8. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/__init__.py +1 -1
  9. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/constants.py +12 -2
  10. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/models.py +120 -11
  11. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/predicate_pushdown.py +1 -1
  12. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/common.py +0 -13
  13. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/filter_node.py +1 -15
  14. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/group_node.py +19 -1
  15. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/group_to_node.py +0 -12
  16. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/multiselect_node.py +1 -10
  17. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/rowset_node.py +3 -14
  18. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/select_node.py +26 -0
  19. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/window_node.py +1 -1
  20. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/base_node.py +28 -1
  21. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/group_node.py +31 -18
  22. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/merge_node.py +13 -4
  23. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/select_node_v2.py +4 -0
  24. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/utility.py +91 -3
  25. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/query_processor.py +1 -2
  26. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/common.py +10 -8
  27. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/common.py +18 -2
  28. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/parse_engine.py +13 -9
  29. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/trilogy.lark +2 -2
  30. pytrilogy-0.0.2.13/tests/test_datatypes.py +0 -13
  31. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/LICENSE.md +0 -0
  32. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/README.md +0 -0
  33. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/pyproject.toml +0 -0
  34. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/SOURCES.txt +0 -0
  35. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/dependency_links.txt +0 -0
  36. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/entry_points.txt +0 -0
  37. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/requires.txt +0 -0
  38. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/top_level.txt +0 -0
  39. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/setup.cfg +0 -0
  40. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/setup.py +0 -0
  41. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_declarations.py +0 -0
  42. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_derived_concepts.py +0 -0
  43. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_discovery_nodes.py +0 -0
  44. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_environment.py +0 -0
  45. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_functions.py +0 -0
  46. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_imports.py +0 -0
  47. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_metadata.py +0 -0
  48. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_multi_join_assignments.py +0 -0
  49. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_query_processing.py +0 -0
  50. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_select.py +0 -0
  51. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_statements.py +0 -0
  52. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/tests/test_undefined_concept.py +0 -0
  53. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/compiler.py +0 -0
  54. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/__init__.py +0 -0
  55. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/constants.py +0 -0
  56. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/enums.py +0 -0
  57. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/env_processor.py +0 -0
  58. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/environment_helpers.py +0 -0
  59. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/ergonomics.py +0 -0
  60. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/exceptions.py +0 -0
  61. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/functions.py +0 -0
  62. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/graph_models.py +0 -0
  63. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/internal.py +0 -0
  64. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/optimization.py +0 -0
  65. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/__init__.py +0 -0
  66. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/base_optimization.py +0 -0
  67. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/inline_constant.py +0 -0
  68. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/inline_datasource.py +0 -0
  69. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/__init__.py +0 -0
  70. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  71. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/graph_utils.py +0 -0
  72. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/__init__.py +0 -0
  73. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  74. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  75. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  76. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/__init__.py +0 -0
  77. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/filter_node.py +0 -0
  78. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  79. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/window_node.py +0 -0
  80. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/__init__.py +0 -0
  81. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/base.py +0 -0
  82. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/bigquery.py +0 -0
  83. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/config.py +0 -0
  84. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/duckdb.py +0 -0
  85. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/enums.py +0 -0
  86. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/postgres.py +0 -0
  87. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/presto.py +0 -0
  88. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/snowflake.py +0 -0
  89. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/dialect/sql_server.py +0 -0
  90. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/engine.py +0 -0
  91. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/executor.py +0 -0
  92. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/hooks/__init__.py +0 -0
  93. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/hooks/base_hook.py +0 -0
  94. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/hooks/graph_hook.py +0 -0
  95. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/hooks/query_debugger.py +0 -0
  96. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/metadata/__init__.py +0 -0
  97. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parser.py +0 -0
  98. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/__init__.py +0 -0
  99. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/config.py +0 -0
  100. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/exceptions.py +0 -0
  101. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/helpers.py +0 -0
  102. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/parsing/render.py +0 -0
  103. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/py.typed +0 -0
  104. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/scripts/__init__.py +0 -0
  105. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/scripts/trilogy.py +0 -0
  106. {pytrilogy-0.0.2.13 → pytrilogy-0.0.2.14}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.13
3
+ Version: 0.0.2.14
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.13
3
+ Version: 0.0.2.14
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -0,0 +1,58 @@
1
+ from trilogy.core.models import (
2
+ NumericType,
3
+ )
4
+ from trilogy.parsing.parse_engine import (
5
+ parse_text,
6
+ )
7
+ from trilogy.core.exceptions import InvalidSyntaxException
8
+
9
+
10
+ def test_numeric():
11
+ env, _ = parse_text(
12
+ "const order_id numeric(12,2); const rounded <- cast(order_id as numeric(15,2));"
13
+ )
14
+ assert env.concepts["order_id"].datatype == NumericType(precision=12, scale=2)
15
+
16
+
17
+ def test_cast_error():
18
+ found = False
19
+ try:
20
+ env, _ = parse_text(
21
+ """
22
+ const x <- 1;
23
+ const y <- 'fun';
24
+
25
+ select
26
+ x
27
+ where
28
+ x = y;
29
+
30
+ """
31
+ )
32
+ except InvalidSyntaxException as e:
33
+ assert "Cannot compare DataType.INTEGER and DataType.STRING" in str(e)
34
+ found = True
35
+ if not found:
36
+ assert False, "Expected InvalidSyntaxException not raised"
37
+
38
+
39
+ def test_is_error():
40
+ found = False
41
+ try:
42
+ env, _ = parse_text(
43
+ """
44
+ const x <- 1;
45
+ const y <- 'fun';
46
+
47
+ select
48
+ x
49
+ where
50
+ x is [1,2];
51
+
52
+ """
53
+ )
54
+ except InvalidSyntaxException as e:
55
+ assert "Cannot use is with non-null or boolean value [1, 2]" in str(e)
56
+ found = True
57
+ if not found:
58
+ assert False, "Expected InvalidSyntaxException not raised"
@@ -73,17 +73,15 @@ def test_concept(test_environment, test_environment_graph):
73
73
  def test_concept_filter(test_environment, test_environment_graph):
74
74
  test_concept: Concept = list(test_environment.concepts.values())[0]
75
75
  new = test_concept.with_filter(
76
- Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
76
+ Comparison(left=1, right=2, operator=ComparisonOperator.EQ)
77
77
  )
78
78
  new2 = test_concept.with_filter(
79
- Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
79
+ Comparison(left=1, right=2, operator=ComparisonOperator.EQ)
80
80
  )
81
81
 
82
82
  assert new.name == new2.name != test_concept.name
83
83
 
84
- new3 = new.with_filter(
85
- Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
86
- )
84
+ new3 = new.with_filter(Comparison(left=1, right=2, operator=ComparisonOperator.EQ))
87
85
  assert new3 == new
88
86
 
89
87
 
@@ -34,7 +34,7 @@ def test_in():
34
34
  assert rendered.strip() == "( 1,2,3 )".strip()
35
35
 
36
36
  _, parsed = parse_text(
37
- "const order_id <- 3; SELECT order_id WHERE order_id IN (1);"
37
+ "const order_id <- 3; SELECT order_id WHERE order_id IN (1,);"
38
38
  )
39
39
  query = parsed[-1]
40
40
  right = query.where_clause.conditional.right
@@ -42,7 +42,7 @@ def test_in():
42
42
  right,
43
43
  Parenthetical,
44
44
  ), type(right)
45
- assert right.content == 1
45
+ assert right.content[0] == 1
46
46
  rendered = BaseDialect().render_expr(right)
47
47
  assert rendered.strip() == "( 1 )".strip()
48
48
 
@@ -59,7 +59,7 @@ def setup_titanic(env: Environment):
59
59
  name="survived",
60
60
  namespace=namespace,
61
61
  purpose=Purpose.PROPERTY,
62
- datatype=DataType.BOOL,
62
+ datatype=DataType.INTEGER,
63
63
  keys=(id,),
64
64
  )
65
65
  fare = Concept(
@@ -185,7 +185,7 @@ where
185
185
  def test_like_filter(test_environment):
186
186
  declarations = """
187
187
  property special_order <- filter order_id where like(category_name, 'test') = True;
188
- property special_order_2 <- filter order_id where like(category_name, 'test') = 1;
188
+ property special_order_2 <- filter order_id where like(category_name, 'test') is True;
189
189
 
190
190
  select
191
191
  special_order
@@ -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.13"
7
+ __version__ = "0.0.2.14"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -30,7 +30,13 @@ class Optimizations:
30
30
 
31
31
  @dataclass
32
32
  class Comments:
33
- pass
33
+ """Control what is placed in CTE comments"""
34
+
35
+ show: bool = False
36
+ basic: bool = True
37
+ joins: bool = True
38
+ nullable: bool = False
39
+ partial: bool = False
34
40
 
35
41
 
36
42
  # TODO: support loading from environments
@@ -39,9 +45,13 @@ class Config:
39
45
  strict_mode: bool = True
40
46
  human_identifiers: bool = True
41
47
  validate_missing: bool = True
42
- show_comments: bool = False
48
+ comments: Comments = field(default_factory=Comments)
43
49
  optimizations: Optimizations = field(default_factory=Optimizations)
44
50
 
51
+ @property
52
+ def show_comments(self) -> bool:
53
+ return self.comments.show
54
+
45
55
  def set_random_seed(self, seed: int):
46
56
  random.seed(seed)
47
57
 
@@ -79,6 +79,18 @@ VT = TypeVar("VT")
79
79
  LT = TypeVar("LT")
80
80
 
81
81
 
82
+ def is_compatible_datatype(left, right):
83
+ if left == right:
84
+ return True
85
+ if {left, right} == {DataType.NUMERIC, DataType.FLOAT}:
86
+ return True
87
+ if {left, right} == {DataType.NUMERIC, DataType.INTEGER}:
88
+ return True
89
+ if {left, right} == {DataType.FLOAT, DataType.INTEGER}:
90
+ return True
91
+ return False
92
+
93
+
82
94
  def get_version():
83
95
  from trilogy import __version__
84
96
 
@@ -220,6 +232,7 @@ class DataType(Enum):
220
232
  ARRAY = "array"
221
233
  DATE_PART = "date_part"
222
234
  STRUCT = "struct"
235
+ NULL = "null"
223
236
 
224
237
  # GRANULAR
225
238
  UNIX_SECONDS = "unix_seconds"
@@ -960,6 +973,10 @@ class ColumnAssignment(BaseModel):
960
973
  def is_complete(self) -> bool:
961
974
  return Modifier.PARTIAL not in self.modifiers
962
975
 
976
+ @property
977
+ def is_nullable(self) -> bool:
978
+ return Modifier.NULLABLE in self.modifiers
979
+
963
980
  def with_namespace(self, namespace: str) -> "ColumnAssignment":
964
981
  return ColumnAssignment(
965
982
  alias=(
@@ -1065,9 +1082,12 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1065
1082
  ]
1066
1083
  ]
1067
1084
 
1068
- def __str__(self):
1085
+ def __repr__(self):
1069
1086
  return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
1070
1087
 
1088
+ def __str__(self):
1089
+ return self.__repr__()
1090
+
1071
1091
  @property
1072
1092
  def datatype(self):
1073
1093
  return self.output_datatype
@@ -2072,6 +2092,10 @@ class Datasource(Namespaced, BaseModel):
2072
2092
  def full_concepts(self) -> List[Concept]:
2073
2093
  return [c.concept for c in self.columns if Modifier.PARTIAL not in c.modifiers]
2074
2094
 
2095
+ @property
2096
+ def nullable_concepts(self) -> List[Concept]:
2097
+ return [c.concept for c in self.columns if Modifier.NULLABLE in c.modifiers]
2098
+
2075
2099
  @property
2076
2100
  def output_concepts(self) -> List[Concept]:
2077
2101
  return self.concepts
@@ -2135,13 +2159,27 @@ class InstantiatedUnnestJoin(BaseModel):
2135
2159
  alias: str = "unnest"
2136
2160
 
2137
2161
 
2162
+ class ConceptPair(BaseModel):
2163
+ left: Concept
2164
+ right: Concept
2165
+ modifiers: List[Modifier] = Field(default_factory=list)
2166
+
2167
+ @property
2168
+ def is_partial(self):
2169
+ return Modifier.PARTIAL in self.modifiers
2170
+
2171
+ @property
2172
+ def is_nullable(self):
2173
+ return Modifier.NULLABLE in self.modifiers
2174
+
2175
+
2138
2176
  class BaseJoin(BaseModel):
2139
2177
  left_datasource: Union[Datasource, "QueryDatasource"]
2140
2178
  right_datasource: Union[Datasource, "QueryDatasource"]
2141
2179
  concepts: List[Concept]
2142
2180
  join_type: JoinType
2143
2181
  filter_to_mutual: bool = False
2144
- concept_pairs: list[tuple[Concept, Concept]] | None = None
2182
+ concept_pairs: list[ConceptPair] | None = None
2145
2183
 
2146
2184
  def __init__(self, **data: Any):
2147
2185
  super().__init__(**data)
@@ -2219,7 +2257,7 @@ class BaseJoin(BaseModel):
2219
2257
  return (
2220
2258
  f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
2221
2259
  f" {self.right_datasource.identifier} on"
2222
- f" {','.join([str(k[0])+'='+str(k[1]) for k in self.concept_pairs])}"
2260
+ f" {','.join([str(k.left)+'='+str(k.right) for k in self.concept_pairs])}"
2223
2261
  )
2224
2262
  return (
2225
2263
  f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
@@ -2243,8 +2281,9 @@ class QueryDatasource(BaseModel):
2243
2281
  filter_concepts: List[Concept] = Field(default_factory=list)
2244
2282
  source_type: SourceType = SourceType.SELECT
2245
2283
  partial_concepts: List[Concept] = Field(default_factory=list)
2246
- join_derived_concepts: List[Concept] = Field(default_factory=list)
2247
2284
  hidden_concepts: List[Concept] = Field(default_factory=list)
2285
+ nullable_concepts: List[Concept] = Field(default_factory=list)
2286
+ join_derived_concepts: List[Concept] = Field(default_factory=list)
2248
2287
  force_group: bool | None = None
2249
2288
  existence_source_map: Dict[str, Set[Union[Datasource, "QueryDatasource"]]] = Field(
2250
2289
  default_factory=dict
@@ -2500,6 +2539,7 @@ class CTE(BaseModel):
2500
2539
  joins: List[Union["Join", "InstantiatedUnnestJoin"]] = Field(default_factory=list)
2501
2540
  condition: Optional[Union["Conditional", "Comparison", "Parenthetical"]] = None
2502
2541
  partial_concepts: List[Concept] = Field(default_factory=list)
2542
+ nullable_concepts: List[Concept] = Field(default_factory=list)
2503
2543
  join_derived_concepts: List[Concept] = Field(default_factory=list)
2504
2544
  hidden_concepts: List[Concept] = Field(default_factory=list)
2505
2545
  order_by: Optional[OrderBy] = None
@@ -2583,6 +2623,10 @@ class CTE(BaseModel):
2583
2623
  base += f"\n-- Output: {', '.join([str(x) for x in self.output_columns])}."
2584
2624
  if self.hidden_concepts:
2585
2625
  base += f"\n-- Hidden: {', '.join([str(x) for x in self.hidden_concepts])}."
2626
+ if self.nullable_concepts:
2627
+ base += (
2628
+ f"\n-- Nullable: {', '.join([str(x) for x in self.nullable_concepts])}."
2629
+ )
2586
2630
 
2587
2631
  return base
2588
2632
 
@@ -2677,6 +2721,9 @@ class CTE(BaseModel):
2677
2721
  self.source.output_concepts = unique(
2678
2722
  self.source.output_concepts + other.source.output_concepts, "address"
2679
2723
  )
2724
+ self.nullable_concepts = unique(
2725
+ self.nullable_concepts + other.nullable_concepts, "address"
2726
+ )
2680
2727
  self.hidden_concepts = mutually_hidden
2681
2728
  self.existence_source_map = {
2682
2729
  **self.existence_source_map,
@@ -2842,7 +2889,7 @@ class Join(BaseModel):
2842
2889
  right_cte: CTE | Datasource
2843
2890
  jointype: JoinType
2844
2891
  joinkeys: List[JoinKey]
2845
- joinkey_pairs: List[tuple[Concept, Concept]] | None = None
2892
+ joinkey_pairs: List[ConceptPair] | None = None
2846
2893
 
2847
2894
  @property
2848
2895
  def left_name(self) -> str:
@@ -2877,7 +2924,7 @@ class Join(BaseModel):
2877
2924
  return (
2878
2925
  f"{self.jointype.value} JOIN {self.left_name} and"
2879
2926
  f" {self.right_name} on"
2880
- f" {','.join([str(k[0])+'='+str(k[1]) for k in self.joinkey_pairs])}"
2927
+ f" {','.join([str(k.left)+'='+str(k.right)+str(k.modifiers) for k in self.joinkey_pairs])}"
2881
2928
  )
2882
2929
  return (
2883
2930
  f"{self.jointype.value} JOIN {self.left_name} and"
@@ -3450,11 +3497,41 @@ class Comparison(
3450
3497
  ]
3451
3498
  operator: ComparisonOperator
3452
3499
 
3453
- def __post_init__(self):
3454
- if arg_to_datatype(self.left) != arg_to_datatype(self.right):
3455
- raise SyntaxError(
3456
- f"Cannot compare {self.left} and {self.right} of different types"
3457
- )
3500
+ def __init__(self, *args, **kwargs) -> None:
3501
+ super().__init__(*args, **kwargs)
3502
+ if self.operator in (ComparisonOperator.IS, ComparisonOperator.IS_NOT):
3503
+ if self.right != MagicConstants.NULL and DataType.BOOL != arg_to_datatype(
3504
+ self.right
3505
+ ):
3506
+ raise SyntaxError(
3507
+ f"Cannot use {self.operator.value} with non-null or boolean value {self.right}"
3508
+ )
3509
+ elif self.operator in (ComparisonOperator.IN, ComparisonOperator.NOT_IN):
3510
+ right = arg_to_datatype(self.right)
3511
+ if not isinstance(self.right, Concept) and not isinstance(right, ListType):
3512
+ raise SyntaxError(
3513
+ f"Cannot use {self.operator.value} with non-list type {right} in {str(self)}"
3514
+ )
3515
+
3516
+ elif isinstance(right, ListType) and not is_compatible_datatype(
3517
+ arg_to_datatype(self.left), right.value_data_type
3518
+ ):
3519
+ raise SyntaxError(
3520
+ f"Cannot compare {arg_to_datatype(self.left)} and {right} with operator {self.operator} in {str(self)}"
3521
+ )
3522
+ elif isinstance(self.right, Concept) and not is_compatible_datatype(
3523
+ arg_to_datatype(self.left), arg_to_datatype(self.right)
3524
+ ):
3525
+ raise SyntaxError(
3526
+ f"Cannot compare {arg_to_datatype(self.left)} and {arg_to_datatype(self.right)} with operator {self.operator} in {str(self)}"
3527
+ )
3528
+ else:
3529
+ if not is_compatible_datatype(
3530
+ arg_to_datatype(self.left), arg_to_datatype(self.right)
3531
+ ):
3532
+ raise SyntaxError(
3533
+ f"Cannot compare {arg_to_datatype(self.left)} and {arg_to_datatype(self.right)} of different types with operator {self.operator} in {str(self)}"
3534
+ )
3458
3535
 
3459
3536
  def __add__(self, other):
3460
3537
  if other is None:
@@ -3655,6 +3732,12 @@ class CaseWhen(Namespaced, SelectContext, BaseModel):
3655
3732
  comparison: Conditional | SubselectComparison | Comparison
3656
3733
  expr: "Expr"
3657
3734
 
3735
+ def __str__(self):
3736
+ return self.__repr__()
3737
+
3738
+ def __repr__(self):
3739
+ return f"WHEN {str(self.comparison)} THEN {str(self.expr)}"
3740
+
3658
3741
  @property
3659
3742
  def concept_arguments(self):
3660
3743
  return get_concept_arguments(self.comparison) + get_concept_arguments(self.expr)
@@ -4347,6 +4430,7 @@ class ShowStatement(BaseModel):
4347
4430
 
4348
4431
  Expr = (
4349
4432
  bool
4433
+ | MagicConstants
4350
4434
  | int
4351
4435
  | str
4352
4436
  | float
@@ -4401,9 +4485,34 @@ def dict_to_map_wrapper(arg):
4401
4485
  return MapWrapper(arg, key_type=key_types[0], value_type=value_types[0])
4402
4486
 
4403
4487
 
4488
+ def merge_datatypes(
4489
+ inputs: list[DataType | ListType | StructType | MapType | NumericType],
4490
+ ) -> DataType | ListType | StructType | MapType | NumericType:
4491
+ """This is a temporary hack for doing between
4492
+ allowable datatype transformation matrix"""
4493
+ if len(inputs) == 1:
4494
+ return inputs[0]
4495
+ if set(inputs) == {DataType.INTEGER, DataType.FLOAT}:
4496
+ return DataType.FLOAT
4497
+ if set(inputs) == {DataType.INTEGER, DataType.NUMERIC}:
4498
+ return DataType.NUMERIC
4499
+ if any(isinstance(x, NumericType) for x in inputs) and all(
4500
+ isinstance(x, NumericType)
4501
+ or x in (DataType.INTEGER, DataType.FLOAT, DataType.NUMERIC)
4502
+ for x in inputs
4503
+ ):
4504
+ candidate = next(x for x in inputs if isinstance(x, NumericType))
4505
+ return candidate
4506
+ return inputs[0]
4507
+
4508
+
4404
4509
  def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | NumericType:
4405
4510
  if isinstance(arg, Function):
4406
4511
  return arg.output_datatype
4512
+ elif isinstance(arg, MagicConstants):
4513
+ if arg == MagicConstants.NULL:
4514
+ return DataType.NULL
4515
+ raise ValueError(f"Cannot parse arg datatype for arg of type {arg}")
4407
4516
  elif isinstance(arg, Concept):
4408
4517
  return arg.datatype
4409
4518
  elif isinstance(arg, bool):
@@ -135,7 +135,7 @@ class PredicatePushdown(OptimizationRule):
135
135
  f"Skipping {candidate} as not a basic [no aggregate, etc] condition"
136
136
  )
137
137
  continue
138
- self.log(
138
+ self.debug(
139
139
  f"Checking candidate {candidate}, {type(candidate)}, scalar: {is_scalar_condition(candidate)}"
140
140
  )
141
141
  for parent_cte in cte.parent_ctes:
@@ -15,12 +15,10 @@ from trilogy.utility import unique
15
15
  from trilogy.core.processing.nodes.base_node import StrategyNode
16
16
  from trilogy.core.processing.nodes.merge_node import MergeNode
17
17
  from trilogy.core.processing.nodes import History
18
- from trilogy.core.enums import JoinType
19
18
  from trilogy.core.processing.nodes import (
20
19
  NodeJoin,
21
20
  )
22
21
  from collections import defaultdict
23
- from trilogy.core.processing.utility import concept_to_relevant_joins
24
22
 
25
23
 
26
24
  def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
@@ -218,17 +216,6 @@ def gen_enrichment_node(
218
216
  g=g,
219
217
  parents=[enrich_node, base_node],
220
218
  force_group=False,
221
- node_joins=[
222
- NodeJoin(
223
- left_node=enrich_node,
224
- right_node=base_node,
225
- concepts=concept_to_relevant_joins(
226
- [x for x in join_keys if x in enrich_node.output_lcl]
227
- ),
228
- filter_to_mutual=False,
229
- join_type=JoinType.LEFT_OUTER,
230
- )
231
- ],
232
219
  )
233
220
 
234
221
 
@@ -1,12 +1,10 @@
1
1
  from typing import List
2
2
 
3
3
 
4
- from trilogy.core.enums import JoinType
5
4
  from trilogy.core.models import Concept, Environment, FilterItem, Grain, WhereClause
6
5
  from trilogy.core.processing.nodes import (
7
6
  FilterNode,
8
7
  MergeNode,
9
- NodeJoin,
10
8
  History,
11
9
  StrategyNode,
12
10
  SelectNode,
@@ -16,7 +14,6 @@ from trilogy.core.processing.node_generators.common import (
16
14
  )
17
15
  from trilogy.constants import logger
18
16
  from trilogy.core.processing.utility import padding, unique
19
- from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
20
17
  from trilogy.core.processing.utility import is_scalar_condition
21
18
 
22
19
  LOGGER_PREFIX = "[GEN_FILTER_NODE]"
@@ -215,16 +212,5 @@ def gen_filter_node(
215
212
  # this node fetches only what we need to filter
216
213
  filter_node,
217
214
  enrich_node,
218
- ],
219
- node_joins=[
220
- NodeJoin(
221
- left_node=enrich_node,
222
- right_node=filter_node,
223
- concepts=concept_to_relevant_joins(
224
- [immediate_parent] + parent_row_concepts
225
- ),
226
- join_type=JoinType.LEFT_OUTER,
227
- filter_to_mutual=True,
228
- )
229
- ],
215
+ ]
230
216
  )
@@ -1,4 +1,11 @@
1
- from trilogy.core.models import Concept, Environment, LooseConceptList, WhereClause
1
+ from trilogy.core.models import (
2
+ Concept,
3
+ Environment,
4
+ LooseConceptList,
5
+ WhereClause,
6
+ Function,
7
+ AggregateWrapper,
8
+ )
2
9
  from trilogy.utility import unique
3
10
  from trilogy.core.processing.nodes import GroupNode, StrategyNode, History
4
11
  from typing import List
@@ -42,6 +49,17 @@ def gen_group_node(
42
49
  )
43
50
  parent_concepts += grain_components
44
51
  output_concepts += grain_components
52
+ for possible_agg in local_optional:
53
+ if possible_agg.grain and possible_agg.grain == concept.grain:
54
+ if not isinstance(possible_agg.lineage, (AggregateWrapper, Function)):
55
+ continue
56
+ agg_parents: List[Concept] = resolve_function_parent_concepts(
57
+ possible_agg
58
+ )
59
+ if set([x.address for x in agg_parents]).issubset(
60
+ set([x.address for x in parent_concepts])
61
+ ):
62
+ output_concepts.append(possible_agg)
45
63
 
46
64
  if parent_concepts:
47
65
  logger.info(
@@ -3,15 +3,12 @@ from trilogy.core.processing.nodes import (
3
3
  GroupNode,
4
4
  StrategyNode,
5
5
  MergeNode,
6
- NodeJoin,
7
6
  History,
8
7
  )
9
8
  from typing import List
10
- from trilogy.core.enums import JoinType
11
9
 
12
10
  from trilogy.constants import logger
13
11
  from trilogy.core.processing.utility import padding
14
- from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
15
12
 
16
13
  LOGGER_PREFIX = "[GEN_GROUP_TO_NODE]"
17
14
 
@@ -84,15 +81,6 @@ def gen_group_to_node(
84
81
  # this node gets enrichment
85
82
  enrich_node,
86
83
  ],
87
- node_joins=[
88
- NodeJoin(
89
- left_node=group_node,
90
- right_node=enrich_node,
91
- concepts=concept_to_relevant_joins(parent_concepts),
92
- filter_to_mutual=False,
93
- join_type=JoinType.LEFT_OUTER,
94
- )
95
- ],
96
84
  whole_grain=True,
97
85
  depth=depth,
98
86
  )
@@ -10,7 +10,7 @@ from typing import List
10
10
  from trilogy.core.enums import JoinType
11
11
  from trilogy.constants import logger
12
12
  from trilogy.core.processing.utility import padding
13
- from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
13
+ from trilogy.core.processing.utility import concept_to_relevant_joins
14
14
  from collections import defaultdict
15
15
  from itertools import combinations
16
16
  from trilogy.core.enums import Purpose
@@ -176,14 +176,5 @@ def gen_multiselect_node(
176
176
  # this node gets enrichment
177
177
  enrich_node,
178
178
  ],
179
- node_joins=[
180
- NodeJoin(
181
- left_node=enrich_node,
182
- right_node=node,
183
- concepts=possible_joins,
184
- filter_to_mutual=False,
185
- join_type=JoinType.LEFT_OUTER,
186
- )
187
- ],
188
179
  partial_concepts=node.partial_concepts,
189
180
  )
@@ -6,14 +6,14 @@ from trilogy.core.models import (
6
6
  RowsetItem,
7
7
  MultiSelectStatement,
8
8
  )
9
- from trilogy.core.processing.nodes import MergeNode, NodeJoin, History, StrategyNode
9
+ from trilogy.core.processing.nodes import MergeNode, History, StrategyNode
10
10
  from trilogy.core.processing.nodes.base_node import concept_list_to_grain
11
11
  from typing import List
12
12
 
13
- from trilogy.core.enums import JoinType, PurposeLineage
13
+ from trilogy.core.enums import PurposeLineage
14
14
  from trilogy.constants import logger
15
15
  from trilogy.core.processing.utility import padding
16
- from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
16
+ from trilogy.core.processing.utility import concept_to_relevant_joins
17
17
 
18
18
 
19
19
  LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
@@ -113,19 +113,8 @@ def gen_rowset_node(
113
113
  g=g,
114
114
  depth=depth,
115
115
  parents=[
116
- # this node gets the window
117
116
  node,
118
- # this node gets enrichment
119
117
  enrich_node,
120
118
  ],
121
- node_joins=[
122
- NodeJoin(
123
- left_node=enrich_node,
124
- right_node=node,
125
- concepts=concept_to_relevant_joins(additional_relevant),
126
- filter_to_mutual=False,
127
- join_type=JoinType.LEFT_OUTER,
128
- )
129
- ],
130
119
  partial_concepts=node.partial_concepts,
131
120
  )