pytrilogy 0.0.3.19__py3-none-any.whl → 0.0.3.21__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
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.19
3
+ Version: 0.0.3.21
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -31,6 +31,7 @@ Dynamic: author-email
31
31
  Dynamic: classifier
32
32
  Dynamic: description
33
33
  Dynamic: description-content-type
34
+ Dynamic: license-file
34
35
  Dynamic: provides-extra
35
36
  Dynamic: requires-dist
36
37
  Dynamic: summary
@@ -44,7 +45,7 @@ pytrilogy is an experimental implementation of the Trilogy language, a higher-le
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
46
 
46
47
  > [!TIP]
47
- > To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
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/).
48
49
 
49
50
  Installation: `pip install pytrilogy`
50
51
 
@@ -1,16 +1,17 @@
1
- trilogy/__init__.py,sha256=KxuTkRMFOlZrKTEJ94OxeHN92vhWtBys2MRzNG8uGBU,303
1
+ pytrilogy-0.0.3.21.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=n0fAEm3rOxsyfVJboGyrvMZZfBz-GUzKvCTTS1z9qwE,303
2
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
4
+ trilogy/constants.py,sha256=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
4
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
5
- trilogy/executor.py,sha256=CU-T7hl5hQab17KkJz9XhwlyI4-7MQL-JGdTDMVsE4E,16025
6
+ trilogy/executor.py,sha256=_xihzIaUEbE5lzwHECsvQ75Dm5fdRPBdMCVz6gNBpV4,16091
6
7
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
7
8
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- trilogy/render.py,sha256=D6rI1RNtn0StJeSe4e18lnlc-U--cNu4lh5C_NkU_uM,1218
9
+ trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
9
10
  trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
10
11
  trilogy/authoring/__init__.py,sha256=ohkYA3_LGYZh3fwzEYKTN6ofACDI5GYl3VCbGxVvlzY,2233
11
12
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
13
- trilogy/core/enums.py,sha256=ND69oja7DOsZS2T8JlIuDW2-uKm74x9SJOWbAqNeopU,7137
14
+ trilogy/core/enums.py,sha256=wuW667WD3mhZnXmN2VXzohseHpdlmzrfLvPtQJNdhdw,7165
14
15
  trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
15
16
  trilogy/core/environment_helpers.py,sha256=oOpewPwMp8xOtx2ayeeyuLNUwr-cli7UanHKot5ebNY,7627
16
17
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
@@ -27,7 +28,7 @@ trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g6
27
28
  trilogy/core/models/core.py,sha256=nb4h1HHm5_qwmUkYth4zRhEttS1EtsMZCP4vT20EEAE,10326
28
29
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
29
30
  trilogy/core/models/environment.py,sha256=RlHNrRer4p1uSQM030iwGJL82M1hMyY5p8a550XTfUI,26606
30
- trilogy/core/models/execute.py,sha256=kxkw14vgUudHHELXLo73AHiaMEpLfiv0qAvbxjJxn_k,33929
31
+ trilogy/core/models/execute.py,sha256=4jbfwRt6Qv0kNzVU8b_z10Ln0Nk-CDvnbMEP1gCAbck,34204
31
32
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
32
33
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
33
34
  trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52doLU9sIg3pmU2_S08,1332
@@ -64,21 +65,21 @@ trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ
64
65
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
65
66
  trilogy/core/processing/nodes/window_node.py,sha256=STvwheVttxSWVHB-yUQUSo-Pyz7Uk8G1txFDAbWMp-s,1380
66
67
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
- trilogy/core/statements/author.py,sha256=1cJ4nIBInTD3YcS89yTBda2ypghIhMvb-Mut66m_sxM,14615
68
+ trilogy/core/statements/author.py,sha256=sPrItQEKXzE7IR3SGOTVN4OBvGHldUCqXCzbcmEpb7I,14575
68
69
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
70
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
70
71
  trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
71
72
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- trilogy/dialect/base.py,sha256=QnNK5fk8x2WBwOwnDqmWLD6cghLQg7e160G2Wtdrjkg,40338
73
+ trilogy/dialect/base.py,sha256=QxBqbMop8l1eD36RNxSNL8XWd0mbI71FryvW1XK5NuQ,40797
73
74
  trilogy/dialect/bigquery.py,sha256=PkjFcNGZHYOe655PmJhb8a0afdFULuovqP0qQVO8m0I,2953
74
- trilogy/dialect/common.py,sha256=vYb-QPf_CnZ3mMLpOVjteWeLH1iaq2mn4WPx0XGoo20,4033
75
- trilogy/dialect/config.py,sha256=EGYRQIbrkeMuud5Bkds7jSD5dCJR5hEYZUYcy-lYZl4,3308
75
+ trilogy/dialect/common.py,sha256=XjHkP8Dqezjkd2JU5xoAlMRS_6HNyXQCF4CykLK3C8o,5011
76
+ trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
76
77
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
77
78
  trilogy/dialect/duckdb.py,sha256=TepCOhYWYw1oUuOT6ZGlB3l4X6S8rYcldWe3zZm3HoU,3710
78
- trilogy/dialect/enums.py,sha256=QYIcVr5RgpYMA1Wl0nWeojVVxJxy0V2_sn8uqSFNx20,4615
79
+ trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
79
80
  trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
80
81
  trilogy/dialect/presto.py,sha256=Mw7_F8h19mWfuZHkHQJizQWbpu1lIHe6t2PA0r88gsY,3392
81
- trilogy/dialect/snowflake.py,sha256=wmao9p26jX5yIX5SC8sRAZTXkPGTvq6ixO693QTfhz8,2989
82
+ trilogy/dialect/snowflake.py,sha256=vc0374Og0O5OIB7-Z7jbwoJJg0iomjvnUqHlxM8B0rg,3120
82
83
  trilogy/dialect/sql_server.py,sha256=z2Vg7Qvw83rbGiEFIvHHLqVWJTWiz2xs76kpQj4HdTU,3131
83
84
  trilogy/hooks/__init__.py,sha256=T3SF3phuUDPLXKGRVE_Lf9mzuwoXWyaLolncR_1kY30,144
84
85
  trilogy/hooks/base_hook.py,sha256=I_l-NBMNC7hKTDx1JgHZPVOOCvLQ36m2oIGaR5EUMXY,1180
@@ -90,14 +91,13 @@ trilogy/parsing/common.py,sha256=99tDKrpQTk-SpyTXUqKFtm-lfmhjCOQIn25hxbQvRRg,214
90
91
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
91
92
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
92
93
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
93
- trilogy/parsing/parse_engine.py,sha256=AAzekSp9tcpI3lbfDoghB6zi7FkjLZIbZRSD9GLbDmU,59605
94
+ trilogy/parsing/parse_engine.py,sha256=UiTrjU6Lpp25S-wly_pNS0wUYEsZpjHExxduArtE1vQ,60602
94
95
  trilogy/parsing/render.py,sha256=o_XuQWhcwx1lD9eGVqkqZEwkmQK0HdmWWokGBtdeH4I,17837
95
96
  trilogy/parsing/trilogy.lark,sha256=7libFS5HNiyHJYzr5_lEiY-Lpqit04_PgyIPHMZT7-w,12928
96
97
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
98
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
98
- pytrilogy-0.0.3.19.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
99
- pytrilogy-0.0.3.19.dist-info/METADATA,sha256=Gm_Cjipo1VE5HncqCyT3MNW3YLoLDAmV0P7pScqO_Ro,8984
100
- pytrilogy-0.0.3.19.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
101
- pytrilogy-0.0.3.19.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
102
- pytrilogy-0.0.3.19.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
103
- pytrilogy-0.0.3.19.dist-info/RECORD,,
99
+ pytrilogy-0.0.3.21.dist-info/METADATA,sha256=JgrwJO6p9VrP93Ccj8XjYglOZ6VfWKD3DtuXENQxGT0,9100
100
+ pytrilogy-0.0.3.21.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
101
+ pytrilogy-0.0.3.21.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
102
+ pytrilogy-0.0.3.21.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
103
+ pytrilogy-0.0.3.21.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (77.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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.19"
7
+ __version__ = "0.0.3.21"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -47,6 +47,14 @@ class Rendering:
47
47
  concise: bool = False
48
48
 
49
49
 
50
+ @dataclass
51
+ class Parsing:
52
+ """Control Parsing"""
53
+
54
+ strict_name_shadow_enforcement: bool = False
55
+ select_as_definition: bool = True
56
+
57
+
50
58
  # TODO: support loading from environments
51
59
  @dataclass
52
60
  class Config:
@@ -57,7 +65,7 @@ class Config:
57
65
  comments: Comments = field(default_factory=Comments)
58
66
  optimizations: Optimizations = field(default_factory=Optimizations)
59
67
  rendering: Rendering = field(default_factory=Rendering)
60
- select_as_definition: bool = True
68
+ parsing: Parsing = field(default_factory=Parsing)
61
69
 
62
70
  @property
63
71
  def show_comments(self) -> bool:
trilogy/core/enums.py CHANGED
@@ -8,6 +8,7 @@ class UnnestMode(Enum):
8
8
  CROSS_APPLY = "cross_apply"
9
9
  CROSS_JOIN = "cross_join"
10
10
  CROSS_JOIN_ALIAS = "cross_join_alias"
11
+ SNOWFLAKE = "snowflake"
11
12
 
12
13
 
13
14
  class ConceptSource(Enum):
@@ -59,6 +59,14 @@ class CTE(BaseModel):
59
59
  base_name_override: Optional[str] = None
60
60
  base_alias_override: Optional[str] = None
61
61
 
62
+ @field_validator("join_derived_concepts")
63
+ def validate_join_derived_concepts(cls, v):
64
+ if len(v) > 1:
65
+ raise NotImplementedError(
66
+ "Multiple join derived concepts not yet supported."
67
+ )
68
+ return unique(v, "address")
69
+
62
70
  @property
63
71
  def identifier(self):
64
72
  return self.name
@@ -142,7 +142,7 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
142
142
  if isinstance(x.content.output, UndefinedConcept):
143
143
  continue
144
144
  if (
145
- CONFIG.select_as_definition
145
+ CONFIG.parsing.select_as_definition
146
146
  and not environment.frozen
147
147
  and x.concept.address not in environment.concepts
148
148
  ):
@@ -156,7 +156,7 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
156
156
  elif isinstance(x.content, ConceptRef):
157
157
  output.local_concepts[x.content.address] = environment.concepts[
158
158
  x.content.address
159
- ] # .set_select_grain(output.grain, environment)
159
+ ]
160
160
  output.validate_syntax(environment)
161
161
  return output
162
162
 
trilogy/dialect/base.py CHANGED
@@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
3
3
 
4
4
  from jinja2 import Template
5
5
 
6
- from trilogy.constants import CONFIG, MagicConstants, logger
6
+ from trilogy.constants import CONFIG, MagicConstants, Rendering, logger
7
7
  from trilogy.core.enums import (
8
8
  DatePart,
9
9
  FunctionType,
@@ -283,6 +283,9 @@ class BaseDialect:
283
283
  DATATYPE_MAP = DATATYPE_MAP
284
284
  UNNEST_MODE = UnnestMode.CROSS_APPLY
285
285
 
286
+ def __init__(self, rendering: Rendering | None = None):
287
+ self.rendering = rendering or CONFIG.rendering
288
+
286
289
  def render_order_item(
287
290
  self,
288
291
  order_item: BuildOrderItem,
@@ -416,7 +419,7 @@ class BaseDialect:
416
419
  elif (
417
420
  isinstance(c.lineage, FUNCTION_ITEMS)
418
421
  and c.lineage.operator == FunctionType.CONSTANT
419
- and CONFIG.rendering.parameters is True
422
+ and self.rendering.parameters is True
420
423
  and c.datatype.data_type != DataType.MAP
421
424
  ):
422
425
  rval = f":{c.safe_address}"
@@ -633,7 +636,7 @@ class BaseDialect:
633
636
  if (
634
637
  isinstance(e.lineage, FUNCTION_ITEMS)
635
638
  and e.lineage.operator == FunctionType.CONSTANT
636
- and CONFIG.rendering.parameters is True
639
+ and self.rendering.parameters is True
637
640
  and e.datatype.data_type != DataType.MAP
638
641
  ):
639
642
  return f":{e.safe_address}"
@@ -694,6 +697,7 @@ class BaseDialect:
694
697
  UnnestMode.CROSS_APPLY,
695
698
  UnnestMode.CROSS_JOIN,
696
699
  UnnestMode.CROSS_JOIN_ALIAS,
700
+ UnnestMode.SNOWFLAKE,
697
701
  ):
698
702
  # for a cross apply, derivation happens in the join
699
703
  # so we only use the alias to select
@@ -723,6 +727,12 @@ class BaseDialect:
723
727
  UnnestMode.CROSS_JOIN,
724
728
  UnnestMode.CROSS_APPLY,
725
729
  ):
730
+
731
+ source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_concept_sql, cte)}"
732
+ elif (
733
+ cte.join_derived_concepts
734
+ and self.UNNEST_MODE == UnnestMode.SNOWFLAKE
735
+ ):
726
736
  source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_concept_sql, cte)}"
727
737
  # direct - eg DUCK DB - can be directly selected inline
728
738
  elif (
trilogy/dialect/common.py CHANGED
@@ -25,7 +25,15 @@ def render_unnest(
25
25
  ):
26
26
  if unnest_mode == UnnestMode.CROSS_JOIN:
27
27
  return f"{render_func(concept, cte, False)} as {quote_character}{concept.safe_address}{quote_character}"
28
- return f"{render_func(concept, cte, False)} as unnest_wrapper ({quote_character}{concept.safe_address}{quote_character})"
28
+ elif unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
29
+ return f"{render_func(concept, cte, False)} as unnest_wrapper ({quote_character}{concept.safe_address}{quote_character})"
30
+ elif unnest_mode == UnnestMode.SNOWFLAKE:
31
+ # if we don't actually have a join, we're directly unnesting a concept, and we can skip the flatten
32
+ if not cte.render_from_clause:
33
+ return f"{render_func(concept, cte, False)} as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
34
+ # otherwise, flatten the concept for the join
35
+ return f"flatten({render_func(concept, cte, False)}) as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
36
+ return f"{render_func(concept, cte, False)} as {quote_character}{concept.safe_address}{quote_character}"
29
37
 
30
38
 
31
39
  def render_join_concept(
@@ -67,6 +75,8 @@ def render_join(
67
75
  return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
68
76
  if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
69
77
  return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
78
+ if unnest_mode == UnnestMode.SNOWFLAKE:
79
+ return f"LEFT JOIN LATERAL {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
70
80
  return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
71
81
  # left_name = join.left_name
72
82
  right_name = join.right_name
trilogy/dialect/config.py CHANGED
@@ -76,12 +76,22 @@ class SnowflakeConfig(DialectConfig):
76
76
  account: str,
77
77
  username: str,
78
78
  password: str,
79
+ database: str | None = None,
80
+ schema: str | None = None,
79
81
  ):
80
82
  self.account = account
81
83
  self.username = username
82
84
  self.password = password
85
+ self.database = database
86
+ self.schema = schema
87
+ if self.schema and not self.database:
88
+ raise ValueError("Setting snowflake schema also requires setting database")
83
89
 
84
90
  def connection_string(self) -> str:
91
+ if self.schema:
92
+ return f"snowflake://{self.username}:{self.password}@{self.account}/{self.database}/{self.schema}"
93
+ if self.database:
94
+ return f"snowflake://{self.username}:{self.password}@{self.account}/{self.database}"
85
95
  return f"snowflake://{self.username}:{self.password}@{self.account}"
86
96
 
87
97
 
trilogy/dialect/enums.py CHANGED
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
7
7
  from trilogy import Executor
8
8
  from trilogy.hooks.base_hook import BaseHook
9
9
 
10
- from trilogy.constants import logger
10
+ from trilogy.constants import Rendering, logger
11
11
  from trilogy.dialect.config import DialectConfig
12
12
 
13
13
 
@@ -114,6 +114,7 @@ class Dialects(Enum):
114
114
  environment: Optional["Environment"] = None,
115
115
  hooks: List["BaseHook"] | None = None,
116
116
  conf: DialectConfig | None = None,
117
+ rendering: Rendering | None = None,
117
118
  _engine_factory: Callable | None = None,
118
119
  ) -> "Executor":
119
120
  from trilogy import Executor
@@ -123,6 +124,7 @@ class Dialects(Enum):
123
124
  engine=self.default_engine(conf=conf, _engine_factory=_engine_factory),
124
125
  environment=environment or Environment(),
125
126
  dialect=self,
127
+ rendering=rendering,
126
128
  hooks=hooks,
127
129
  )
128
130
 
@@ -130,5 +132,6 @@ class Dialects(Enum):
130
132
  engine=self.default_engine(conf=conf),
131
133
  environment=environment or Environment(),
132
134
  dialect=self,
135
+ rendering=rendering,
133
136
  hooks=hooks,
134
137
  )
@@ -30,6 +30,8 @@ FUNCTION_MAP = {
30
30
  FunctionType.QUARTER: lambda x: f"EXTRACT(QUARTER from {x[0]})",
31
31
  # math
32
32
  FunctionType.DIVIDE: lambda x: f"DIV0({x[0]},{x[1]})",
33
+ FunctionType.UNNEST: lambda x: f"table(flatten({x[0]}))",
34
+ FunctionType.ARRAY: lambda x: f"ARRAY_CONSTRUCT({', '.join(x)})",
33
35
  }
34
36
 
35
37
  FUNCTION_GRAIN_MATCH_MAP = {
@@ -83,4 +85,4 @@ class SnowflakeDialect(BaseDialect):
83
85
  }
84
86
  QUOTE_CHARACTER = '"'
85
87
  SQL_TEMPLATE = BQ_SQL_TEMPLATE
86
- UNNEST_MODE = UnnestMode.CROSS_JOIN
88
+ UNNEST_MODE = UnnestMode.SNOWFLAKE
trilogy/executor.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, Generator, List, Optional, Protocol
6
6
  from sqlalchemy import text
7
7
  from sqlalchemy.engine import CursorResult
8
8
 
9
- from trilogy.constants import logger
9
+ from trilogy.constants import Rendering, logger
10
10
  from trilogy.core.enums import FunctionType, Granularity, IOType
11
11
  from trilogy.core.models.author import Concept, Function
12
12
  from trilogy.core.models.build import BuildConcept, BuildFunction
@@ -75,6 +75,7 @@ class Executor(object):
75
75
  dialect: Dialects,
76
76
  engine: ExecutionEngine,
77
77
  environment: Optional[Environment] = None,
78
+ rendering: Rendering | None = None,
78
79
  hooks: List[BaseHook] | None = None,
79
80
  ):
80
81
  self.dialect: Dialects = dialect
@@ -83,7 +84,7 @@ class Executor(object):
83
84
  self.generator: BaseDialect
84
85
  self.logger = logger
85
86
  self.hooks = hooks
86
- self.generator = get_dialect_generator(self.dialect)
87
+ self.generator = get_dialect_generator(self.dialect, rendering)
87
88
  self.connection = self.engine.connect()
88
89
  # TODO: make generic
89
90
  if self.dialect == Dialects.DATAFRAME:
@@ -18,9 +18,11 @@ from lark.tree import Meta
18
18
  from pydantic import ValidationError
19
19
 
20
20
  from trilogy.constants import (
21
+ CONFIG,
21
22
  DEFAULT_NAMESPACE,
22
23
  NULL_VALUE,
23
24
  MagicConstants,
25
+ Parsing,
24
26
  )
25
27
  from trilogy.core.enums import (
26
28
  BooleanOperator,
@@ -282,6 +284,7 @@ class ParseToObjects(Transformer):
282
284
  text_lookup: dict[Path | str, str] | None = None,
283
285
  environment_lookup: dict[str, Environment] | None = None,
284
286
  import_keys: list[str] | None = None,
287
+ parse_config: Parsing | None = None,
285
288
  ):
286
289
  Transformer.__init__(self, True)
287
290
  self.environment: Environment = environment
@@ -298,6 +301,7 @@ class ParseToObjects(Transformer):
298
301
  self.parse_pass = ParsePass.INITIAL
299
302
  self.function_factory = FunctionFactory(self.environment)
300
303
  self.import_keys: list[str] = import_keys or ["root"]
304
+ self.parse_config: Parsing = parse_config or CONFIG.parsing
301
305
 
302
306
  def set_text(self, text: str):
303
307
  self.text_lookup[self.token_address] = text
@@ -983,7 +987,12 @@ class ParseToObjects(Transformer):
983
987
  text = self.resolve_import_address(target)
984
988
  self.text_lookup[token_lookup] = text
985
989
 
986
- raw_tokens = PARSER.parse(text)
990
+ try:
991
+ raw_tokens = PARSER.parse(text)
992
+ except Exception as e:
993
+ raise ImportError(
994
+ f"Unable to import '{target}', parsing error: {e}"
995
+ ) from e
987
996
  self.tokens[token_lookup] = raw_tokens
988
997
 
989
998
  if cache_lookup in self.parsed:
@@ -1009,6 +1018,7 @@ class ParseToObjects(Transformer):
1009
1018
  tokens=self.tokens,
1010
1019
  text_lookup=self.text_lookup,
1011
1020
  import_keys=self.import_keys + [cache_key],
1021
+ parse_config=self.parse_config,
1012
1022
  )
1013
1023
  nparser.transform(raw_tokens)
1014
1024
  self.parsed[cache_lookup] = nparser
@@ -1143,8 +1153,8 @@ class ParseToObjects(Transformer):
1143
1153
  elif isinstance(arg, HavingClause):
1144
1154
  having = arg
1145
1155
  if not select_items:
1146
- raise ValueError("Malformed select, missing select items")
1147
-
1156
+ raise ParseError("Malformed select, missing select items")
1157
+ pre_keys = set(self.environment.concepts.keys())
1148
1158
  base = SelectStatement.from_inputs(
1149
1159
  environment=self.environment,
1150
1160
  selection=select_items,
@@ -1154,6 +1164,15 @@ class ParseToObjects(Transformer):
1154
1164
  limit=limit,
1155
1165
  meta=Metadata(line_number=meta.line),
1156
1166
  )
1167
+ if (
1168
+ self.parse_pass == ParsePass.INITIAL
1169
+ and self.parse_config.strict_name_shadow_enforcement
1170
+ ):
1171
+ intersection = base.locally_derived.intersection(pre_keys)
1172
+ if intersection:
1173
+ raise ParseError(
1174
+ f"Select statement {base} has derived concepts {list(intersection)} that shadow existing environment concepts, which may cause unexpected behavior. Rename these."
1175
+ )
1157
1176
  return base
1158
1177
 
1159
1178
  @v_args(meta=True)
@@ -1735,7 +1754,10 @@ def parse_text_raw(text: str, environment: Optional[Environment] = None):
1735
1754
 
1736
1755
 
1737
1756
  def parse_text(
1738
- text: str, environment: Optional[Environment] = None, root: Path | None = None
1757
+ text: str,
1758
+ environment: Optional[Environment] = None,
1759
+ root: Path | None = None,
1760
+ parse_config: Parsing | None = None,
1739
1761
  ) -> Tuple[
1740
1762
  Environment,
1741
1763
  List[
@@ -1751,7 +1773,9 @@ def parse_text(
1751
1773
  environment = environment or (
1752
1774
  Environment(working_path=root) if root else Environment()
1753
1775
  )
1754
- parser = ParseToObjects(environment=environment, import_keys=["root"])
1776
+ parser = ParseToObjects(
1777
+ environment=environment, import_keys=["root"], parse_config=parse_config
1778
+ )
1755
1779
 
1756
1780
  try:
1757
1781
  parser.set_text(text)
trilogy/render.py CHANGED
@@ -1,38 +1,42 @@
1
+ from trilogy.constants import Rendering
2
+ from trilogy.dialect.base import BaseDialect
1
3
  from trilogy.dialect.enums import Dialects
2
4
 
3
5
 
4
- def get_dialect_generator(dialect: Dialects):
6
+ def get_dialect_generator(
7
+ dialect: Dialects, rendering: Rendering | None = None
8
+ ) -> BaseDialect:
5
9
  if dialect == Dialects.BIGQUERY:
6
10
  from trilogy.dialect.bigquery import BigqueryDialect
7
11
 
8
- return BigqueryDialect()
12
+ return BigqueryDialect(rendering=rendering)
9
13
  elif dialect == Dialects.SQL_SERVER:
10
14
  from trilogy.dialect.sql_server import SqlServerDialect
11
15
 
12
- return SqlServerDialect()
16
+ return SqlServerDialect(rendering=rendering)
13
17
  elif dialect == Dialects.DUCK_DB:
14
18
  from trilogy.dialect.duckdb import DuckDBDialect
15
19
 
16
- return DuckDBDialect()
20
+ return DuckDBDialect(rendering=rendering)
17
21
  elif dialect == Dialects.PRESTO:
18
22
  from trilogy.dialect.presto import PrestoDialect
19
23
 
20
- return PrestoDialect()
24
+ return PrestoDialect(rendering=rendering)
21
25
  elif dialect == Dialects.TRINO:
22
26
  from trilogy.dialect.presto import TrinoDialect
23
27
 
24
- return TrinoDialect()
28
+ return TrinoDialect(rendering=rendering)
25
29
  elif dialect == Dialects.POSTGRES:
26
30
  from trilogy.dialect.postgres import PostgresDialect
27
31
 
28
- return PostgresDialect()
32
+ return PostgresDialect(rendering=rendering)
29
33
  elif dialect == Dialects.SNOWFLAKE:
30
34
  from trilogy.dialect.snowflake import SnowflakeDialect
31
35
 
32
- return SnowflakeDialect()
36
+ return SnowflakeDialect(rendering=rendering)
33
37
  elif dialect == Dialects.DATAFRAME:
34
38
  from trilogy.dialect.dataframe import DataframeDialect
35
39
 
36
- return DataframeDialect()
40
+ return DataframeDialect(rendering=rendering)
37
41
  else:
38
42
  raise ValueError(f"Unsupported dialect {dialect}")