pytrilogy 0.0.3.112__py3-none-any.whl → 0.0.3.113__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.112
3
+ Version: 0.0.3.113
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Classifier: Programming Language :: Python
6
6
  Classifier: Programming Language :: Python :: 3
@@ -51,61 +51,41 @@ The Trilogy language is an experiment in better SQL for analytics - a streamline
51
51
 
52
52
  Trilogy is especially powerful for data consumption, providing a rich metadata layer that makes creating, interpreting, and visualizing queries easy and expressive.
53
53
 
54
+
55
+ We recommend starting with the studio to explore Trilogy. For 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.
56
+
57
+
54
58
  ## Quick Start
55
59
 
56
60
  > [!TIP]
57
61
  > **Try it now:** [Open-source studio](https://trilogydata.dev/trilogy-studio-core/) | [Interactive demo](https://trilogydata.dev/demo/) | [Documentation](https://trilogydata.dev/)
58
62
 
59
- **Install locally:**
63
+ **Install**
60
64
  ```bash
61
65
  pip install pytrilogy
62
66
  ```
63
67
 
64
- **Your first query:**
68
+ **Save in hello.preql**
65
69
  ```sql
66
- # Save as hello.preql
67
- import names;
68
-
69
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
70
+ const prime <- unnest([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]);
70
71
 
71
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
72
+ def cube_plus_one(x) -> (x * x * x + 1);
72
73
 
73
74
  WHERE
74
- @initcap(name) in top_names
75
+ prime_cubed_plus_one % 7 = 0
75
76
  SELECT
76
- name,
77
- sum(births) as name_count
77
+ prime,
78
+ @cube_plus_one(prime) as prime_cubed_plus_one
78
79
  ORDER BY
79
- name_count desc
80
+ prime asc
80
81
  LIMIT 10;
81
82
  ```
82
83
 
83
- **Run it:**
84
+ **Run it in DuckDB**
84
85
  ```bash
85
86
  trilogy run hello.preql duckdb
86
87
  ```
87
88
 
88
- We recommend starting with the studio to explore Trilogy. For 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.
89
-
90
- ## Trilogy Looks Like SQL
91
-
92
- ```sql
93
- import names;
94
-
95
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
96
-
97
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
98
-
99
- WHERE
100
- @initcap(name) in top_names
101
- SELECT
102
- name,
103
- sum(births) as name_count
104
- ORDER BY
105
- name_count desc
106
- LIMIT 10;
107
- ```
108
-
109
89
  ## Trilogy is Easy to Write
110
90
  For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
111
91
 
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.112.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=KpC5q9qz6KQi0DSThY-iVF-bEm7ue6IBVE2nlKbQJHk,304
1
+ pytrilogy-0.0.3.113.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=lAuWMqhwvK53mopDwiFjvqXcgCmF97vD3MVcY8Npwq4,304
3
3
  trilogy/constants.py,sha256=g_zkVCNjGop6coZ1kM8eXXAzCnUN22ldx3TYFz0E9sc,1747
4
4
  trilogy/engine.py,sha256=v4TpNktM4zZ9OX7jZH2nde4dpX5uAH2U23ELfULTCSg,2280
5
5
  trilogy/executor.py,sha256=q3EsAjzgxNxPn-yTHD_FTFzm7bJ2mlf9CrJEjyt6-pE,17884
@@ -23,24 +23,24 @@ trilogy/ai/providers/utils.py,sha256=yttP6y2E_XzdytBCwhaKekfXfxM6gE6MRce4AtyLL60
23
23
  trilogy/authoring/__init__.py,sha256=TABMOETSMERrWuyDLR0nK4ISlqR0yaqeXrmuOdrSvAY,3060
24
24
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  trilogy/core/constants.py,sha256=nizWYDCJQ1bigQMtkNIEMNTcN0NoEAXiIHLzpelxQ24,201
26
- trilogy/core/enums.py,sha256=H8I2Dz4POHZ4ixYCGzNs4c3KDqxLQklGLVfmje1DSMo,8877
26
+ trilogy/core/enums.py,sha256=B0EbyjIBFl5SaP0J3lHIqudgy16fXCf9rlr16yxk6kk,8933
27
27
  trilogy/core/env_processor.py,sha256=H-rr2ALj31l5oh3FqeI47Qju6OOfiXBacXNJGNZ92zQ,4521
28
28
  trilogy/core/environment_helpers.py,sha256=TRlqVctqIRBxzfjRBmpQsAVoiCcsEKBhG1B6PUE0l1M,12743
29
29
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
30
30
  trilogy/core/exceptions.py,sha256=axkVXYJYQXCCwMHwlyDA232g4tCOwdCZUt7eHeUMDMg,2829
31
- trilogy/core/functions.py,sha256=sdV6Z3NUVfwL1d18eNcaAXllVNqzLez23McsJ6xIp7M,33182
31
+ trilogy/core/functions.py,sha256=966xC6ypgzWwCs4BvORFwzWEMJoVlqvH2biipkIYl4E,34005
32
32
  trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
33
33
  trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
34
34
  trilogy/core/optimization.py,sha256=Km0ITEx9n6Iv5ReX6tm4uXO5uniSv_ooahycNNiET3g,9212
35
35
  trilogy/core/query_processor.py,sha256=rMrtLSQxVm7yeyh0nWjDNI9nnu4Xi0NgHvBJ14gvu4I,20384
36
36
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
37
37
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- trilogy/core/models/author.py,sha256=3I7PFpJgoQT9RPOT3DfiqAjEtkcQPJnScs60I2UoyWo,81461
38
+ trilogy/core/models/author.py,sha256=YXom_XC-wdcZmJ2PexWCrJ2Slr3FnrxA__X4ayc_A8w,81792
39
39
  trilogy/core/models/build.py,sha256=iqk_-3plxX1BdxvUCTebqE9F3x62f40neKGf6Ld4VVU,70858
40
40
  trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYycCrP4zF-rHU,5258
41
41
  trilogy/core/models/core.py,sha256=iT9WdZoiXeglmUHWn6bZyXCTBpkApTGPKtNm_Mhbu_g,12987
42
42
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
43
- trilogy/core/models/environment.py,sha256=hwTIRnJgaHUdCYof7U5A9NPitGZ2s9yxqiW5O2SaJ9Y,28759
43
+ trilogy/core/models/environment.py,sha256=do1Xvr9oyjDj0knAxgIqexSbt0HMrHbLJNyl9utkSvs,28760
44
44
  trilogy/core/models/execute.py,sha256=3fgEdho2e7S0outq91cCzb9jFwz6L1hTbsTrJwGvIFs,42311
45
45
  trilogy/core/optimizations/__init__.py,sha256=yspWc25M5SgAuvXYoSt5J8atyPbDlOfsKjIo5yGD9s4,368
46
46
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
@@ -84,7 +84,7 @@ trilogy/core/processing/nodes/union_node.py,sha256=hLAXXVWqEgMWi7dlgSHfCF59fon64
84
84
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
85
85
  trilogy/core/processing/nodes/window_node.py,sha256=JXJ0iVRlSEM2IBr1TANym2RaUf_p5E_l2sNykRzXWDo,1710
86
86
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- trilogy/core/statements/author.py,sha256=VFzylve72fw0tqMSP5Yiwp8--_r92b9zzX1XAdxuTYQ,15963
87
+ trilogy/core/statements/author.py,sha256=w_d55It4zs8qBMyEGxRyUjgMX0U3AQojG_GJ587UgrM,16414
88
88
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  trilogy/core/statements/common.py,sha256=VnVLULQg1TJLNUFzJaROT1tsf2ewk3IpuhvZaP36R6A,535
90
90
  trilogy/core/statements/execute.py,sha256=kiwJcVeMa4wZR-xLfM2oYOJ9DeyJkP8An38WFyJxktM,2413
@@ -95,12 +95,12 @@ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGx
95
95
  trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
96
96
  trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
97
97
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
- trilogy/dialect/base.py,sha256=Qk4HkjKlnAnhcZwwLte9Arb_1pVnBmkgRlwRFX1A_GQ,50680
98
+ trilogy/dialect/base.py,sha256=B_zjQ6HyOQEW0iRGgGBjhqJ1Xr-KODk1fUBZDAWfE54,50798
99
99
  trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
100
100
  trilogy/dialect/common.py,sha256=I5Ku_TR5MwJTB3ZhcQenrtvXhH2RvTQ8wQe9w5lfkfA,5708
101
101
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
102
102
  trilogy/dialect/dataframe.py,sha256=nDTHMSd7GiGjEhjAobrZND0w4zjr-vgOalM2Cdxjets,1596
103
- trilogy/dialect/duckdb.py,sha256=cRPyqnuMgjhZVaW9BYA360p-5OXle_1Xt65Yy0Vzbr4,5901
103
+ trilogy/dialect/duckdb.py,sha256=Z_mxrfQXS3KP8PEbkKsCLolyjJgPEM-aBkVCdWiLph0,6312
104
104
  trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
105
105
  trilogy/dialect/metadata.py,sha256=p_V-MYPQ2iR6fcTjagnptCIWtsZe4fTfoS_iXpavPzY,7098
106
106
  trilogy/dialect/postgres.py,sha256=el2PKwfyvWGk5EZtLudqAH5ewLitY1sFHJiocBSyxyM,3393
@@ -113,13 +113,13 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
113
113
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
114
114
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
115
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- trilogy/parsing/common.py,sha256=NJLm31J3W9BLWq1ClhNvYE43jrF950698KJ3o0UfSCo,31340
116
+ trilogy/parsing/common.py,sha256=alayr5yy6MZOLTnw8CKLtxU2drtzbq4bKteQWfTw9QU,32281
117
117
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
118
118
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
119
119
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
120
- trilogy/parsing/parse_engine.py,sha256=T-3Q4UH256IB6cfX85crScZwZ6gAwslgv0fy3WKBdjc,81930
120
+ trilogy/parsing/parse_engine.py,sha256=p758ukVifI_ygWp-1vJIy3X5NZVmwpNbbxVDfbkkTbU,82253
121
121
  trilogy/parsing/render.py,sha256=FhSU3-bMA0YM3oEn6nfpfjbM74nvH2r1TtFgbWNzOsM,24204
122
- trilogy/parsing/trilogy.lark,sha256=6eBDD6d4D9N1Nnn4CtmaoB-NpOpjHrEn5oi0JykAlbE,16509
122
+ trilogy/parsing/trilogy.lark,sha256=EN0Nrwz8cagzt69O85VSteW-k30lj8U5bRtXetM0JiU,16671
123
123
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
124
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
125
125
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -132,8 +132,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
132
132
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
133
133
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
134
134
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
135
- pytrilogy-0.0.3.112.dist-info/METADATA,sha256=vUL4xdKsovl8VHq7P_NyBTDiI31R0E2EkcJYOog7A8Q,13289
136
- pytrilogy-0.0.3.112.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
- pytrilogy-0.0.3.112.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
- pytrilogy-0.0.3.112.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
- pytrilogy-0.0.3.112.dist-info/RECORD,,
135
+ pytrilogy-0.0.3.113.dist-info/METADATA,sha256=4Ix5np1ZL_PCT0B6i4e8mazXdFNvRkpHznssca1beeI,12911
136
+ pytrilogy-0.0.3.113.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
+ pytrilogy-0.0.3.113.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
+ pytrilogy-0.0.3.113.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
+ pytrilogy-0.0.3.113.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.112"
7
+ __version__ = "0.0.3.113"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -236,6 +236,8 @@ class FunctionType(Enum):
236
236
  MONTH = "month"
237
237
  QUARTER = "quarter"
238
238
  YEAR = "year"
239
+ MONTH_NAME = "month_name"
240
+ DAY_NAME = "day_name"
239
241
 
240
242
  DATE_PART = "date_part"
241
243
  DATE_TRUNCATE = "date_truncate"
trilogy/core/functions.py CHANGED
@@ -175,6 +175,10 @@ def get_date_trunc_output(
175
175
  return DataType.DATETIME
176
176
  elif target == DatePart.SECOND:
177
177
  return DataType.DATETIME
178
+ elif target == DatePart.WEEK:
179
+ return DataType.DATE
180
+ elif target == DatePart.QUARTER:
181
+ return DataType.DATE
178
182
  else:
179
183
  raise InvalidSyntaxException(f"Date truncation not supported for {target}")
180
184
 
@@ -640,6 +644,17 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
640
644
  output_type=TraitDataType(type=DataType.INTEGER, traits=["day"]),
641
645
  arg_count=1,
642
646
  ),
647
+ FunctionType.DAY_NAME: FunctionConfig(
648
+ valid_inputs={
649
+ DataType.DATE,
650
+ DataType.TIMESTAMP,
651
+ DataType.DATETIME,
652
+ # DataType.STRING,
653
+ },
654
+ output_purpose=Purpose.PROPERTY,
655
+ output_type=TraitDataType(type=DataType.STRING, traits=["day_name"]),
656
+ arg_count=1,
657
+ ),
643
658
  FunctionType.WEEK: FunctionConfig(
644
659
  valid_inputs={
645
660
  DataType.DATE,
@@ -662,6 +677,17 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
662
677
  output_type=TraitDataType(type=DataType.INTEGER, traits=["month"]),
663
678
  arg_count=1,
664
679
  ),
680
+ FunctionType.MONTH_NAME: FunctionConfig(
681
+ valid_inputs={
682
+ DataType.DATE,
683
+ DataType.TIMESTAMP,
684
+ DataType.DATETIME,
685
+ # DataType.STRING,
686
+ },
687
+ output_purpose=Purpose.PROPERTY,
688
+ output_type=TraitDataType(type=DataType.STRING, traits=["month_name"]),
689
+ arg_count=1,
690
+ ),
665
691
  FunctionType.QUARTER: FunctionConfig(
666
692
  valid_inputs={
667
693
  DataType.DATE,
@@ -461,6 +461,11 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, BaseModel):
461
461
  conditional=self.conditional.with_namespace(namespace)
462
462
  )
463
463
 
464
+ def with_reference_replacement(self, source, target):
465
+ return self.__class__.model_construct(
466
+ conditional=self.conditional.with_reference_replacement(source, target)
467
+ )
468
+
464
469
 
465
470
  class HavingClause(WhereClause):
466
471
  pass
@@ -1212,6 +1217,8 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1212
1217
  return Derivation.FILTER
1213
1218
  elif lineage and isinstance(lineage, (BuildAggregateWrapper, AggregateWrapper)):
1214
1219
  return Derivation.AGGREGATE
1220
+ # elif lineage and isinstance(lineage, (BuildParenthetical, Parenthetical)):
1221
+ # return Derivation.PARENTHETICAL
1215
1222
  elif lineage and isinstance(lineage, (BuildRowsetItem, RowsetItem)):
1216
1223
  return Derivation.ROWSET
1217
1224
  elif lineage and isinstance(
@@ -563,6 +563,7 @@ class Environment(BaseModel):
563
563
  existing = self.validate_concept(concept, meta=meta)
564
564
  if existing:
565
565
  concept = existing
566
+
566
567
  self.concepts[concept.address] = concept
567
568
 
568
569
  from trilogy.core.environment_helpers import generate_related_concepts
@@ -31,6 +31,7 @@ from trilogy.core.models.author import (
31
31
  Metadata,
32
32
  MultiSelectLineage,
33
33
  OrderBy,
34
+ Parenthetical,
34
35
  SelectLineage,
35
36
  UndefinedConcept,
36
37
  WhereClause,
@@ -48,7 +49,12 @@ from trilogy.utility import unique
48
49
 
49
50
  class ConceptTransform(BaseModel):
50
51
  function: (
51
- Function | FilterItem | WindowItem | AggregateWrapper | FunctionCallWrapper
52
+ Function
53
+ | FilterItem
54
+ | WindowItem
55
+ | AggregateWrapper
56
+ | FunctionCallWrapper
57
+ | Parenthetical
52
58
  )
53
59
  output: Concept # this has to be a full concept, as it may not exist in environment
54
60
  modifiers: List[Modifier] = Field(default_factory=list)
@@ -190,9 +196,17 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
190
196
  if self.where_clause:
191
197
  for x in self.where_clause.concept_arguments:
192
198
  if isinstance(x, UndefinedConcept):
193
- environment.concepts.raise_undefined(
194
- x.address, x.metadata.line_number if x.metadata else None
195
- )
199
+ validate = environment.concepts.get(x.address)
200
+ if validate:
201
+ self.where_clause = (
202
+ self.where_clause.with_reference_replacement(
203
+ x.address, validate.reference
204
+ )
205
+ )
206
+ else:
207
+ environment.concepts.raise_undefined(
208
+ x.address, x.metadata.line_number if x.metadata else None
209
+ )
196
210
  all_in_output = [x for x in self.output_components]
197
211
  if self.where_clause:
198
212
  for cref in self.where_clause.concept_arguments:
trilogy/dialect/base.py CHANGED
@@ -263,9 +263,11 @@ FUNCTION_MAP = {
263
263
  FunctionType.MINUTE: lambda x: f"minute({x[0]})",
264
264
  FunctionType.HOUR: lambda x: f"hour({x[0]})",
265
265
  FunctionType.DAY: lambda x: f"day({x[0]})",
266
+ FunctionType.DAY_NAME: lambda x: f"dayname({x[0]})",
266
267
  FunctionType.DAY_OF_WEEK: lambda x: f"day_of_week({x[0]})",
267
268
  FunctionType.WEEK: lambda x: f"week({x[0]})",
268
269
  FunctionType.MONTH: lambda x: f"month({x[0]})",
270
+ FunctionType.MONTH_NAME: lambda x: f"monthname({x[0]})",
269
271
  FunctionType.QUARTER: lambda x: f"quarter({x[0]})",
270
272
  FunctionType.YEAR: lambda x: f"year({x[0]})",
271
273
  # string types
trilogy/dialect/duckdb.py CHANGED
@@ -57,6 +57,15 @@ def render_log(args):
57
57
  raise ValueError("log function requires 1 or 2 arguments")
58
58
 
59
59
 
60
+ def map_date_part_specifier(specifier: str) -> str:
61
+ """Map date part specifiers to DuckDB-compatible names"""
62
+ mapping = {
63
+ "day_of_week": "dow",
64
+ # Add other mappings if needed
65
+ }
66
+ return mapping.get(specifier, specifier)
67
+
68
+
60
69
  FUNCTION_MAP = {
61
70
  FunctionType.COUNT: lambda args: f"count({args[0]})",
62
71
  FunctionType.SUM: lambda args: f"sum({args[0]})",
@@ -84,11 +93,13 @@ FUNCTION_MAP = {
84
93
  FunctionType.ARRAY_AGG: lambda args: f"array_agg({args[0]})",
85
94
  # datetime is aliased
86
95
  FunctionType.CURRENT_DATETIME: lambda x: "cast(get_current_timestamp() as datetime)",
96
+ FunctionType.DATETIME: lambda x: f"cast({x[0]} as datetime)",
97
+ FunctionType.TIMESTAMP: lambda x: f"cast({x[0]} as timestamp)",
87
98
  FunctionType.DATE: lambda x: f"cast({x[0]} as date)",
88
99
  FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc('{x[1]}', {x[0]})",
89
100
  FunctionType.DATE_ADD: lambda x: f"date_add({x[0]}, {x[2]} * INTERVAL 1 {x[1]})",
90
101
  FunctionType.DATE_SUB: lambda x: f"date_add({x[0]}, -{x[2]} * INTERVAL 1 {x[1]})",
91
- FunctionType.DATE_PART: lambda x: f"date_part('{x[1]}', {x[0]})",
102
+ FunctionType.DATE_PART: lambda x: f"date_part('{map_date_part_specifier(x[1])}', {x[0]})",
92
103
  FunctionType.DATE_DIFF: lambda x: f"date_diff('{x[2]}', {x[0]}, {x[1]})",
93
104
  FunctionType.CONCAT: lambda x: f"({' || '.join(x)})",
94
105
  FunctionType.DATE_LITERAL: lambda x: f"date '{x}'",
trilogy/parsing/common.py CHANGED
@@ -55,6 +55,21 @@ from trilogy.core.models.environment import Environment
55
55
  from trilogy.core.statements.author import RowsetDerivationStatement, SelectStatement
56
56
  from trilogy.utility import string_to_hash, unique
57
57
 
58
+ ARBITRARY_INPUTS = (
59
+ AggregateWrapper
60
+ | FunctionCallWrapper
61
+ | WindowItem
62
+ | FilterItem
63
+ | Function
64
+ | Parenthetical
65
+ | ListWrapper
66
+ | MapWrapper
67
+ | int
68
+ | float
69
+ | str
70
+ | date
71
+ )
72
+
58
73
 
59
74
  def process_function_arg(
60
75
  arg,
@@ -625,7 +640,7 @@ def window_item_to_concept(
625
640
  fmetadata = metadata or Metadata()
626
641
  if not isinstance(parent.content, ConceptRef):
627
642
  raise NotImplementedError(
628
- f"Window function wiht non ref content {parent.content} not yet supported"
643
+ f"Window function with non ref content {parent.content} not yet supported"
629
644
  )
630
645
  bcontent = environment.concepts[parent.content.address]
631
646
  if isinstance(bcontent, UndefinedConcept):
@@ -844,6 +859,7 @@ def generate_concept_name(
844
859
  | Function
845
860
  | ListWrapper
846
861
  | MapWrapper
862
+ | Parenthetical
847
863
  | int
848
864
  | float
849
865
  | str
@@ -865,24 +881,36 @@ def generate_concept_name(
865
881
  return f"{VIRTUAL_CONCEPT_PREFIX}_group_to_{string_to_hash(str(parent))}"
866
882
  else:
867
883
  return f"{VIRTUAL_CONCEPT_PREFIX}_func_{parent.operator.value}_{string_to_hash(str(parent))}"
884
+ elif isinstance(parent, Parenthetical):
885
+ return f"{VIRTUAL_CONCEPT_PREFIX}_paren_{string_to_hash(str(parent))}"
886
+ elif isinstance(parent, FunctionCallWrapper):
887
+ return f"{VIRTUAL_CONCEPT_PREFIX}_{parent.name}_{string_to_hash(str(parent))}"
868
888
  else: # ListWrapper, MapWrapper, or primitive types
869
889
  return f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
870
890
 
871
891
 
892
+ def parenthetical_to_concept(
893
+ parent: Parenthetical,
894
+ name: str,
895
+ namespace: str,
896
+ environment: Environment,
897
+ metadata: Metadata | None = None,
898
+ ) -> Concept:
899
+ if isinstance(
900
+ parent.content,
901
+ ARBITRARY_INPUTS,
902
+ ):
903
+
904
+ return arbitrary_to_concept(
905
+ parent.content, environment, namespace, name, metadata
906
+ )
907
+ raise NotImplementedError(
908
+ f"Parenthetical with non-supported content {parent.content} ({type(parent.content)}) not yet supported"
909
+ )
910
+
911
+
872
912
  def arbitrary_to_concept(
873
- parent: (
874
- AggregateWrapper
875
- | FunctionCallWrapper
876
- | WindowItem
877
- | FilterItem
878
- | Function
879
- | ListWrapper
880
- | MapWrapper
881
- | int
882
- | float
883
- | str
884
- | date
885
- ),
913
+ parent: ARBITRARY_INPUTS,
886
914
  environment: Environment,
887
915
  namespace: str | None = None,
888
916
  name: str | None = None,
@@ -938,5 +966,7 @@ def arbitrary_to_concept(
938
966
  )
939
967
  elif isinstance(parent, ListWrapper):
940
968
  return constant_to_concept(parent, name, namespace, metadata)
969
+ elif isinstance(parent, Parenthetical):
970
+ return parenthetical_to_concept(parent, name, namespace, environment, metadata)
941
971
  else:
942
972
  return constant_to_concept(parent, name, namespace, metadata)
@@ -223,7 +223,14 @@ def expr_to_boolean(
223
223
  def unwrap_transformation(
224
224
  input: Expr,
225
225
  environment: Environment,
226
- ) -> Function | FilterItem | WindowItem | AggregateWrapper | FunctionCallWrapper:
226
+ ) -> (
227
+ Function
228
+ | FilterItem
229
+ | WindowItem
230
+ | AggregateWrapper
231
+ | FunctionCallWrapper
232
+ | Parenthetical
233
+ ):
227
234
  if isinstance(input, Function):
228
235
  return input
229
236
  elif isinstance(input, AggregateWrapper):
@@ -243,7 +250,7 @@ def unwrap_transformation(
243
250
  elif isinstance(input, FunctionCallWrapper):
244
251
  return input
245
252
  elif isinstance(input, Parenthetical):
246
- return unwrap_transformation(input.content, environment)
253
+ return input
247
254
  else:
248
255
  return Function.model_construct(
249
256
  operator=FunctionType.CONSTANT,
@@ -779,7 +786,6 @@ class ParseToObjects(Transformer):
779
786
  lookup, namespace, name, parent = parse_concept_reference(
780
787
  name, self.environment
781
788
  )
782
-
783
789
  concept = Concept(
784
790
  name=name,
785
791
  datatype=arg_to_datatype(constant),
@@ -1953,6 +1959,10 @@ class ParseToObjects(Transformer):
1953
1959
  def fday(self, meta, args):
1954
1960
  return self.function_factory.create_function(args, FunctionType.DAY, meta)
1955
1961
 
1962
+ @v_args(meta=True)
1963
+ def fday_name(self, meta, args):
1964
+ return self.function_factory.create_function(args, FunctionType.DAY_NAME, meta)
1965
+
1956
1966
  @v_args(meta=True)
1957
1967
  def fday_of_week(self, meta, args):
1958
1968
  return self.function_factory.create_function(
@@ -1967,6 +1977,12 @@ class ParseToObjects(Transformer):
1967
1977
  def fmonth(self, meta, args):
1968
1978
  return self.function_factory.create_function(args, FunctionType.MONTH, meta)
1969
1979
 
1980
+ @v_args(meta=True)
1981
+ def fmonth_name(self, meta, args):
1982
+ return self.function_factory.create_function(
1983
+ args, FunctionType.MONTH_NAME, meta
1984
+ )
1985
+
1970
1986
  @v_args(meta=True)
1971
1987
  def fquarter(self, meta, args):
1972
1988
  return self.function_factory.create_function(args, FunctionType.QUARTER, meta)
@@ -379,12 +379,16 @@
379
379
  fhour: _HOUR expr ")"
380
380
  _DAY.1: "day("i
381
381
  fday: _DAY expr ")"
382
+ _DAY_NAME.1: "day_name("i
383
+ fday_name: _DAY_NAME expr ")"
382
384
  _DAY_OF_WEEK.1: "day_of_week("i
383
385
  fday_of_week: _DAY_OF_WEEK expr ")"
384
386
  _WEEK.1: "week("i
385
387
  fweek: _WEEK expr ")"
386
388
  _MONTH.1: "month("i
387
389
  fmonth: _MONTH expr ")"
390
+ _MONTH_NAME.1: "month_name("i
391
+ fmonth_name: _MONTH_NAME expr ")"
388
392
  _QUARTER.1: "quarter("i
389
393
  fquarter: _QUARTER expr ")"
390
394
  _YEAR.1: "year("i
@@ -402,7 +406,7 @@
402
406
  _DATE_DIFF.1: "date_diff("i
403
407
  fdate_diff: _DATE_DIFF expr "," expr "," DATE_PART ")"
404
408
 
405
- _date_functions: fdate | fdate_add | fdate_sub | fdate_diff | fdatetime | ftimestamp | fsecond | fminute | fhour | fday | fday_of_week | fweek | fmonth | fquarter | fyear | fdate_part | fdate_trunc
409
+ _date_functions: fdate | fdate_add | fdate_sub | fdate_diff | fdatetime | ftimestamp | fsecond | fminute | fhour | fday |fday_name | fday_of_week | fweek | fmonth | fmonth_name | fquarter | fyear | fdate_part | fdate_trunc
406
410
 
407
411
  _static_functions: _string_functions | _math_functions | _array_functions | _map_functions
408
412