pytrilogy 0.0.2.58__py3-none-any.whl → 0.0.3.0__py3-none-any.whl

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.
Files changed (75) hide show
  1. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/METADATA +9 -2
  2. pytrilogy-0.0.3.0.dist-info/RECORD +99 -0
  3. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/WHEEL +1 -1
  4. trilogy/__init__.py +2 -2
  5. trilogy/core/enums.py +1 -7
  6. trilogy/core/env_processor.py +17 -5
  7. trilogy/core/environment_helpers.py +11 -25
  8. trilogy/core/exceptions.py +4 -0
  9. trilogy/core/functions.py +695 -261
  10. trilogy/core/graph_models.py +10 -10
  11. trilogy/core/internal.py +11 -2
  12. trilogy/core/models/__init__.py +0 -0
  13. trilogy/core/models/author.py +2110 -0
  14. trilogy/core/models/build.py +1845 -0
  15. trilogy/core/models/build_environment.py +151 -0
  16. trilogy/core/models/core.py +370 -0
  17. trilogy/core/models/datasource.py +297 -0
  18. trilogy/core/models/environment.py +696 -0
  19. trilogy/core/models/execute.py +931 -0
  20. trilogy/core/optimization.py +14 -16
  21. trilogy/core/optimizations/base_optimization.py +1 -1
  22. trilogy/core/optimizations/inline_constant.py +6 -6
  23. trilogy/core/optimizations/inline_datasource.py +17 -11
  24. trilogy/core/optimizations/predicate_pushdown.py +17 -16
  25. trilogy/core/processing/concept_strategies_v3.py +180 -145
  26. trilogy/core/processing/graph_utils.py +1 -1
  27. trilogy/core/processing/node_generators/basic_node.py +19 -18
  28. trilogy/core/processing/node_generators/common.py +50 -44
  29. trilogy/core/processing/node_generators/filter_node.py +26 -13
  30. trilogy/core/processing/node_generators/group_node.py +26 -21
  31. trilogy/core/processing/node_generators/group_to_node.py +11 -8
  32. trilogy/core/processing/node_generators/multiselect_node.py +60 -43
  33. trilogy/core/processing/node_generators/node_merge_node.py +76 -38
  34. trilogy/core/processing/node_generators/rowset_node.py +57 -36
  35. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +27 -34
  36. trilogy/core/processing/node_generators/select_merge_node.py +161 -64
  37. trilogy/core/processing/node_generators/select_node.py +13 -13
  38. trilogy/core/processing/node_generators/union_node.py +12 -11
  39. trilogy/core/processing/node_generators/unnest_node.py +9 -7
  40. trilogy/core/processing/node_generators/window_node.py +19 -16
  41. trilogy/core/processing/nodes/__init__.py +21 -18
  42. trilogy/core/processing/nodes/base_node.py +82 -66
  43. trilogy/core/processing/nodes/filter_node.py +19 -13
  44. trilogy/core/processing/nodes/group_node.py +50 -35
  45. trilogy/core/processing/nodes/merge_node.py +45 -36
  46. trilogy/core/processing/nodes/select_node_v2.py +53 -39
  47. trilogy/core/processing/nodes/union_node.py +5 -7
  48. trilogy/core/processing/nodes/unnest_node.py +7 -11
  49. trilogy/core/processing/nodes/window_node.py +9 -4
  50. trilogy/core/processing/utility.py +103 -75
  51. trilogy/core/query_processor.py +65 -47
  52. trilogy/core/statements/__init__.py +0 -0
  53. trilogy/core/statements/author.py +413 -0
  54. trilogy/core/statements/build.py +0 -0
  55. trilogy/core/statements/common.py +30 -0
  56. trilogy/core/statements/execute.py +42 -0
  57. trilogy/dialect/base.py +146 -106
  58. trilogy/dialect/common.py +9 -10
  59. trilogy/dialect/duckdb.py +1 -1
  60. trilogy/dialect/enums.py +4 -2
  61. trilogy/dialect/presto.py +1 -1
  62. trilogy/dialect/sql_server.py +1 -1
  63. trilogy/executor.py +44 -32
  64. trilogy/hooks/base_hook.py +6 -4
  65. trilogy/hooks/query_debugger.py +110 -93
  66. trilogy/parser.py +1 -1
  67. trilogy/parsing/common.py +303 -64
  68. trilogy/parsing/parse_engine.py +263 -617
  69. trilogy/parsing/render.py +50 -26
  70. trilogy/scripts/trilogy.py +2 -1
  71. pytrilogy-0.0.2.58.dist-info/RECORD +0 -87
  72. trilogy/core/models.py +0 -4960
  73. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/LICENSE.md +0 -0
  74. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/entry_points.txt +0 -0
  75. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,297 @@
1
+ from typing import TYPE_CHECKING, ItemsView, List, Optional, Union, ValuesView
2
+
3
+ from pydantic import BaseModel, Field, ValidationInfo, field_validator
4
+
5
+ from trilogy.constants import DEFAULT_NAMESPACE, logger
6
+ from trilogy.core.enums import Modifier
7
+ from trilogy.core.models.author import (
8
+ Concept,
9
+ ConceptRef,
10
+ Function,
11
+ Grain,
12
+ HasUUID,
13
+ LooseConceptList,
14
+ Namespaced,
15
+ WhereClause,
16
+ )
17
+
18
+ LOGGER_PREFIX = "[MODELS_DATASOURCE]"
19
+
20
+ if TYPE_CHECKING:
21
+ pass
22
+
23
+
24
+ class RawColumnExpr(BaseModel):
25
+ text: str
26
+
27
+
28
+ class ColumnAssignment(BaseModel):
29
+ alias: str | RawColumnExpr | Function
30
+ concept: ConceptRef
31
+ modifiers: List[Modifier] = Field(default_factory=list)
32
+
33
+ @field_validator("concept", mode="before")
34
+ def force_reference(cls, v: ConceptRef, info: ValidationInfo):
35
+ if isinstance(v, Concept):
36
+ return v.reference
37
+ return v
38
+
39
+ def __eq__(self, other):
40
+ if not isinstance(other, ColumnAssignment):
41
+ return False
42
+ return (
43
+ self.alias == other.alias
44
+ and self.concept == other.concept
45
+ and self.modifiers == other.modifiers
46
+ )
47
+
48
+ @property
49
+ def is_complete(self) -> bool:
50
+ return Modifier.PARTIAL not in self.modifiers
51
+
52
+ @property
53
+ def is_nullable(self) -> bool:
54
+ return Modifier.NULLABLE in self.modifiers
55
+
56
+ def with_namespace(self, namespace: str) -> "ColumnAssignment":
57
+ return ColumnAssignment.model_construct(
58
+ alias=(
59
+ self.alias.with_namespace(namespace)
60
+ if isinstance(self.alias, Function)
61
+ else self.alias
62
+ ),
63
+ concept=self.concept.with_namespace(namespace),
64
+ modifiers=self.modifiers,
65
+ )
66
+
67
+ def with_merge(
68
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
69
+ ) -> "ColumnAssignment":
70
+ return ColumnAssignment.model_construct(
71
+ alias=self.alias,
72
+ concept=self.concept.with_merge(source, target, modifiers),
73
+ modifiers=(
74
+ modifiers if self.concept.address == source.address else self.modifiers
75
+ ),
76
+ )
77
+
78
+
79
+ class Address(BaseModel):
80
+ location: str
81
+ is_query: bool = False
82
+ quoted: bool = False
83
+
84
+
85
+ class Query(BaseModel):
86
+ text: str
87
+
88
+
89
+ class DatasourceMetadata(BaseModel):
90
+ freshness_concept: Concept | None
91
+ partition_fields: List[Concept] = Field(default_factory=list)
92
+ line_no: int | None = None
93
+
94
+
95
+ def safe_grain(v) -> Grain:
96
+ if isinstance(v, dict):
97
+ return Grain.model_validate(v)
98
+ elif isinstance(v, Grain):
99
+ return v
100
+ elif not v:
101
+ return Grain(components=set())
102
+ else:
103
+ raise ValueError(f"Invalid input type to safe_grain {type(v)}")
104
+
105
+
106
+ class Datasource(HasUUID, Namespaced, BaseModel):
107
+ name: str
108
+ columns: List[ColumnAssignment]
109
+ address: Union[Address, str]
110
+ grain: Grain = Field(
111
+ default_factory=lambda: Grain(components=set()), validate_default=True
112
+ )
113
+ namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
114
+ metadata: DatasourceMetadata = Field(
115
+ default_factory=lambda: DatasourceMetadata(freshness_concept=None)
116
+ )
117
+ where: Optional[WhereClause] = None
118
+ non_partial_for: Optional[WhereClause] = None
119
+
120
+ def __eq__(self, other):
121
+ if not isinstance(other, Datasource):
122
+ return False
123
+ return (
124
+ self.name == other.name
125
+ and self.namespace == other.namespace
126
+ and self.grain == other.grain
127
+ and self.address == other.address
128
+ and self.where == other.where
129
+ and self.columns == other.columns
130
+ and self.non_partial_for == other.non_partial_for
131
+ )
132
+
133
+ def duplicate(self) -> "Datasource":
134
+ return self.model_copy(deep=True)
135
+
136
+ @property
137
+ def hidden_concepts(self) -> List[Concept]:
138
+ return []
139
+
140
+ def merge_concept(
141
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
142
+ ):
143
+ original = [c for c in self.columns if c.concept.address == source.address]
144
+ early_exit_check = [
145
+ c for c in self.columns if c.concept.address == target.address
146
+ ]
147
+ if early_exit_check:
148
+ logger.info(
149
+ f"No concept merge needed on merge of {source} to {target}, have {[x.concept.address for x in self.columns]}"
150
+ )
151
+ return None
152
+ if len(original) != 1:
153
+ raise ValueError(
154
+ f"Expected exactly one column to merge, got {len(original)} for {source.address}, {[x.alias for x in original]}"
155
+ )
156
+ # map to the alias with the modifier, and the original
157
+ self.columns = [
158
+ c.with_merge(source, target, modifiers)
159
+ for c in self.columns
160
+ if c.concept.address != source.address
161
+ ] + original
162
+ self.grain = self.grain.with_merge(source, target, modifiers)
163
+ self.where = (
164
+ self.where.with_merge(source, target, modifiers) if self.where else None
165
+ )
166
+
167
+ self.add_column(target, original[0].alias, modifiers)
168
+
169
+ @property
170
+ def identifier(self) -> str:
171
+ if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
172
+ return self.name
173
+ return f"{self.namespace}.{self.name}"
174
+
175
+ @property
176
+ def safe_identifier(self) -> str:
177
+ return self.identifier.replace(".", "_")
178
+
179
+ @property
180
+ def output_lcl(self) -> LooseConceptList:
181
+ return LooseConceptList(concepts=self.output_concepts)
182
+
183
+ @property
184
+ def non_partial_concept_addresses(self) -> set[str]:
185
+ return set([c.address for c in self.full_concepts])
186
+
187
+ @field_validator("namespace", mode="plain")
188
+ @classmethod
189
+ def namespace_validation(cls, v):
190
+ return v or DEFAULT_NAMESPACE
191
+
192
+ @field_validator("address")
193
+ @classmethod
194
+ def address_enforcement(cls, v):
195
+ if isinstance(v, str):
196
+ v = Address(location=v)
197
+ return v
198
+
199
+ @field_validator("grain", mode="before")
200
+ @classmethod
201
+ def grain_enforcement(cls, v: Grain, info: ValidationInfo):
202
+ grain: Grain = safe_grain(v)
203
+ return grain
204
+
205
+ def add_column(
206
+ self,
207
+ concept: Concept,
208
+ alias: str | RawColumnExpr | Function,
209
+ modifiers: List[Modifier] | None = None,
210
+ ):
211
+ self.columns.append(
212
+ ColumnAssignment(
213
+ alias=alias, concept=concept.reference, modifiers=modifiers or []
214
+ )
215
+ )
216
+
217
+ def __add__(self, other):
218
+ if not other == self:
219
+ raise ValueError(
220
+ "Attempted to add two datasources that are not identical, this is not a valid operation"
221
+ )
222
+ return self
223
+
224
+ def __repr__(self):
225
+ return f"Datasource<{self.identifier}@<{self.grain}>"
226
+
227
+ def __str__(self):
228
+ return self.__repr__()
229
+
230
+ def __hash__(self):
231
+ return self.identifier.__hash__()
232
+
233
+ def with_namespace(self, namespace: str):
234
+ new_namespace = (
235
+ namespace + "." + self.namespace
236
+ if self.namespace and self.namespace != DEFAULT_NAMESPACE
237
+ else namespace
238
+ )
239
+ new = Datasource.model_construct(
240
+ name=self.name,
241
+ namespace=new_namespace,
242
+ grain=self.grain.with_namespace(namespace),
243
+ address=self.address,
244
+ columns=[c.with_namespace(namespace) for c in self.columns],
245
+ where=self.where.with_namespace(namespace) if self.where else None,
246
+ )
247
+ return new
248
+
249
+ @property
250
+ def concepts(self) -> List[ConceptRef]:
251
+ return [c.concept for c in self.columns]
252
+
253
+ @property
254
+ def group_required(self):
255
+ return False
256
+
257
+ @property
258
+ def full_concepts(self) -> List[ConceptRef]:
259
+ return [c.concept for c in self.columns if Modifier.PARTIAL not in c.modifiers]
260
+
261
+ @property
262
+ def nullable_concepts(self) -> List[ConceptRef]:
263
+ return [c.concept for c in self.columns if Modifier.NULLABLE in c.modifiers]
264
+
265
+ @property
266
+ def output_concepts(self) -> List[ConceptRef]:
267
+ return self.concepts
268
+
269
+ @property
270
+ def partial_concepts(self) -> List[ConceptRef]:
271
+ return [c.concept for c in self.columns if Modifier.PARTIAL in c.modifiers]
272
+
273
+
274
+ class EnvironmentDatasourceDict(dict):
275
+ def __init__(self, *args, **kwargs) -> None:
276
+ super().__init__(self, *args, **kwargs)
277
+
278
+ def __getitem__(self, key: str) -> Datasource:
279
+ try:
280
+ return super(EnvironmentDatasourceDict, self).__getitem__(key)
281
+ except KeyError:
282
+ if DEFAULT_NAMESPACE + "." + key in self:
283
+ return self.__getitem__(DEFAULT_NAMESPACE + "." + key)
284
+ if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
285
+ return self.__getitem__(key.split(".", 1)[1])
286
+ raise
287
+
288
+ def values(self) -> ValuesView[Datasource]: # type: ignore
289
+ return super().values()
290
+
291
+ def items(self) -> ItemsView[str, Datasource]: # type: ignore
292
+ return super().items()
293
+
294
+ def duplicate(self) -> "EnvironmentDatasourceDict":
295
+ new = EnvironmentDatasourceDict()
296
+ new.update({k: v.duplicate() for k, v in self.items()})
297
+ return new