pytrilogy 0.0.3.67__py3-none-any.whl → 0.0.3.69__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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.67
3
+ Version: 0.0.3.69
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -40,46 +40,61 @@ Dynamic: summary
40
40
  [![Website](https://img.shields.io/badge/INTRO-WEB-orange?)](https://trilogydata.dev/)
41
41
  [![Discord](https://img.shields.io/badge/DISCORD-CHAT-red?logo=discord)](https://discord.gg/Z4QSSuqGEd)
42
42
 
43
- pytrilogy is an experimental implementation of the Trilogy language, a higher-level SQL that replaces tables/joins with a lightweight semantic binding layer.
43
+ The Trilogy language is an experiment in better SQL for analytics - a streamlined SQL that replaces tables/joins with a lightweight semantic binding layer and provides easy reuse and composability. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
44
44
 
45
- Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want more reusability and composability without losing the expressiveness and iterative value of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
45
+ [pytrilogy](https://github.com/trilogy-data/pytrilogy) is the reference implementation, written in Python.
46
+
47
+ Trilogy concretely solves these common problems in karge, SQL based analytics teams:
48
+ - decoupling consumption code from specific physical assets
49
+ - better testability and change management
50
+ - reduced boilerplate and opportunity for OLAP style optimization at scale
51
+
52
+ Trilogy can be especially powerful as a frontend consumption language, since the decoupling from the physical layout makes dynamic and interactive dashboards backed by SQL tables much easier to create.
46
53
 
47
54
  > [!TIP]
48
- > Try it online in a hosted [open-source studio](https://trilogydata.dev/trilogy-studio-core/). To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
55
+ > You can try Trilogy in a [open-source studio](https://trilogydata.dev/trilogy-studio-core/). More details on the language can be found on the [documentation](https://trilogydata.dev/).
49
56
 
50
- Installation: `pip install pytrilogy`
57
+ We recommend the studio as the fastest way to explore Trilogy. For deeper work and integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
51
58
 
52
- `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
59
+ Installation: `pip install pytrilogy`
53
60
 
54
- You can read more about the project [here](https://trilogydata.dev/) and try out an interactive demo [here](https://trilogydata.dev/demo/).
61
+ ### Trilogy Looks Like SQL
55
62
 
56
- Trilogy looks like SQL:
57
63
  ```sql
64
+ import names;
65
+
66
+ const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
67
+
68
+ def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
69
+
58
70
  WHERE
59
- name like '%lvis%'
71
+ @initcap(name) in top_names
60
72
  SELECT
61
73
  name,
62
- count(name) as name_count
74
+ sum(births) as name_count
63
75
  ORDER BY
64
76
  name_count desc
65
77
  LIMIT 10;
66
78
  ```
67
79
  ## Goals
68
- And aims to:
80
+ Versus SQL, Trilogy aims to:
69
81
 
70
- Preserve:
82
+ Keep:
71
83
  - Correctness
72
84
  - Accessibility
73
85
 
74
- Enhance:
86
+ Improve:
75
87
  - Simplicity
76
- - Understandability
77
88
  - Refactoring/mantainability
78
89
  - Reusability
79
90
 
80
91
  Maintain:
81
92
  - Acceptable performance
82
93
 
94
+ Remove:
95
+ - Lower-level procedural features
96
+ - Transactional optimizations/non-analytics features
97
+
83
98
  ## Hello World
84
99
 
85
100
  Save the following code in a file named `hello.preql`
@@ -141,10 +156,7 @@ SELECT
141
156
  sentences.text
142
157
  ;
143
158
 
144
-
145
-
146
159
  ```
147
-
148
160
  Run the following from the directory the file is in.
149
161
 
150
162
  ```bash
@@ -157,11 +169,15 @@ trilogy run hello.trilogy duckdb
157
169
 
158
170
  The current Trilogy implementation supports these backends:
159
171
 
172
+ ### Core
160
173
  - Bigquery
161
- - SQL Server
162
174
  - DuckDB
163
175
  - Snowflake
164
176
 
177
+ ### Experimental
178
+ - SQL Server
179
+ - Presto
180
+
165
181
  ## Basic Example - Python
166
182
 
167
183
  Trilogy can be run directly in python through the core SDK. Trilogy code can be defined and parsed inline or parsed out of files.
@@ -170,7 +186,6 @@ A bigquery example, similar to bigquery [the quickstart](https://cloud.google.co
170
186
 
171
187
  ```python
172
188
 
173
-
174
189
  from trilogy import Dialects, Environment
175
190
 
176
191
  environment = Environment()
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.67.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=kIt0gE_lZ8Zmqhi-fcE9krdwlETcz0sP0kiCjnsBQog,303
1
+ pytrilogy-0.0.3.69.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=cI0erWMqoxAN4RoZk4oMIaGKoFySaz8SJ8_yrbpiYwA,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=lv_aJWP6dn6e2aF4BAE72jbnNtceFddfqtiDSsvzno0,1692
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -20,11 +20,11 @@ trilogy/core/functions.py,sha256=poVfAwet1xdxTkC7WL38UmGRDpUVO9iSMNWSagl9_r4,293
20
20
  trilogy/core/graph_models.py,sha256=wIT-oBchHWE46GLDkgN5K7EzhOBEo8LfaeWV5G5cYcE,3302
21
21
  trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
22
22
  trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
23
- trilogy/core/query_processor.py,sha256=jA1lek3Kbpi_-NamDPjJuJanUdY7KM3ODB7tS_qqxH4,20311
23
+ trilogy/core/query_processor.py,sha256=t91tYQS0o89r-acdxo8uDUpB1elNcqAhLWkbM8BFjho,20283
24
24
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
25
25
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- trilogy/core/models/author.py,sha256=44VyEInWied287lJp99T14Ahq8RSQK4q3Y-AzAByMTY,77818
27
- trilogy/core/models/build.py,sha256=eNjbFwg_mEcwPeagQpkIybIluIrRCKJLAHdpZjm01e8,64857
26
+ trilogy/core/models/author.py,sha256=Wz_6yEZS5EyXJ5wHHl-C44ikFka5XftBYOjNRK98Hfw,77790
27
+ trilogy/core/models/build.py,sha256=BNHzfqD5NWCKHntvnswvDrtCD1JFgvPedx23cPq942U,65405
28
28
  trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
29
29
  trilogy/core/models/core.py,sha256=EMAuWTngoNVGCdfNrAY7_k6g528iodNQLwPRVip-8DA,10980
30
30
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
@@ -37,24 +37,24 @@ trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP75
37
37
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  trilogy/core/processing/concept_strategies_v3.py,sha256=zy5VZa9LITOws6aIILfv_bSR2-jR1Ndldy-nmwMyQ5w,23144
39
39
  trilogy/core/processing/discovery_loop.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- trilogy/core/processing/discovery_node_factory.py,sha256=73fViHQfKsrFbuk8zfx8YIiaT66pF5AFvw4suV7bXbM,14985
40
+ trilogy/core/processing/discovery_node_factory.py,sha256=I3JJxoF-u8OVvqXXAOhvMg2h-KdpHQwg6EpCeQtxGCI,15123
41
41
  trilogy/core/processing/discovery_utility.py,sha256=3xdd1ypKappSDm0SJs7WtW5YegL80SlYhDQlkNePp4E,4549
42
42
  trilogy/core/processing/discovery_validation.py,sha256=fGWJmKpgEd1f4RkK-fYOBUT1cwsJnahwXFAdRlou7MI,5365
43
43
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
44
- trilogy/core/processing/utility.py,sha256=mrfR9pgek-xjxoDQSlvPqOW9dpmREjgzqn4AGoqpGeM,22774
44
+ trilogy/core/processing/utility.py,sha256=b1F3NT7-MP_-U4KmpC52BOAwLu6mybfndeA1iiZwChw,22016
45
45
  trilogy/core/processing/node_generators/__init__.py,sha256=w8TQQgNhyAra6JQHdg1_Ags4BGyxjXYruu6UeC5yOkI,873
46
46
  trilogy/core/processing/node_generators/basic_node.py,sha256=luN8LftafZepoFgDRv4gmvEGFlOI2j0icJ5fz4UT7uo,5165
47
47
  trilogy/core/processing/node_generators/common.py,sha256=PdysdroW9DUADP7f5Wv_GKPUyCTROZV1g3L45fawxi8,9443
48
48
  trilogy/core/processing/node_generators/filter_node.py,sha256=oRRq2-T3ufgn4D23uQsc58f20eFk-djs4QI3WKA75K8,10908
49
- trilogy/core/processing/node_generators/group_node.py,sha256=S7mzEAVmjc7rnHrJ9PAp4Ld8njcBSrvAu3K1cxsLQdY,6140
49
+ trilogy/core/processing/node_generators/group_node.py,sha256=1QJhRxsTklJ5xq8wHlAURZaN9gL9FPpeCa1OJ7IwXnY,6769
50
50
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
51
51
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
52
52
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=dSqfqWp2SolhDB16nkPaaTXgNQo4QquEufPdf7q0Tb4,17398
53
53
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
54
54
  trilogy/core/processing/node_generators/rowset_node.py,sha256=2BiSsegbRF9csJ_Xl8P_CxIm4dAAb7dF29u6v_Odr-A,6709
55
- trilogy/core/processing/node_generators/select_merge_node.py,sha256=fisCkMyzNDEdDIi9BMuekpIJT5lBF4h0z_pEwR14x9s,21438
55
+ trilogy/core/processing/node_generators/select_merge_node.py,sha256=2KIljy2TFLT9kjKUfhkFwSDkcu4hv4yRVTKOGzEe-NM,21763
56
56
  trilogy/core/processing/node_generators/select_node.py,sha256=Ta1G39V94gjX_AgyZDz9OqnwLz4BjY3D6Drx9YpziMQ,3555
57
- trilogy/core/processing/node_generators/synonym_node.py,sha256=F9DWaKEmJDYnwAmQduTuQP2LCCHqAMDA3oDERRDN2pU,3773
57
+ trilogy/core/processing/node_generators/synonym_node.py,sha256=AnAsa_Wj50NJ_IK0HSgab_7klYmKVrv0WI1uUe-GvEY,3766
58
58
  trilogy/core/processing/node_generators/union_node.py,sha256=VNo6Oey4p8etU9xrOh2oTT2lIOTvY6PULUPRvVa2uxU,2877
59
59
  trilogy/core/processing/node_generators/unnest_node.py,sha256=ueOQtoTf2iJHO09RzWHDFQ5iKZq2fVhGf2KAF2U2kU8,2677
60
60
  trilogy/core/processing/node_generators/window_node.py,sha256=GP3Hvkbb0TDA6ef7W7bmvQEHVH-NRIfBT_0W4fcH3g4,6529
@@ -76,8 +76,8 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
76
76
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
77
77
  trilogy/core/statements/execute.py,sha256=rqfuoMuXPcH7L7TmE1dSiZ_K_A1ohB8whVMfGimZBOk,1294
78
78
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- trilogy/dialect/base.py,sha256=_EkBijBaJGF_d0SBNBCf_mbmDD7N9HrnO2XbRWM8ypg,43629
80
- trilogy/dialect/bigquery.py,sha256=6ghCqy-k7UioIJc1EEQ7gRo_PHaO8Vm7yYbiQ-kgpzs,3629
79
+ trilogy/dialect/base.py,sha256=SpNv1D9eig-XDOui2xoeMvdTfRpejAfJ_HaAMkuPW9w,45112
80
+ trilogy/dialect/bigquery.py,sha256=e19dGcarapgA0x5_Xmq2StyHzuDWPOOPaR4elkWXwug,4203
81
81
  trilogy/dialect/common.py,sha256=hhzuMTFW9QQIP7TKLT9BlJy6lw2R03a68jKQ-7t4-2c,6070
82
82
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
83
83
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
@@ -93,11 +93,11 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
93
93
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
94
94
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
- trilogy/parsing/common.py,sha256=_-KWPP3NvNAHiB4B6jyGSnYKqlLs2XqXeTZiX4MCtWU,30861
96
+ trilogy/parsing/common.py,sha256=_5UEnLtu40VQ8gb6wg3GtSrxf6IONhEOntmdsm0X4lU,30961
97
97
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
98
98
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
99
99
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
100
- trilogy/parsing/parse_engine.py,sha256=O7aM5nZ4SjKlqO2x8XWefI1BMCW06jYYLhABU4k1HCI,72430
100
+ trilogy/parsing/parse_engine.py,sha256=fKsV5eGe31oTVDEjeGSv_pb-222XaVRHtVgzFSnixZk,72439
101
101
  trilogy/parsing/render.py,sha256=gGCFj2ue0UoaU2MR6qHGMAHXkYRMkTmHjnBowdcgFMY,19603
102
102
  trilogy/parsing/trilogy.lark,sha256=x9D1BXtE1E9Kxatx5Kt7xCaid8zgedabwca_B7j7L7o,14331
103
103
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -110,8 +110,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
110
110
  trilogy/std/net.preql,sha256=-bMV6dyofskl4Kvows-iQ4JCxjVUwsZOeWCy8JO5Ftw,135
111
111
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
112
112
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
113
- pytrilogy-0.0.3.67.dist-info/METADATA,sha256=q69gBB9Ympec6F7E9ASBDjTfuRs06HmUws7QvAxVBeA,9095
114
- pytrilogy-0.0.3.67.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- pytrilogy-0.0.3.67.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
- pytrilogy-0.0.3.67.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
- pytrilogy-0.0.3.67.dist-info/RECORD,,
113
+ pytrilogy-0.0.3.69.dist-info/METADATA,sha256=Je-V6aoBf1WujzpfMUFzAt1JduQTZ1ItagHgTRtubmU,9734
114
+ pytrilogy-0.0.3.69.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
+ pytrilogy-0.0.3.69.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
+ pytrilogy-0.0.3.69.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
+ pytrilogy-0.0.3.69.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.67"
7
+ __version__ = "0.0.3.69"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -2303,7 +2303,7 @@ class CustomFunctionFactory:
2303
2303
  ]
2304
2304
  return self
2305
2305
 
2306
- def __call__(self, *creation_args: list[Expr]):
2306
+ def __call__(self, *creation_args: Expr):
2307
2307
  nout = (
2308
2308
  self.function.model_copy(deep=True)
2309
2309
  if isinstance(self.function, BaseModel)
@@ -2389,7 +2389,6 @@ Expr = (
2389
2389
  | int
2390
2390
  | str
2391
2391
  | float
2392
- | list
2393
2392
  | date
2394
2393
  | datetime
2395
2394
  | TupleWrapper
@@ -2430,7 +2429,6 @@ FuncArgs = (
2430
2429
  | ListType
2431
2430
  | MapType
2432
2431
  | NumericType
2433
- | list
2434
2432
  | ListWrapper[Any]
2435
2433
  | TupleWrapper[Any]
2436
2434
  | Comparison
@@ -155,6 +155,7 @@ def concepts_to_build_grain_concepts(
155
155
  pconcepts.append(c)
156
156
  elif environment:
157
157
  pconcepts.append(environment.concepts[c])
158
+
158
159
  else:
159
160
  raise ValueError(
160
161
  f"Unable to resolve input {c} without environment provided to concepts_to_grain call"
@@ -248,7 +249,7 @@ def get_concept_arguments(expr) -> List["BuildConcept"]:
248
249
  return output
249
250
 
250
251
 
251
- class BuildParamaterizedConceptReference(BaseModel):
252
+ class BuildParamaterizedConceptReference(DataTyped, BaseModel):
252
253
  concept: BuildConcept
253
254
 
254
255
  def __str__(self):
@@ -258,6 +259,10 @@ class BuildParamaterizedConceptReference(BaseModel):
258
259
  def safe_address(self) -> str:
259
260
  return self.concept.safe_address
260
261
 
262
+ @property
263
+ def output_datatype(self) -> DataType:
264
+ return self.concept.output_datatype
265
+
261
266
 
262
267
  class BuildGrain(BaseModel):
263
268
  components: set[str] = Field(default_factory=set)
@@ -1810,8 +1815,8 @@ class Factory:
1810
1815
  right_c, _ = self.instantiate_concept(base.right)
1811
1816
  right = right_c
1812
1817
  return BuildSubselectComparison.model_construct(
1813
- left=self.build(base.left),
1814
- right=self.build(right),
1818
+ left=self.handle_constant(self.build(base.left)),
1819
+ right=self.handle_constant(self.build(right)),
1815
1820
  operator=base.operator,
1816
1821
  )
1817
1822
 
@@ -1916,7 +1921,17 @@ class Factory:
1916
1921
  where_factory = Factory(
1917
1922
  grain=Grain(), environment=self.environment, local_concepts={}
1918
1923
  )
1919
-
1924
+ where_clause = (
1925
+ where_factory.build(base.where_clause) if base.where_clause else None
1926
+ )
1927
+ # if the where clause derives new concepts
1928
+ # we need to ensure these are accessible from the general factory
1929
+ # post resolution
1930
+ for bk, bv in where_factory.local_concepts.items():
1931
+ # but do not override any local cahced grains
1932
+ if bk in materialized:
1933
+ continue
1934
+ materialized[bk] = bv
1920
1935
  final: List[BuildConcept] = []
1921
1936
  for original in base.selection:
1922
1937
  new = original
@@ -1943,9 +1958,7 @@ class Factory:
1943
1958
  factory.build(base.having_clause) if base.having_clause else None
1944
1959
  ),
1945
1960
  # this uses a different grain factory
1946
- where_clause=(
1947
- where_factory.build(base.where_clause) if base.where_clause else None
1948
- ),
1961
+ where_clause=where_clause,
1949
1962
  )
1950
1963
 
1951
1964
  @build.register
@@ -307,16 +307,17 @@ class RootNodeHandler:
307
307
  def _resolve_root_concepts(
308
308
  self, root_targets: List[BuildConcept]
309
309
  ) -> Optional[StrategyNode]:
310
- synonym_node = self._try_synonym_resolution(root_targets)
311
- if synonym_node:
312
- logger.info(
313
- f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
314
- f"resolved root concepts through synonyms"
315
- )
316
- return synonym_node
317
310
  expanded_node = self._try_merge_expansion(root_targets)
318
311
  if expanded_node:
319
312
  return expanded_node
313
+ if self.ctx.accept_partial:
314
+ synonym_node = self._try_synonym_resolution(root_targets)
315
+ if synonym_node:
316
+ logger.info(
317
+ f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
318
+ f"resolved root concepts through synonyms"
319
+ )
320
+ return synonym_node
320
321
 
321
322
  return None
322
323
 
@@ -468,6 +469,7 @@ def generate_node(
468
469
  Derivation.GROUP_TO: lambda: _generate_group_to_node(context),
469
470
  Derivation.BASIC: lambda: _generate_basic_node(context),
470
471
  Derivation.ROOT: lambda: RootNodeHandler(context).generate(),
472
+ Derivation.CONSTANT: lambda: RootNodeHandler(context).generate(),
471
473
  }
472
474
 
473
475
  handler = derivation_handlers.get(concept.derivation)
@@ -22,6 +22,24 @@ from trilogy.utility import unique
22
22
  LOGGER_PREFIX = "[GEN_GROUP_NODE]"
23
23
 
24
24
 
25
+ def get_aggregate_grain(
26
+ concept: BuildConcept, environment: BuildEnvironment
27
+ ) -> BuildGrain:
28
+ parent_concepts: List[BuildConcept] = unique(
29
+ resolve_function_parent_concepts(concept, environment=environment), "address"
30
+ )
31
+ if (
32
+ concept.grain
33
+ and len(concept.grain.components) > 0
34
+ and not concept.grain.abstract
35
+ ):
36
+ grain_components = [environment.concepts[c] for c in concept.grain.components]
37
+ parent_concepts += grain_components
38
+ return BuildGrain.from_concepts(parent_concepts)
39
+ else:
40
+ return BuildGrain.from_concepts(parent_concepts)
41
+
42
+
25
43
  def gen_group_node(
26
44
  concept: BuildConcept,
27
45
  local_optional: List[BuildConcept],
@@ -51,7 +69,7 @@ def gen_group_node(
51
69
  ):
52
70
  grain_components = [environment.concepts[c] for c in concept.grain.components]
53
71
  parent_concepts += grain_components
54
- build_grain_parents = BuildGrain.from_concepts(parent_concepts)
72
+ build_grain_parents = get_aggregate_grain(concept, environment)
55
73
  output_concepts += grain_components
56
74
  for possible_agg in local_optional:
57
75
 
@@ -70,6 +88,7 @@ def gen_group_node(
70
88
  possible_agg,
71
89
  environment=environment,
72
90
  )
91
+ comp_grain = get_aggregate_grain(possible_agg, environment)
73
92
  if set([x.address for x in agg_parents]).issubset(
74
93
  set([x.address for x in parent_concepts])
75
94
  ):
@@ -77,7 +96,7 @@ def gen_group_node(
77
96
  logger.info(
78
97
  f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
79
98
  )
80
- elif BuildGrain.from_concepts(agg_parents) == build_grain_parents:
99
+ elif comp_grain == build_grain_parents:
81
100
  extra = [x for x in agg_parents if x.address not in parent_concepts]
82
101
  parent_concepts += extra
83
102
  output_concepts.append(possible_agg)
@@ -86,7 +105,7 @@ def gen_group_node(
86
105
  )
87
106
  else:
88
107
  logger.info(
89
- f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; mismatched parent grain {BuildGrain.from_concepts(agg_parents)} vs local parent {BuildGrain.from_concepts(parent_concepts)}"
108
+ f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; it has mismatched parent grain {comp_grain } vs local parent {build_grain_parents}"
90
109
  )
91
110
  if parent_concepts:
92
111
  logger.info(
@@ -231,7 +231,10 @@ def create_pruned_concept_graph(
231
231
 
232
232
 
233
233
  def resolve_subgraphs(
234
- g: nx.DiGraph, relevant: list[BuildConcept], conditions: BuildWhereClause | None
234
+ g: nx.DiGraph,
235
+ relevant: list[BuildConcept],
236
+ conditions: BuildWhereClause | None,
237
+ depth: int = 0,
235
238
  ) -> dict[str, list[str]]:
236
239
  """When we have multiple distinct subgraphs within our matched
237
240
  nodes that can satisfy a query, resolve which one of those we should
@@ -241,7 +244,7 @@ def resolve_subgraphs(
241
244
  discarding duplicates.
242
245
  Duplicate subgraphs will be resolved based on which
243
246
  ones are most 'optimal' to use, a hueristic
244
- that can evolve in the future but is currently based on
247
+ that can evolve in the future but is currently based on datasource
245
248
  cardinality."""
246
249
  datasources = [n for n in g.nodes if n.startswith("ds~")]
247
250
  subgraphs: dict[str, list[str]] = {
@@ -261,7 +264,7 @@ def resolve_subgraphs(
261
264
  pruned_subgraphs = {}
262
265
 
263
266
  def score_node(input: str):
264
- logger.debug(f"scoring node {input}")
267
+ logger.debug(f"{padding(depth)}{LOGGER_PREFIX} scoring node {input}")
265
268
  grain = grain_length[input]
266
269
  # first - go for lowest grain
267
270
  # but if the object we want is in the grain, treat that as "free"
@@ -275,7 +278,7 @@ def resolve_subgraphs(
275
278
  len(subgraphs[input]),
276
279
  input,
277
280
  )
278
- logger.debug(score)
281
+ logger.debug(f"{padding(depth)}{LOGGER_PREFIX} node {input} has score {score}")
279
282
  return score
280
283
 
281
284
  for key, nodes in subgraphs.items():
@@ -296,7 +299,7 @@ def resolve_subgraphs(
296
299
  if len(value) < len(other_value):
297
300
  is_subset = True
298
301
  logger.debug(
299
- f"Dropping subgraph {key} with {value} as it is a subset of {other_key} with {other_value}"
302
+ f"{padding(depth)}{LOGGER_PREFIX} Dropping subgraph {key} with {value} as it is a subset of {other_key} with {other_value}"
300
303
  )
301
304
  elif len(value) == len(other_value) and len(all_concepts) == len(
302
305
  other_all_concepts
@@ -305,7 +308,9 @@ def resolve_subgraphs(
305
308
  matches.add(key)
306
309
  if matches and not is_subset:
307
310
  min_node = min(matches, key=score_node)
308
- logger.debug(f"minimum source score is {min_node}")
311
+ logger.debug(
312
+ f"{padding(depth)}{LOGGER_PREFIX} minimum source score is {min_node}"
313
+ )
309
314
  is_subset = key is not min(matches, key=score_node)
310
315
  if not is_subset:
311
316
  pruned_subgraphs[key] = nodes
@@ -330,7 +335,9 @@ def resolve_subgraphs(
330
335
  > 1
331
336
  )
332
337
  if not keep:
333
- logger.debug(f"Pruning node {node} as irrelevant after subgraph resolution")
338
+ logger.debug(
339
+ f"{padding(depth)}{LOGGER_PREFIX} Pruning node {node} as irrelevant after subgraph resolution"
340
+ )
334
341
  pruned_subgraphs = {
335
342
  k: [n for n in v if n != node] for k, v in pruned_subgraphs.items()
336
343
  }
@@ -493,7 +500,7 @@ def create_select_node(
493
500
  else:
494
501
 
495
502
  candidate = bcandidate
496
- assert candidate.resolve().output_concepts == all_concepts
503
+
497
504
  return candidate
498
505
 
499
506
 
@@ -536,7 +543,12 @@ def gen_select_merge_node(
536
543
  force_group=False,
537
544
  conditions=conditions.conditional if conditions else None,
538
545
  )
539
- for attempt in [False, True]:
546
+ attempts = [
547
+ False,
548
+ ]
549
+ if accept_partial:
550
+ attempts.append(True)
551
+ for attempt in attempts:
540
552
  pruned_concept_graph = create_pruned_concept_graph(
541
553
  g,
542
554
  non_constant,
@@ -556,7 +568,7 @@ def gen_select_merge_node(
556
568
  return None
557
569
 
558
570
  sub_nodes = resolve_subgraphs(
559
- pruned_concept_graph, relevant=non_constant, conditions=conditions
571
+ pruned_concept_graph, relevant=non_constant, conditions=conditions, depth=depth
560
572
  )
561
573
 
562
574
  logger.info(f"{padding(depth)}{LOGGER_PREFIX} fetching subgraphs {sub_nodes}")
@@ -3,8 +3,8 @@ from collections import defaultdict
3
3
  from typing import List
4
4
 
5
5
  from trilogy.constants import logger
6
- from trilogy.core.enums import FunctionType
7
- from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
6
+ from trilogy.core.enums import Derivation
7
+ from trilogy.core.models.build import BuildConcept, BuildWhereClause
8
8
  from trilogy.core.models.build_environment import BuildEnvironment
9
9
  from trilogy.core.processing.nodes import History, StrategyNode
10
10
  from trilogy.core.processing.utility import padding
@@ -12,13 +12,6 @@ from trilogy.core.processing.utility import padding
12
12
  LOGGER_PREFIX = "[GEN_SYNONYM_NODE]"
13
13
 
14
14
 
15
- def is_union(c: BuildConcept):
16
- return (
17
- isinstance(c.lineage, BuildFunction)
18
- and c.lineage.operator == FunctionType.UNION
19
- )
20
-
21
-
22
15
  def gen_synonym_node(
23
16
  all_concepts: List[BuildConcept],
24
17
  environment: BuildEnvironment,
@@ -41,7 +34,6 @@ def gen_synonym_node(
41
34
  synonyms[x.address].append(parent)
42
35
  has_synonyms = True
43
36
  for y in x.pseudonyms:
44
-
45
37
  if y in environment.alias_origin_lookup:
46
38
  synonyms[x.address].append(environment.alias_origin_lookup[y])
47
39
  has_synonyms = True
@@ -59,11 +51,14 @@ def gen_synonym_node(
59
51
  itertools.product(*(synonyms[obj] for obj in sorted_keys))
60
52
  )
61
53
 
62
- def similarity_sort_key(combo):
54
+ def similarity_sort_key(combo: tuple[BuildConcept, ...]):
63
55
  addresses = [x.address for x in combo]
64
56
 
65
57
  # Calculate similarity score - count how many pairs share prefixes
66
58
  similarity_score = 0
59
+ roots = sum(
60
+ [1 for x in combo if x.derivation in (Derivation.ROOT, Derivation.CONSTANT)]
61
+ )
67
62
  for i in range(len(addresses)):
68
63
  for j in range(i + 1, len(addresses)):
69
64
  # Find common prefix length
@@ -77,8 +72,8 @@ def gen_synonym_node(
77
72
  break
78
73
  similarity_score += common_prefix_len
79
74
 
80
- # Sort by similarity (descending), then by addresses (ascending) for ties
81
- return (-similarity_score, addresses)
75
+ # Sort by roots, similarity (descending), then by addresses (ascending) for ties
76
+ return (-roots, -similarity_score, addresses)
82
77
 
83
78
  combinations_list.sort(key=similarity_sort_key)
84
79
  for combo in combinations_list:
@@ -322,8 +322,10 @@ def resolve_instantiated_concept(
322
322
  for k in concept.pseudonyms:
323
323
  if k in datasource.output_concepts:
324
324
  return [x for x in datasource.output_concepts if x.address == k].pop()
325
+ if any(k in x.pseudonyms for x in datasource.output_concepts):
326
+ return [x for x in datasource.output_concepts if k in x.pseudonyms].pop()
325
327
  raise SyntaxError(
326
- f"Could not find {concept.address} in {datasource.identifier} output {[c.address for c in datasource.output_concepts]}"
328
+ f"Could not find {concept.address} in {datasource.identifier} output {[c.address for c in datasource.output_concepts]}, acceptable synonyms {concept.pseudonyms}"
327
329
  )
328
330
 
329
331
 
@@ -601,19 +603,28 @@ def find_nullable_concepts(
601
603
 
602
604
 
603
605
  def sort_select_output_processed(
604
- cte: CTE | UnionCTE, query: ProcessedQuery
606
+ cte: CTE | UnionCTE, query: SelectStatement | MultiSelectStatement | ProcessedQuery
605
607
  ) -> CTE | UnionCTE:
606
- output_addresses = [c.address for c in query.output_columns]
608
+ if isinstance(query, ProcessedQuery):
609
+ targets = query.output_columns
610
+ hidden = query.hidden_columns
611
+ else:
612
+ targets = query.output_components
613
+ hidden = query.hidden_components
614
+
615
+ output_addresses = [c.address for c in targets]
607
616
 
608
617
  mapping = {x.address: x for x in cte.output_columns}
609
618
 
610
619
  new_output: list[BuildConcept] = []
611
- for x in query.output_columns:
620
+ for x in targets:
612
621
  if x.address in mapping:
613
622
  new_output.append(mapping[x.address])
614
623
  for oc in cte.output_columns:
615
624
  if x.address in oc.pseudonyms:
616
625
  # create a wrapper BuildConcept to render the pseudonym under the original name
626
+ if any(x.address == y for y in mapping.keys()):
627
+ continue
617
628
  new_output.append(
618
629
  BuildConcept(
619
630
  name=x.name,
@@ -636,10 +647,7 @@ def sort_select_output_processed(
636
647
  [
637
648
  c.address
638
649
  for c in cte.output_columns
639
- if (
640
- c.address not in query.output_columns
641
- or c.address in query.hidden_columns
642
- )
650
+ if (c.address not in targets or c.address in hidden)
643
651
  ]
644
652
  )
645
653
  cte.output_columns = new_output
@@ -650,38 +658,4 @@ def sort_select_output(
650
658
  cte: CTE | UnionCTE, query: SelectStatement | MultiSelectStatement | ProcessedQuery
651
659
  ) -> CTE | UnionCTE:
652
660
 
653
- if isinstance(query, ProcessedQuery):
654
- return sort_select_output_processed(cte, query)
655
-
656
- mapping = {x.address: x for x in cte.output_columns}
657
-
658
- new_output: list[BuildConcept] = []
659
- for x in query.output_components:
660
- if x.address in mapping:
661
- new_output.append(mapping[x.address])
662
- else:
663
- for oc in cte.output_columns:
664
- if x.address in oc.pseudonyms:
665
- # create a wrapper BuildConcept to render the pseudonym under the original name
666
- new_output.append(
667
- BuildConcept(
668
- name=x.name,
669
- namespace=x.namespace,
670
- pseudonyms={oc.address},
671
- datatype=oc.datatype,
672
- purpose=oc.purpose,
673
- grain=oc.grain,
674
- build_is_aggregate=oc.build_is_aggregate,
675
- )
676
- )
677
- break
678
- cte.output_columns = new_output
679
- cte.hidden_concepts = set(
680
- [
681
- c.address
682
- for c in query.output_components
683
- if c.address in query.hidden_components
684
- ]
685
- )
686
-
687
- return cte
661
+ return sort_select_output_processed(cte, query)
@@ -395,14 +395,13 @@ def get_query_node(
395
395
  if not statement.output_components:
396
396
  raise ValueError(f"Statement has no output components {statement}")
397
397
  history = history or History(base_environment=environment)
398
- print(
398
+ logger.info(
399
399
  f"{LOGGER_PREFIX} building query node for {statement.output_components} grain {statement.grain}"
400
400
  )
401
401
  build_statement: BuildSelectLineage | BuildMultiSelectLineage = Factory(
402
402
  environment=environment,
403
403
  ).build(statement)
404
404
 
405
- # build_statement = statement
406
405
  build_environment = environment.materialize_for_select(
407
406
  build_statement.local_concepts
408
407
  )
trilogy/dialect/base.py CHANGED
@@ -12,6 +12,7 @@ from trilogy.constants import (
12
12
  )
13
13
  from trilogy.core.constants import UNNEST_NAME
14
14
  from trilogy.core.enums import (
15
+ ComparisonOperator,
15
16
  DatePart,
16
17
  FunctionType,
17
18
  UnnestMode,
@@ -276,7 +277,13 @@ ORDER BY{% for order in order_by %}
276
277
  )
277
278
 
278
279
 
279
- def safe_get_cte_value(coalesce, cte: CTE | UnionCTE, c: BuildConcept, quote_char: str):
280
+ def safe_get_cte_value(
281
+ coalesce,
282
+ cte: CTE | UnionCTE,
283
+ c: BuildConcept,
284
+ quote_char: str,
285
+ render_expr: Callable,
286
+ ) -> Optional[str]:
280
287
  address = c.address
281
288
  raw = cte.source_map.get(address, None)
282
289
 
@@ -287,6 +294,9 @@ def safe_get_cte_value(coalesce, cte: CTE | UnionCTE, c: BuildConcept, quote_cha
287
294
  return f"{quote_char}{raw}{quote_char}.{safe_quote(rendered, quote_char)}"
288
295
  if isinstance(raw, list) and len(raw) == 1:
289
296
  rendered = cte.get_alias(c, raw[0])
297
+ if isinstance(rendered, FUNCTION_ITEMS):
298
+ # if it's a function, we need to render it as a function
299
+ return f"{render_expr(rendered, cte=cte, raise_invalid=True)}"
290
300
  return f"{quote_char}{raw[0]}{quote_char}.{safe_quote(rendered, quote_char)}"
291
301
  return coalesce(
292
302
  sorted(
@@ -499,6 +509,7 @@ class BaseDialect:
499
509
  cte,
500
510
  c,
501
511
  self.QUOTE_CHARACTER,
512
+ self.render_expr,
502
513
  )
503
514
  if not rval:
504
515
  # unions won't have a specific source mapped; just use a generic column reference
@@ -515,6 +526,17 @@ class BaseDialect:
515
526
  )
516
527
  return rval
517
528
 
529
+ def render_array_unnest(
530
+ self,
531
+ left,
532
+ right,
533
+ operator: ComparisonOperator,
534
+ cte: CTE | UnionCTE | None = None,
535
+ cte_map: Optional[Dict[str, CTE | UnionCTE]] = None,
536
+ raise_invalid: bool = False,
537
+ ):
538
+ return f"{self.render_expr(left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {operator.value} {self.render_expr(right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
539
+
518
540
  def render_expr(
519
541
  self,
520
542
  e: Union[
@@ -556,6 +578,7 @@ class BaseDialect:
556
578
  raise_invalid: bool = False,
557
579
  ) -> str:
558
580
  if isinstance(e, SUBSELECT_COMPARISON_ITEMS):
581
+
559
582
  if isinstance(e.right, BuildConcept):
560
583
  # we won't always have an existnce map
561
584
  # so fall back to the normal map
@@ -585,10 +608,22 @@ class BaseDialect:
585
608
  info = cte.inlined_ctes[target]
586
609
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {info.new_base} as {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
587
610
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
588
-
611
+ elif isinstance(e.right, BuildParamaterizedConceptReference):
612
+ if isinstance(e.right.concept.lineage, BuildFunction) and isinstance(
613
+ e.right.concept.lineage.arguments[0], ListWrapper
614
+ ):
615
+ return self.render_array_unnest(
616
+ e.left,
617
+ e.right,
618
+ e.operator,
619
+ cte=cte,
620
+ cte_map=cte_map,
621
+ raise_invalid=raise_invalid,
622
+ )
623
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
589
624
  elif isinstance(
590
625
  e.right,
591
- (ListWrapper, TupleWrapper, BuildParenthetical, list),
626
+ (ListWrapper, TupleWrapper, BuildParenthetical),
592
627
  ):
593
628
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
594
629
 
@@ -692,13 +727,11 @@ class BaseDialect:
692
727
  return f"'{e}'"
693
728
  elif isinstance(e, (int, float)):
694
729
  return str(e)
695
- elif isinstance(e, ListWrapper):
696
- return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}]"
697
730
  elif isinstance(e, TupleWrapper):
698
731
  return f"({','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])})"
699
732
  elif isinstance(e, MapWrapper):
700
733
  return f"MAP {{{','.join([f'{self.render_expr(k, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}:{self.render_expr(v, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}' for k, v in e.items()])}}}"
701
- elif isinstance(e, list):
734
+ elif isinstance(e, ListWrapper):
702
735
  return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}"
703
736
  elif isinstance(e, DataType):
704
737
  return self.DATATYPE_MAP.get(e, e.value)
@@ -762,6 +795,7 @@ class BaseDialect:
762
795
  ] + [
763
796
  f"{self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
764
797
  for c in cte.join_derived_concepts
798
+ if c.address not in cte.hidden_concepts
765
799
  ]
766
800
  elif self.UNNEST_MODE in (UnnestMode.CROSS_JOIN_UNNEST, UnnestMode.PRESTO):
767
801
  select_columns = [
@@ -772,6 +806,7 @@ class BaseDialect:
772
806
  ] + [
773
807
  f"{UNNEST_NAME} as {self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
774
808
  for c in cte.join_derived_concepts
809
+ if c.address not in cte.hidden_concepts
775
810
  ]
776
811
  else:
777
812
  # otherwse, assume we are unnesting directly in the select
@@ -1,11 +1,17 @@
1
- from typing import Any, Callable, Mapping
1
+ from typing import Any, Callable, Dict, Mapping, Optional
2
2
 
3
3
  from jinja2 import Template
4
4
 
5
- from trilogy.core.enums import FunctionType, UnnestMode, WindowType
5
+ from trilogy.core.enums import (
6
+ ComparisonOperator,
7
+ FunctionType,
8
+ UnnestMode,
9
+ WindowType,
10
+ )
6
11
  from trilogy.core.models.core import (
7
12
  DataType,
8
13
  )
14
+ from trilogy.core.models.execute import CTE, UnionCTE
9
15
  from trilogy.dialect.base import BaseDialect
10
16
 
11
17
  WINDOW_FUNCTION_MAP: Mapping[WindowType, Callable[[Any, Any, Any], str]] = {}
@@ -99,3 +105,14 @@ class BigqueryDialect(BaseDialect):
99
105
  SQL_TEMPLATE = BQ_SQL_TEMPLATE
100
106
  UNNEST_MODE = UnnestMode.CROSS_JOIN_UNNEST
101
107
  DATATYPE_MAP = DATATYPE_MAP
108
+
109
+ def render_array_unnest(
110
+ self,
111
+ left,
112
+ right,
113
+ operator: ComparisonOperator,
114
+ cte: CTE | UnionCTE | None = None,
115
+ cte_map: Optional[Dict[str, CTE | UnionCTE]] = None,
116
+ raise_invalid: bool = False,
117
+ ):
118
+ return f"{self.render_expr(left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {operator.value} unnest({self.render_expr(right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)})"
trilogy/parsing/common.py CHANGED
@@ -44,6 +44,7 @@ from trilogy.core.models.author import (
44
44
  RowsetLineage,
45
45
  SubselectComparison,
46
46
  TraitDataType,
47
+ TupleWrapper,
47
48
  UndefinedConcept,
48
49
  WhereClause,
49
50
  WindowItem,
@@ -172,7 +173,7 @@ def concept_list_to_keys(
172
173
 
173
174
 
174
175
  def constant_to_concept(
175
- parent: ListWrapper | MapWrapper | list | int | float | str,
176
+ parent: ListWrapper | TupleWrapper | MapWrapper | int | float | str,
176
177
  name: str,
177
178
  namespace: str,
178
179
  metadata: Metadata | None = None,
@@ -183,7 +184,7 @@ def constant_to_concept(
183
184
  output_purpose=Purpose.CONSTANT,
184
185
  arguments=[parent],
185
186
  )
186
- assert const_function.arguments[0] == parent, const_function.arguments[0]
187
+ # assert const_function.arguments[0] == parent, f'{const_function.arguments[0]} != {parent}, {type(const_function.arguments[0])} != {type(parent)}'
187
188
  fmetadata = metadata or Metadata()
188
189
  return Concept(
189
190
  name=name,
@@ -146,7 +146,7 @@ class ParsePass(Enum):
146
146
  VALIDATION = 2
147
147
 
148
148
 
149
- CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
149
+ CONSTANT_TYPES = (int, float, str, bool, ListWrapper, TupleWrapper, MapWrapper)
150
150
 
151
151
  SELF_LABEL = "root"
152
152
 
@@ -742,6 +742,7 @@ class ParseToObjects(Transformer):
742
742
  lookup, namespace, name, parent = parse_concept_reference(
743
743
  name, self.environment
744
744
  )
745
+
745
746
  concept = Concept(
746
747
  name=name,
747
748
  datatype=arg_to_datatype(constant),