pytrilogy 0.0.2.12__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.12/pytrilogy.egg-info → pytrilogy-0.0.2.14}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.12 → 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.12 → pytrilogy-0.0.2.14}/tests/test_models.py +3 -5
  5. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_parsing.py +2 -2
  6. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_partial_handling.py +1 -1
  7. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_where_clause.py +1 -1
  8. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/__init__.py +1 -1
  9. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/constants.py +16 -1
  10. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/enums.py +3 -0
  11. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/models.py +150 -17
  12. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/predicate_pushdown.py +1 -1
  13. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/basic_node.py +8 -1
  14. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/common.py +13 -36
  15. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/filter_node.py +1 -15
  16. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/group_node.py +19 -1
  17. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/group_to_node.py +0 -12
  18. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/multiselect_node.py +1 -10
  19. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/rowset_node.py +3 -14
  20. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/select_node.py +26 -0
  21. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/window_node.py +1 -1
  22. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/base_node.py +40 -11
  23. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/group_node.py +31 -18
  24. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/merge_node.py +14 -5
  25. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/select_node_v2.py +4 -0
  26. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/utility.py +91 -3
  27. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/query_processor.py +6 -12
  28. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/common.py +10 -8
  29. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/executor.py +8 -2
  30. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/common.py +34 -4
  31. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/parse_engine.py +31 -19
  32. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/trilogy.lark +5 -5
  33. pytrilogy-0.0.2.12/tests/test_datatypes.py +0 -13
  34. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/LICENSE.md +0 -0
  35. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/README.md +0 -0
  36. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/pyproject.toml +0 -0
  37. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/SOURCES.txt +0 -0
  38. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/dependency_links.txt +0 -0
  39. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/entry_points.txt +0 -0
  40. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/requires.txt +0 -0
  41. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/pytrilogy.egg-info/top_level.txt +0 -0
  42. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/setup.cfg +0 -0
  43. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/setup.py +0 -0
  44. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_declarations.py +0 -0
  45. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_derived_concepts.py +0 -0
  46. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_discovery_nodes.py +0 -0
  47. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_environment.py +0 -0
  48. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_functions.py +0 -0
  49. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_imports.py +0 -0
  50. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_metadata.py +0 -0
  51. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_multi_join_assignments.py +0 -0
  52. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_query_processing.py +0 -0
  53. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_select.py +0 -0
  54. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_statements.py +0 -0
  55. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/tests/test_undefined_concept.py +0 -0
  56. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/compiler.py +0 -0
  57. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/__init__.py +0 -0
  58. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/constants.py +0 -0
  59. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/env_processor.py +0 -0
  60. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/environment_helpers.py +0 -0
  61. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/ergonomics.py +0 -0
  62. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/exceptions.py +0 -0
  63. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/functions.py +0 -0
  64. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/graph_models.py +0 -0
  65. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/internal.py +0 -0
  66. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/optimization.py +0 -0
  67. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/__init__.py +0 -0
  68. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/base_optimization.py +0 -0
  69. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/inline_constant.py +0 -0
  70. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/optimizations/inline_datasource.py +0 -0
  71. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/__init__.py +0 -0
  72. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  73. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/graph_utils.py +0 -0
  74. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/__init__.py +0 -0
  75. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  76. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  77. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/__init__.py +0 -0
  78. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/filter_node.py +0 -0
  79. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  80. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/core/processing/nodes/window_node.py +0 -0
  81. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/__init__.py +0 -0
  82. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/base.py +0 -0
  83. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/bigquery.py +0 -0
  84. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/config.py +0 -0
  85. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/duckdb.py +0 -0
  86. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/enums.py +0 -0
  87. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/postgres.py +0 -0
  88. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/presto.py +0 -0
  89. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/snowflake.py +0 -0
  90. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/dialect/sql_server.py +0 -0
  91. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/engine.py +0 -0
  92. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/hooks/__init__.py +0 -0
  93. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/hooks/base_hook.py +0 -0
  94. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/hooks/graph_hook.py +0 -0
  95. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/hooks/query_debugger.py +0 -0
  96. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/metadata/__init__.py +0 -0
  97. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parser.py +0 -0
  98. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/__init__.py +0 -0
  99. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/config.py +0 -0
  100. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/exceptions.py +0 -0
  101. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/helpers.py +0 -0
  102. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/parsing/render.py +0 -0
  103. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/py.typed +0 -0
  104. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/scripts/__init__.py +0 -0
  105. {pytrilogy-0.0.2.12 → pytrilogy-0.0.2.14}/trilogy/scripts/trilogy.py +0 -0
  106. {pytrilogy-0.0.2.12 → 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.12
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.12
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.12"
7
+ __version__ = "0.0.2.14"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -28,15 +28,30 @@ class Optimizations:
28
28
  direct_return: bool = True
29
29
 
30
30
 
31
+ @dataclass
32
+ class Comments:
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
40
+
41
+
31
42
  # TODO: support loading from environments
32
43
  @dataclass
33
44
  class Config:
34
45
  strict_mode: bool = True
35
46
  human_identifiers: bool = True
36
47
  validate_missing: bool = True
37
- show_comments: bool = False
48
+ comments: Comments = field(default_factory=Comments)
38
49
  optimizations: Optimizations = field(default_factory=Optimizations)
39
50
 
51
+ @property
52
+ def show_comments(self) -> bool:
53
+ return self.comments.show
54
+
40
55
  def set_random_seed(self, seed: int):
41
56
  random.seed(seed)
42
57
 
@@ -62,6 +62,8 @@ class Modifier(Enum):
62
62
  strval = str(value)
63
63
  if strval == "~":
64
64
  return Modifier.PARTIAL
65
+ elif strval == "?":
66
+ return Modifier.NULLABLE
65
67
  return super()._missing_(value=strval.capitalize())
66
68
 
67
69
 
@@ -273,6 +275,7 @@ class SourceType(Enum):
273
275
  CONSTANT = "constant"
274
276
  ROWSET = "rowset"
275
277
  MERGE = "merge"
278
+ BASIC = "basic"
276
279
 
277
280
 
278
281
  class ShowCategory(Enum):
@@ -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
@@ -2264,6 +2303,7 @@ class QueryDatasource(BaseModel):
2264
2303
  @field_validator("joins")
2265
2304
  @classmethod
2266
2305
  def validate_joins(cls, v):
2306
+ unique_pairs = set()
2267
2307
  for join in v:
2268
2308
  if not isinstance(join, BaseJoin):
2269
2309
  continue
@@ -2271,7 +2311,16 @@ class QueryDatasource(BaseModel):
2271
2311
  raise SyntaxError(
2272
2312
  f"Cannot join a datasource to itself, joining {join.left_datasource}"
2273
2313
  )
2274
-
2314
+ pairing = "".join(
2315
+ sorted(
2316
+ [join.left_datasource.identifier, join.right_datasource.identifier]
2317
+ )
2318
+ )
2319
+ if pairing in unique_pairs:
2320
+ raise SyntaxError(
2321
+ f"Duplicate join {join.left_datasource.identifier} and {join.right_datasource.identifier}"
2322
+ )
2323
+ unique_pairs.add(pairing)
2275
2324
  return v
2276
2325
 
2277
2326
  @field_validator("input_concepts")
@@ -2386,6 +2435,11 @@ class QueryDatasource(BaseModel):
2386
2435
  final_source_map[key] = other.source_map[key]
2387
2436
  for k, v in final_source_map.items():
2388
2437
  final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2438
+ self_hidden = self.hidden_concepts or []
2439
+ other_hidden = other.hidden_concepts or []
2440
+ hidden = [
2441
+ x for x in self_hidden if x.address in [y.address for y in other_hidden]
2442
+ ]
2389
2443
  qds = QueryDatasource(
2390
2444
  input_concepts=unique(
2391
2445
  self.input_concepts + other.input_concepts, "address"
@@ -2409,9 +2463,7 @@ class QueryDatasource(BaseModel):
2409
2463
  ),
2410
2464
  join_derived_concepts=self.join_derived_concepts,
2411
2465
  force_group=self.force_group,
2412
- hidden_concepts=unique(
2413
- self.hidden_concepts + other.hidden_concepts, "address"
2414
- ),
2466
+ hidden_concepts=hidden,
2415
2467
  )
2416
2468
 
2417
2469
  return qds
@@ -2487,6 +2539,7 @@ class CTE(BaseModel):
2487
2539
  joins: List[Union["Join", "InstantiatedUnnestJoin"]] = Field(default_factory=list)
2488
2540
  condition: Optional[Union["Conditional", "Comparison", "Parenthetical"]] = None
2489
2541
  partial_concepts: List[Concept] = Field(default_factory=list)
2542
+ nullable_concepts: List[Concept] = Field(default_factory=list)
2490
2543
  join_derived_concepts: List[Concept] = Field(default_factory=list)
2491
2544
  hidden_concepts: List[Concept] = Field(default_factory=list)
2492
2545
  order_by: Optional[OrderBy] = None
@@ -2557,6 +2610,7 @@ class CTE(BaseModel):
2557
2610
  @property
2558
2611
  def comment(self) -> str:
2559
2612
  base = f"Target: {str(self.grain)}."
2613
+ base += f" Source: {self.source.source_type}."
2560
2614
  if self.parent_ctes:
2561
2615
  base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
2562
2616
  if self.joins:
@@ -2565,6 +2619,15 @@ class CTE(BaseModel):
2565
2619
  base += (
2566
2620
  f"\n-- Partials: {', '.join([str(x) for x in self.partial_concepts])}."
2567
2621
  )
2622
+ base += f"\n-- Source Map: {self.source_map}."
2623
+ base += f"\n-- Output: {', '.join([str(x) for x in self.output_columns])}."
2624
+ if self.hidden_concepts:
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
+ )
2630
+
2568
2631
  return base
2569
2632
 
2570
2633
  def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
@@ -2632,6 +2695,10 @@ class CTE(BaseModel):
2632
2695
  f" {self.name} {other.name} conditions {self.condition} {other.condition}"
2633
2696
  )
2634
2697
  raise ValueError(error)
2698
+ mutually_hidden = []
2699
+ for concept in self.hidden_concepts:
2700
+ if concept in other.hidden_concepts:
2701
+ mutually_hidden.append(concept)
2635
2702
  self.partial_concepts = unique(
2636
2703
  self.partial_concepts + other.partial_concepts, "address"
2637
2704
  )
@@ -2654,9 +2721,10 @@ class CTE(BaseModel):
2654
2721
  self.source.output_concepts = unique(
2655
2722
  self.source.output_concepts + other.source.output_concepts, "address"
2656
2723
  )
2657
- self.hidden_concepts = unique(
2658
- self.hidden_concepts + other.hidden_concepts, "address"
2724
+ self.nullable_concepts = unique(
2725
+ self.nullable_concepts + other.nullable_concepts, "address"
2659
2726
  )
2727
+ self.hidden_concepts = mutually_hidden
2660
2728
  self.existence_source_map = {
2661
2729
  **self.existence_source_map,
2662
2730
  **other.existence_source_map,
@@ -2821,7 +2889,7 @@ class Join(BaseModel):
2821
2889
  right_cte: CTE | Datasource
2822
2890
  jointype: JoinType
2823
2891
  joinkeys: List[JoinKey]
2824
- joinkey_pairs: List[tuple[Concept, Concept]] | None = None
2892
+ joinkey_pairs: List[ConceptPair] | None = None
2825
2893
 
2826
2894
  @property
2827
2895
  def left_name(self) -> str:
@@ -2856,7 +2924,7 @@ class Join(BaseModel):
2856
2924
  return (
2857
2925
  f"{self.jointype.value} JOIN {self.left_name} and"
2858
2926
  f" {self.right_name} on"
2859
- 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])}"
2860
2928
  )
2861
2929
  return (
2862
2930
  f"{self.jointype.value} JOIN {self.left_name} and"
@@ -3012,6 +3080,9 @@ class EnvironmentDatasourceDict(dict):
3012
3080
  def values(self) -> ValuesView[Datasource]: # type: ignore
3013
3081
  return super().values()
3014
3082
 
3083
+ def items(self) -> ItemsView[str, Datasource]: # type: ignore
3084
+ return super().items()
3085
+
3015
3086
 
3016
3087
  class EnvironmentConceptDict(dict):
3017
3088
  def __init__(self, *args, **kwargs) -> None:
@@ -3426,11 +3497,41 @@ class Comparison(
3426
3497
  ]
3427
3498
  operator: ComparisonOperator
3428
3499
 
3429
- def __post_init__(self):
3430
- if arg_to_datatype(self.left) != arg_to_datatype(self.right):
3431
- raise SyntaxError(
3432
- f"Cannot compare {self.left} and {self.right} of different types"
3433
- )
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
+ )
3434
3535
 
3435
3536
  def __add__(self, other):
3436
3537
  if other is None:
@@ -3631,6 +3732,12 @@ class CaseWhen(Namespaced, SelectContext, BaseModel):
3631
3732
  comparison: Conditional | SubselectComparison | Comparison
3632
3733
  expr: "Expr"
3633
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
+
3634
3741
  @property
3635
3742
  def concept_arguments(self):
3636
3743
  return get_concept_arguments(self.comparison) + get_concept_arguments(self.expr)
@@ -4323,6 +4430,7 @@ class ShowStatement(BaseModel):
4323
4430
 
4324
4431
  Expr = (
4325
4432
  bool
4433
+ | MagicConstants
4326
4434
  | int
4327
4435
  | str
4328
4436
  | float
@@ -4377,9 +4485,34 @@ def dict_to_map_wrapper(arg):
4377
4485
  return MapWrapper(arg, key_type=key_types[0], value_type=value_types[0])
4378
4486
 
4379
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
+
4380
4509
  def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | NumericType:
4381
4510
  if isinstance(arg, Function):
4382
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}")
4383
4516
  elif isinstance(arg, Concept):
4384
4517
  return arg.datatype
4385
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:
@@ -10,6 +10,7 @@ from trilogy.core.processing.node_generators.common import (
10
10
  )
11
11
  from trilogy.utility import unique
12
12
  from trilogy.constants import logger
13
+ from trilogy.core.enums import SourceType
13
14
  from itertools import combinations
14
15
 
15
16
  LOGGER_PREFIX = "[GEN_BASIC_NODE]"
@@ -35,7 +36,11 @@ def gen_basic_node(
35
36
  attempts: List[tuple[list[Concept], list[Concept]]] = [
36
37
  (parent_concepts, [concept] + local_optional_redundant)
37
38
  ]
38
- equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
39
+ equivalent_optional = [
40
+ x
41
+ for x in local_optional
42
+ if x.lineage == concept.lineage and x.address != concept.address
43
+ ]
39
44
  non_equivalent_optional = [
40
45
  x for x in local_optional if x not in equivalent_optional
41
46
  ]
@@ -61,8 +66,10 @@ def gen_basic_node(
61
66
  depth=depth + 1,
62
67
  history=history,
63
68
  )
69
+
64
70
  if not parent_node:
65
71
  continue
72
+ parent_node.source_type = SourceType.BASIC
66
73
  parents: List[StrategyNode] = [parent_node]
67
74
  for x in basic_output:
68
75
  sources = [p for p in parents if x in p.output_concepts]