pytrilogy 0.0.3.9__py3-none-any.whl → 0.0.3.11__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.2
2
2
  Name: pytrilogy
3
- Version: 0.0.3.9
3
+ Version: 0.0.3.11
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,31 +1,32 @@
1
- trilogy/__init__.py,sha256=oZcEJCenpaskC-ViU-OpC-03zsr2chE8ggGCpg2Duws,302
1
+ trilogy/__init__.py,sha256=M6pFqPgs_BEva-ceDAVIDbK_Am1BozUgGDoN-z6dnrk,303
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
4
4
  trilogy/engine.py,sha256=3etkm2RSVKO0IkgPKkrcs33X5gN_fIMyqMNfChcsR1E,1318
5
- trilogy/executor.py,sha256=9HhdLQoou1Cy9KSDgpdYxK6uyg-UPkgx9jXJBuK5ITc,17271
5
+ trilogy/executor.py,sha256=CU-T7hl5hQab17KkJz9XhwlyI4-7MQL-JGdTDMVsE4E,16025
6
6
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ trilogy/render.py,sha256=D6rI1RNtn0StJeSe4e18lnlc-U--cNu4lh5C_NkU_uM,1218
8
9
  trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
9
- trilogy/authoring/__init__.py,sha256=6eIJewNIbrMdP6_dyLpyZ0uU62_pSj9wCP80Pkz2Bl8,1922
10
+ trilogy/authoring/__init__.py,sha256=6KPn4uoPb_7t-i510M8UNXB7nrPpaKlxAXwuRbWBdLE,2115
10
11
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
12
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
12
- trilogy/core/enums.py,sha256=aTFXL6nckmG0hpNLdCxI2kAT26cIsZPahF-pHzLQ9uc,7085
13
+ trilogy/core/enums.py,sha256=jOgUyoLGgC5-8SLBWP0jQDMxptBJbN0J48VsCa1EMYo,7111
13
14
  trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
14
15
  trilogy/core/environment_helpers.py,sha256=oOpewPwMp8xOtx2ayeeyuLNUwr-cli7UanHKot5ebNY,7627
15
16
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
16
17
  trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,673
17
- trilogy/core/functions.py,sha256=rIkZGzw9hpIkXvuqQ1qPWFJO1W_NPYc6T9t0wTZ55M0,24784
18
+ trilogy/core/functions.py,sha256=EsRYHE2kg_FckceVgYGPPs1ylrXvBjr3l1Wa6r0SvL8,25027
18
19
  trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
19
20
  trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
20
21
  trilogy/core/optimization.py,sha256=xGO8piVsLrpqrx-Aid_Y56_5slSv4eZmlP64hCHRiEc,7957
21
22
  trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
22
23
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- trilogy/core/models/author.py,sha256=eJOz-p20Am5IQPDeBntgIkncB1nUZGhuwcy7WBSbCmM,70240
24
+ trilogy/core/models/author.py,sha256=CHVnj-GW_gjGDHaAUg60VGOfPSRr_5n5LbHvocOhQj8,70371
24
25
  trilogy/core/models/build.py,sha256=bO1qYvuGl6LeNGgsfS6ZHAzZBR2lBPLg-QJymp9hgkU,57235
25
26
  trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
26
- trilogy/core/models/core.py,sha256=yie1uuq62uOQ5fjob9NMJbdvQPrCErXUT7JTCuYRyjI,9697
27
+ trilogy/core/models/core.py,sha256=00opIUXwgJy9OF-cwI883zQpArNeh6wkTpSqUszDU78,9909
27
28
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
28
- trilogy/core/models/environment.py,sha256=qFZ0_Op6zIhKc5oVS4EVYZ67f29wJhKP_xoEMV4kkuU,25991
29
+ trilogy/core/models/environment.py,sha256=WCVJNWUze37bxFSFWo_6Z7330OG5k0DDmb7a2Tz5gg4,26264
29
30
  trilogy/core/models/execute.py,sha256=ABylFQgtavjjCfFkEsFdUwfMB4UBQLHjdzQ9E67QlAE,33521
30
31
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
31
32
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
@@ -56,29 +57,29 @@ trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,s
56
57
  trilogy/core/processing/nodes/__init__.py,sha256=DqPG3Y8vl5-UTeox6hn1EE6iwPIJpsM-XeZALHSgLZQ,5058
57
58
  trilogy/core/processing/nodes/base_node.py,sha256=FHrY8GsTKPuMJklOjILbhGqCt5s1nmlj62Z-molARDA,16835
58
59
  trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
59
- trilogy/core/processing/nodes/group_node.py,sha256=qwX1CaCZJzH6oYFlGRKbi-Q7KXOjfYKplz8JDU8oy5c,7998
60
+ trilogy/core/processing/nodes/group_node.py,sha256=1caU36nHknnXS-dM6xmL9Mc7kK0wCq-IS3kL7_XYNlA,8024
60
61
  trilogy/core/processing/nodes/merge_node.py,sha256=bEz1QU2o-fl_O-VotE5dN1GmlZPClufMvUOvL2-2Uo8,15262
61
62
  trilogy/core/processing/nodes/select_node_v2.py,sha256=Xyfq8lU7rP7JTAd8VV0ATDNal64n4xIBgWQsOuMe_Ak,8824
62
63
  trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ_LpvPokpZKOhx4,1425
63
64
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
64
65
  trilogy/core/processing/nodes/window_node.py,sha256=STvwheVttxSWVHB-yUQUSo-Pyz7Uk8G1txFDAbWMp-s,1380
65
66
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- trilogy/core/statements/author.py,sha256=9wKZDwQ-BeaUCMjD9l0ffMMv8zivaYcAg12UhVFi-0Y,14248
67
+ trilogy/core/statements/author.py,sha256=X3NdGlWTUCNdQQw9EGI1kccgsLJMazwNAIsqpUSr_ZY,14443
67
68
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
69
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
69
70
  trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
70
71
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
- trilogy/dialect/base.py,sha256=u00kIIl98as1QzcduiiyyoBzxRGVeBxfeO5hWlRCAJU,40222
72
- trilogy/dialect/bigquery.py,sha256=mKC3zoEU232h9RtIXJjqiZ72lWH8a6S28p6wAZKrAfg,2952
72
+ trilogy/dialect/base.py,sha256=gEbbqll98nfhxs_JWK0h_9M9fh4nWnon2Ntlxp_s_lI,40287
73
+ trilogy/dialect/bigquery.py,sha256=PkjFcNGZHYOe655PmJhb8a0afdFULuovqP0qQVO8m0I,2953
73
74
  trilogy/dialect/common.py,sha256=cbTo_vamdp8pj9spSjGSH-bSZpy4FpNJ12k5vMvyT2Y,3942
74
75
  trilogy/dialect/config.py,sha256=EGYRQIbrkeMuud5Bkds7jSD5dCJR5hEYZUYcy-lYZl4,3308
75
76
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
76
- trilogy/dialect/duckdb.py,sha256=2tH_OetgLJoKf_f4bdeeB0ozGC8f0h_xQ271I8qD-Oo,3690
77
- trilogy/dialect/enums.py,sha256=1KDgds_DC31hGxZzNI_TIggxXF7m9rIjn9KLgNf5WQU,4425
77
+ trilogy/dialect/duckdb.py,sha256=7f4qzNKXnpXA9wkU2ouarB5u2tpBC51TwLyGmN3bEG8,3693
78
+ trilogy/dialect/enums.py,sha256=QYIcVr5RgpYMA1Wl0nWeojVVxJxy0V2_sn8uqSFNx20,4615
78
79
  trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
79
- trilogy/dialect/presto.py,sha256=bAxaDcLL21fivPg7hmBd3HJmd0yYJdPdwNgNA5ga7DE,3391
80
+ trilogy/dialect/presto.py,sha256=Mw7_F8h19mWfuZHkHQJizQWbpu1lIHe6t2PA0r88gsY,3392
80
81
  trilogy/dialect/snowflake.py,sha256=wmao9p26jX5yIX5SC8sRAZTXkPGTvq6ixO693QTfhz8,2989
81
- trilogy/dialect/sql_server.py,sha256=IN91uEM0MpsiVAlsYC89uMQkiVn0i86B8Tst6v9uFkU,3129
82
+ trilogy/dialect/sql_server.py,sha256=z2Vg7Qvw83rbGiEFIvHHLqVWJTWiz2xs76kpQj4HdTU,3131
82
83
  trilogy/hooks/__init__.py,sha256=T3SF3phuUDPLXKGRVE_Lf9mzuwoXWyaLolncR_1kY30,144
83
84
  trilogy/hooks/base_hook.py,sha256=I_l-NBMNC7hKTDx1JgHZPVOOCvLQ36m2oIGaR5EUMXY,1180
84
85
  trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2940
@@ -89,14 +90,14 @@ trilogy/parsing/common.py,sha256=IgZ2K3LzJ0usLIwxRCRmS-4luP6uwmM-f1oqGNyGbm0,213
89
90
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
90
91
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
91
92
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
92
- trilogy/parsing/parse_engine.py,sha256=uZ6MYjg6kkTm5HFfOKLGvVvzHiGgH-vY7lV-AIlIBgY,55701
93
+ trilogy/parsing/parse_engine.py,sha256=YjYe4cAxek3CceewGI1It-XEO_ZygCxY9IQzcJw0Y0Y,57555
93
94
  trilogy/parsing/render.py,sha256=o_XuQWhcwx1lD9eGVqkqZEwkmQK0HdmWWokGBtdeH4I,17837
94
- trilogy/parsing/trilogy.lark,sha256=wZpqI1louDqm-t-TpmzW749dPA9w2EIAyowyEJIeXAM,12620
95
+ trilogy/parsing/trilogy.lark,sha256=zehaPaYKuJZQ335sgCjH8Q6u_hy5A6A02XcdwziZdWE,12817
95
96
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
97
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
97
- pytrilogy-0.0.3.9.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
98
- pytrilogy-0.0.3.9.dist-info/METADATA,sha256=8Bv3VIgAPpBlaRGc1UlrWrMPLb0HYNpy-coiUXMzzK0,8983
99
- pytrilogy-0.0.3.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
100
- pytrilogy-0.0.3.9.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
101
- pytrilogy-0.0.3.9.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
102
- pytrilogy-0.0.3.9.dist-info/RECORD,,
98
+ pytrilogy-0.0.3.11.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
99
+ pytrilogy-0.0.3.11.dist-info/METADATA,sha256=xvuxEQpZ1puSOIBOM0_U1luF8pheKHrLQnTP3YDj-jA,8984
100
+ pytrilogy-0.0.3.11.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
101
+ pytrilogy-0.0.3.11.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
102
+ pytrilogy-0.0.3.11.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
103
+ pytrilogy-0.0.3.11.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.9"
7
+ __version__ = "0.0.3.11"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -33,11 +33,20 @@ from trilogy.core.models.author import (
33
33
  WindowOrder,
34
34
  WindowType,
35
35
  )
36
- from trilogy.core.models.core import DataType, ListType, ListWrapper, StructType
36
+ from trilogy.core.models.core import (
37
+ DataType,
38
+ ListType,
39
+ ListWrapper,
40
+ MapType,
41
+ StructType,
42
+ )
37
43
  from trilogy.core.models.environment import Environment
38
44
  from trilogy.core.statements.author import (
39
45
  ConceptDeclarationStatement,
40
46
  ConceptTransform,
47
+ MultiSelectStatement,
48
+ PersistStatement,
49
+ RawSQLStatement,
41
50
  SelectItem,
42
51
  SelectStatement,
43
52
  )
@@ -60,6 +69,7 @@ __all__ = [
60
69
  "DataType",
61
70
  "StructType",
62
71
  "ListType",
72
+ "MapType",
63
73
  "ListWrapper",
64
74
  "FunctionType",
65
75
  "FunctionFactory",
@@ -87,4 +97,7 @@ __all__ = [
87
97
  "DEFAULT_NAMESPACE",
88
98
  "arbitrary_to_concept",
89
99
  "arg_to_datatype",
100
+ "MultiSelectStatement",
101
+ "PersistStatement",
102
+ "RawSQLStatement",
90
103
  ]
trilogy/core/enums.py CHANGED
@@ -172,6 +172,7 @@ class FunctionType(Enum):
172
172
  UPPER = "upper"
173
173
  SUBSTRING = "substring"
174
174
  STRPOS = "strpos"
175
+ CONTAINS = "contains"
175
176
 
176
177
  # Dates
177
178
  DATE = "date"
trilogy/core/functions.py CHANGED
@@ -271,6 +271,15 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
271
271
  output_type=DataType.INTEGER,
272
272
  arg_count=2,
273
273
  ),
274
+ FunctionType.CONTAINS: FunctionConfig(
275
+ valid_inputs=[
276
+ {DataType.STRING},
277
+ {DataType.STRING},
278
+ ],
279
+ output_purpose=Purpose.PROPERTY,
280
+ output_type=DataType.BOOL,
281
+ arg_count=2,
282
+ ),
274
283
  FunctionType.SUBSTRING: FunctionConfig(
275
284
  valid_inputs=[{DataType.STRING}, {DataType.INTEGER}, {DataType.INTEGER}],
276
285
  output_purpose=Purpose.PROPERTY,
@@ -55,6 +55,7 @@ from trilogy.core.models.core import (
55
55
  MapWrapper,
56
56
  NumericType,
57
57
  StructType,
58
+ TraitDataType,
58
59
  TupleWrapper,
59
60
  arg_to_datatype,
60
61
  is_compatible_datatype,
@@ -101,9 +102,9 @@ class HasUUID(ABC):
101
102
 
102
103
  class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
103
104
  address: str
104
- datatype: DataType | ListType | StructType | MapType | NumericType = (
105
- DataType.UNKNOWN
106
- )
105
+ datatype: (
106
+ DataType | TraitDataType | ListType | StructType | MapType | NumericType
107
+ ) = DataType.UNKNOWN
107
108
  metadata: Optional["Metadata"] = None
108
109
 
109
110
  @property
@@ -756,7 +757,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
756
757
  extra="forbid",
757
758
  )
758
759
  name: str
759
- datatype: DataType | ListType | StructType | MapType | NumericType
760
+ datatype: DataType | TraitDataType | ListType | StructType | MapType | NumericType
760
761
  purpose: Purpose
761
762
  derivation: Derivation = Derivation.ROOT
762
763
  granularity: Granularity = Granularity.MULTI_ROW
@@ -1217,9 +1218,9 @@ class UndefinedConceptFull(Concept, Mergeable, Namespaced):
1217
1218
  model_config = ConfigDict(arbitrary_types_allowed=True)
1218
1219
  name: str
1219
1220
  line_no: int | None = None
1220
- datatype: DataType | ListType | StructType | MapType | NumericType = (
1221
- DataType.UNKNOWN
1222
- )
1221
+ datatype: (
1222
+ DataType | TraitDataType | ListType | StructType | MapType | NumericType
1223
+ ) = DataType.UNKNOWN
1223
1224
  purpose: Purpose = Purpose.UNKNOWN
1224
1225
 
1225
1226
  @property
@@ -2153,6 +2154,11 @@ class ArgBinding(BaseModel):
2153
2154
  default: Expr | None = None
2154
2155
 
2155
2156
 
2157
+ class CustomType(BaseModel):
2158
+ name: str
2159
+ type: DataType
2160
+
2161
+
2156
2162
  Expr = (
2157
2163
  MagicConstants
2158
2164
  | bool
@@ -97,6 +97,19 @@ class DataType(Enum):
97
97
  return self
98
98
 
99
99
 
100
+ class TraitDataType(BaseModel):
101
+ type: DataType
102
+ traits: list[str]
103
+
104
+ @property
105
+ def data_type(self):
106
+ return self.type
107
+
108
+ @property
109
+ def value(self):
110
+ return self.data_type.value
111
+
112
+
100
113
  class NumericType(BaseModel):
101
114
  precision: int = 20
102
115
  scale: int = 5
@@ -40,6 +40,7 @@ from trilogy.core.exceptions import (
40
40
  from trilogy.core.models.author import (
41
41
  Concept,
42
42
  ConceptRef,
43
+ CustomType,
43
44
  Function,
44
45
  SelectLineage,
45
46
  UndefinedConcept,
@@ -59,8 +60,23 @@ class Import:
59
60
  path: Path
60
61
 
61
62
 
63
+ class BaseImportResolver(BaseModel):
64
+ pass
65
+
66
+
67
+ class FileSystemImportResolver(BaseImportResolver):
68
+ pass
69
+
70
+
71
+ class DictImportResolver(BaseImportResolver):
72
+ content: Dict[str, str]
73
+
74
+
62
75
  class EnvironmentOptions(BaseModel):
63
76
  allow_duplicate_declaration: bool = True
77
+ import_resolver: BaseImportResolver = Field(
78
+ default_factory=FileSystemImportResolver
79
+ )
64
80
 
65
81
 
66
82
  class EnvironmentConceptDict(dict):
@@ -191,14 +207,14 @@ class Environment(BaseModel):
191
207
  EnvironmentDatasourceDict, PlainValidator(validate_datasources)
192
208
  ] = Field(default_factory=EnvironmentDatasourceDict)
193
209
  functions: Dict[str, Callable[..., Any]] = Field(default_factory=dict)
194
- data_types: Dict[str, DataType] = Field(default_factory=dict)
210
+ data_types: Dict[str, CustomType] = Field(default_factory=dict)
195
211
  named_statements: Dict[str, SelectLineage] = Field(default_factory=dict)
196
212
  imports: Dict[str, list[Import]] = Field(
197
213
  default_factory=lambda: defaultdict(list) # type: ignore
198
214
  )
199
215
  namespace: str = DEFAULT_NAMESPACE
200
216
  working_path: str | Path = Field(default_factory=lambda: os.getcwd())
201
- environment_config: EnvironmentOptions = Field(default_factory=EnvironmentOptions)
217
+ config: EnvironmentOptions = Field(default_factory=EnvironmentOptions)
202
218
  version: str = Field(default_factory=get_version)
203
219
  cte_name_map: Dict[str, str] = Field(default_factory=dict)
204
220
  materialized_concepts: set[str] = Field(default_factory=set)
@@ -233,7 +249,7 @@ class Environment(BaseModel):
233
249
  imports=dict(self.imports),
234
250
  namespace=self.namespace,
235
251
  working_path=self.working_path,
236
- environment_config=self.environment_config,
252
+ environment_config=self.config,
237
253
  version=self.version,
238
254
  cte_name_map=dict(self.cte_name_map),
239
255
  materialized_concepts=set(self.materialized_concepts),
@@ -341,7 +357,7 @@ class Environment(BaseModel):
341
357
 
342
358
  return None
343
359
 
344
- if existing and self.environment_config.allow_duplicate_declaration:
360
+ if existing and self.config.allow_duplicate_declaration:
345
361
  if existing.metadata.concept_source == ConceptSource.PERSIST_STATEMENT:
346
362
  return handle_persist()
347
363
  return
@@ -92,7 +92,7 @@ class GroupNode(StrategyNode):
92
92
  comp_grain += source.grain
93
93
  for x in source.output_concepts:
94
94
  concept_map[x.address] = x
95
- lookups = [
95
+ lookups: list[BuildConcept | str] = [
96
96
  concept_map[x] if x in concept_map else x for x in comp_grain.components
97
97
  ]
98
98
  comp_grain = BuildGrain.from_concepts(lookups, environment=environment)
@@ -15,8 +15,11 @@ from trilogy.core.enums import (
15
15
  from trilogy.core.models.author import (
16
16
  AggregateWrapper,
17
17
  AlignClause,
18
+ ArgBinding,
18
19
  Concept,
19
20
  ConceptRef,
21
+ CustomType,
22
+ Expr,
20
23
  FilterItem,
21
24
  Function,
22
25
  Grain,
@@ -413,3 +416,13 @@ class ConceptDeclarationStatement(HasUUID, BaseModel):
413
416
 
414
417
  class ConceptDerivationStatement(BaseModel):
415
418
  concept: Concept
419
+
420
+
421
+ class TypeDeclaration(BaseModel):
422
+ type: CustomType
423
+
424
+
425
+ class FunctionDeclaration(BaseModel):
426
+ name: str
427
+ args: list[ArgBinding]
428
+ expr: Expr
trilogy/dialect/base.py CHANGED
@@ -184,6 +184,7 @@ FUNCTION_MAP = {
184
184
  FunctionType.LOWER: lambda x: f"LOWER({x[0]}) ",
185
185
  FunctionType.SUBSTRING: lambda x: f"SUBSTRING({x[0]},{x[1]},{x[2]})",
186
186
  FunctionType.STRPOS: lambda x: f"STRPOS({x[0]},{x[1]})",
187
+ FunctionType.CONTAINS: lambda x: f"CONTAINS({x[0]},{x[1]})",
187
188
  # FunctionType.NOT_LIKE: lambda x: f" CASE WHEN {x[0]} like {x[1]} THEN 0 ELSE 1 END",
188
189
  # date types
189
190
  FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc({x[0]},{x[1]})",
@@ -42,10 +42,9 @@ CREATE OR REPLACE TABLE {{ output.address.location }} AS
42
42
  {% endif %}{%- if ctes %}
43
43
  WITH {% for cte in ctes %}
44
44
  {{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
45
- {%- if full_select %}
45
+ {%- if full_select -%}
46
46
  {{full_select}}
47
- {% else -%}
48
-
47
+ {%- else -%}
49
48
  SELECT
50
49
  {%- for select in select_columns %}
51
50
  {{ select }}{% if not loop.last %},{% endif %}{% endfor %}
trilogy/dialect/duckdb.py CHANGED
@@ -57,8 +57,8 @@ CREATE OR REPLACE TABLE {{ output.address.location }} AS
57
57
  WITH {% for cte in ctes %}
58
58
  {{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
59
59
  {%- if full_select -%}{{full_select}}
60
- {%- else -%}{%- if comment %}
61
- -- {{ comment }}{% endif %}
60
+ {%- else -%}{%- if comment -%}
61
+ -- {{ comment }}{%- endif -%}
62
62
  SELECT
63
63
  {%- for select in select_columns %}
64
64
  {{ select }}{% if not loop.last %},{% endif %}{% endfor %}
trilogy/dialect/enums.py CHANGED
@@ -41,6 +41,11 @@ class Dialects(Enum):
41
41
  return cls.DUCK_DB
42
42
  return super()._missing_(value)
43
43
 
44
+ def default_renderer(self, conf=None, _engine_factory: Callable = default_factory):
45
+ from trilogy.render import get_dialect_generator
46
+
47
+ return get_dialect_generator(self)
48
+
44
49
  def default_engine(self, conf=None, _engine_factory: Callable = default_factory):
45
50
  if self == Dialects.BIGQUERY:
46
51
  from google.auth import default
trilogy/dialect/presto.py CHANGED
@@ -50,7 +50,7 @@ WITH {% for cte in ctes %}
50
50
  {{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
51
51
  {%- if full_select -%}
52
52
  {{full_select}}
53
- {%- else %}
53
+ {%- else -%}
54
54
  SELECT
55
55
  {%- for select in select_columns %}
56
56
  {{ select }}{% if not loop.last %},{% endif %}{% endfor %}
@@ -42,7 +42,7 @@ WITH {% for cte in ctes %}
42
42
  {{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
43
43
  {%- if full_select -%}{{full_select}}
44
44
  {%- else -%}{%- if comment %}
45
- -- {{ comment }}{% endif %}
45
+ -- {{ comment }}{%- endif -%}
46
46
  SELECT
47
47
  {%- if limit is not none %}
48
48
  TOP {{ limit }}{% endif %}
trilogy/executor.py CHANGED
@@ -36,6 +36,7 @@ from trilogy.dialect.enums import Dialects
36
36
  from trilogy.engine import ExecutionEngine
37
37
  from trilogy.hooks.base_hook import BaseHook
38
38
  from trilogy.parser import parse_text
39
+ from trilogy.render import get_dialect_generator
39
40
 
40
41
 
41
42
  class ResultProtocol(Protocol):
@@ -82,40 +83,7 @@ class Executor(object):
82
83
  self.generator: BaseDialect
83
84
  self.logger = logger
84
85
  self.hooks = hooks
85
- if self.dialect == Dialects.BIGQUERY:
86
- from trilogy.dialect.bigquery import BigqueryDialect
87
-
88
- self.generator = BigqueryDialect()
89
- elif self.dialect == Dialects.SQL_SERVER:
90
- from trilogy.dialect.sql_server import SqlServerDialect
91
-
92
- self.generator = SqlServerDialect()
93
- elif self.dialect == Dialects.DUCK_DB:
94
- from trilogy.dialect.duckdb import DuckDBDialect
95
-
96
- self.generator = DuckDBDialect()
97
- elif self.dialect == Dialects.PRESTO:
98
- from trilogy.dialect.presto import PrestoDialect
99
-
100
- self.generator = PrestoDialect()
101
- elif self.dialect == Dialects.TRINO:
102
- from trilogy.dialect.presto import TrinoDialect
103
-
104
- self.generator = TrinoDialect()
105
- elif self.dialect == Dialects.POSTGRES:
106
- from trilogy.dialect.postgres import PostgresDialect
107
-
108
- self.generator = PostgresDialect()
109
- elif self.dialect == Dialects.SNOWFLAKE:
110
- from trilogy.dialect.snowflake import SnowflakeDialect
111
-
112
- self.generator = SnowflakeDialect()
113
- elif self.dialect == Dialects.DATAFRAME:
114
- from trilogy.dialect.dataframe import DataframeDialect
115
-
116
- self.generator = DataframeDialect()
117
- else:
118
- raise ValueError(f"Unsupported dialect {self.dialect}")
86
+ self.generator = get_dialect_generator(self.dialect)
119
87
  self.connection = self.engine.connect()
120
88
  # TODO: make generic
121
89
  if self.dialect == Dialects.DATAFRAME:
@@ -4,7 +4,7 @@ from enum import Enum
4
4
  from os.path import dirname, join
5
5
  from pathlib import Path
6
6
  from re import IGNORECASE
7
- from typing import Callable, List, Optional, Tuple, Union
7
+ from typing import List, Optional, Tuple, Union
8
8
 
9
9
  from lark import Lark, ParseTree, Transformer, Tree, v_args
10
10
  from lark.exceptions import (
@@ -57,6 +57,7 @@ from trilogy.core.models.author import (
57
57
  Concept,
58
58
  ConceptRef,
59
59
  Conditional,
60
+ CustomType,
60
61
  Expr,
61
62
  FilterItem,
62
63
  Function,
@@ -82,6 +83,7 @@ from trilogy.core.models.core import (
82
83
  MapWrapper,
83
84
  NumericType,
84
85
  StructType,
86
+ TraitDataType,
85
87
  TupleWrapper,
86
88
  arg_to_datatype,
87
89
  dict_to_map_wrapper,
@@ -95,12 +97,18 @@ from trilogy.core.models.datasource import (
95
97
  Query,
96
98
  RawColumnExpr,
97
99
  )
98
- from trilogy.core.models.environment import Environment, Import
100
+ from trilogy.core.models.environment import (
101
+ DictImportResolver,
102
+ Environment,
103
+ FileSystemImportResolver,
104
+ Import,
105
+ )
99
106
  from trilogy.core.statements.author import (
100
107
  ConceptDeclarationStatement,
101
108
  ConceptDerivationStatement,
102
109
  ConceptTransform,
103
110
  CopyStatement,
111
+ FunctionDeclaration,
104
112
  ImportStatement,
105
113
  Limit,
106
114
  MergeStatementV2,
@@ -111,6 +119,7 @@ from trilogy.core.statements.author import (
111
119
  SelectItem,
112
120
  SelectStatement,
113
121
  ShowStatement,
122
+ TypeDeclaration,
114
123
  )
115
124
  from trilogy.parsing.common import (
116
125
  align_item_to_concept,
@@ -219,7 +228,7 @@ class ParseToObjects(Transformer):
219
228
  self,
220
229
  environment: Environment,
221
230
  parse_address: str | None = None,
222
- token_address: Path | None = None,
231
+ token_address: Path | str | None = None,
223
232
  parsed: dict[str, "ParseToObjects"] | None = None,
224
233
  tokens: dict[Path | str, ParseTree] | None = None,
225
234
  text_lookup: dict[Path | str, str] | None = None,
@@ -365,8 +374,9 @@ class ParseToObjects(Transformer):
365
374
 
366
375
  def data_type(
367
376
  self, args
368
- ) -> DataType | ListType | StructType | MapType | NumericType:
377
+ ) -> DataType | TraitDataType | ListType | StructType | MapType | NumericType:
369
378
  resolved = args[0]
379
+ traits = args[2:]
370
380
  if isinstance(resolved, StructType):
371
381
  return resolved
372
382
  elif isinstance(resolved, ListType):
@@ -375,7 +385,10 @@ class ParseToObjects(Transformer):
375
385
  return resolved
376
386
  elif isinstance(resolved, MapType):
377
387
  return resolved
378
- return DataType(args[0].lower())
388
+ base = DataType(args[0].lower())
389
+ if traits:
390
+ return TraitDataType(type=base, traits=traits)
391
+ return base
379
392
 
380
393
  def array_comparison(self, args) -> ComparisonOperator:
381
394
  return ComparisonOperator([x.value.lower() for x in args])
@@ -846,8 +859,22 @@ class ParseToObjects(Transformer):
846
859
  )
847
860
 
848
861
  def resolve_import_address(self, address) -> str:
849
- with open(address, "r", encoding="utf-8") as f:
850
- text = f.read()
862
+ if isinstance(
863
+ self.environment.config.import_resolver, FileSystemImportResolver
864
+ ):
865
+ with open(address, "r", encoding="utf-8") as f:
866
+ text = f.read()
867
+ elif isinstance(self.environment.config.import_resolver, DictImportResolver):
868
+ lookup = address
869
+ if lookup not in self.environment.config.import_resolver.content:
870
+ raise ImportError(
871
+ f"Unable to import file {lookup}, not found in import resolver"
872
+ )
873
+ text = self.environment.config.import_resolver.content[lookup]
874
+ else:
875
+ raise ImportError(
876
+ f"Unable to import file {address}, resolver type {type(self.environment.config.import_resolver)} not supported"
877
+ )
851
878
  return text
852
879
 
853
880
  def import_statement(self, args: list[str]) -> ImportStatement:
@@ -859,10 +886,17 @@ class ParseToObjects(Transformer):
859
886
  cache_key = args[0]
860
887
  path = args[0].split(".")
861
888
 
862
- target = join(self.environment.working_path, *path) + ".preql"
863
-
864
- # tokens + text are cached by path
865
- token_lookup = Path(target)
889
+ if isinstance(
890
+ self.environment.config.import_resolver, FileSystemImportResolver
891
+ ):
892
+ target = join(self.environment.working_path, *path) + ".preql"
893
+ # tokens + text are cached by path
894
+ token_lookup: Path | str = Path(target)
895
+ elif isinstance(self.environment.config.import_resolver, DictImportResolver):
896
+ target = ".".join(path)
897
+ token_lookup = target
898
+ else:
899
+ raise NotImplementedError
866
900
 
867
901
  # parser + env has to be cached by prior import path + current key
868
902
  key_path = self.import_keys + [cache_key]
@@ -893,6 +927,7 @@ class ParseToObjects(Transformer):
893
927
  new_env = Environment(
894
928
  working_path=dirname(target),
895
929
  env_file_path=token_lookup,
930
+ config=self.environment.config,
896
931
  )
897
932
  new_env.concepts.fail_on_missing = False
898
933
  self.parsed[self.parse_address] = self
@@ -1087,7 +1122,7 @@ class ParseToObjects(Transformer):
1087
1122
  return ArgBinding(name=args[0], default=None)
1088
1123
 
1089
1124
  @v_args(meta=True)
1090
- def raw_function(self, meta: Meta, args) -> Callable[[list[Expr]], Expr]:
1125
+ def raw_function(self, meta: Meta, args) -> FunctionDeclaration:
1091
1126
  identity = args[0]
1092
1127
  function_arguments: list[ArgBinding] = args[1]
1093
1128
  output = args[2]
@@ -1109,7 +1144,7 @@ class ParseToObjects(Transformer):
1109
1144
  return nout
1110
1145
 
1111
1146
  self.environment.functions[identity] = function_factory
1112
- return function_factory
1147
+ return FunctionDeclaration(name=identity, args=function_arguments, expr=output)
1113
1148
 
1114
1149
  def custom_function(self, args):
1115
1150
  name = args[0]
@@ -1121,6 +1156,13 @@ class ParseToObjects(Transformer):
1121
1156
  def function(self, meta: Meta, args) -> Function:
1122
1157
  return args[0]
1123
1158
 
1159
+ @v_args(meta=True)
1160
+ def type_declaration(self, meta: Meta, args) -> TypeDeclaration:
1161
+ key = args[0]
1162
+ datatype = args[1]
1163
+ self.environment.data_types[key] = datatype
1164
+ return TypeDeclaration(type=CustomType(name=key, type=datatype))
1165
+
1124
1166
  def int_lit(self, args):
1125
1167
  return int("".join(args))
1126
1168
 
@@ -1441,6 +1483,10 @@ class ParseToObjects(Transformer):
1441
1483
  def fstrpos(self, meta, args):
1442
1484
  return self.function_factory.create_function(args, FunctionType.STRPOS, meta)
1443
1485
 
1486
+ @v_args(meta=True)
1487
+ def fcontains(self, meta, args):
1488
+ return self.function_factory.create_function(args, FunctionType.CONTAINS, meta)
1489
+
1444
1490
  @v_args(meta=True)
1445
1491
  def fsubstring(self, meta, args):
1446
1492
  return self.function_factory.create_function(args, FunctionType.SUBSTRING, meta)
@@ -3,6 +3,7 @@
3
3
  ?statement: concept
4
4
  | datasource
5
5
  | function
6
+ | type_declaration
6
7
  | multi_select_statement
7
8
  | select_statement
8
9
  | persist_statement
@@ -92,14 +93,15 @@
92
93
  function_binding_item: IDENTIFIER ("=" literal)?
93
94
  function_binding_list: (function_binding_item ",")* function_binding_item ","?
94
95
  raw_function: "def" IDENTIFIER "(" function_binding_list ")" "->" expr
95
-
96
+
97
+ // TYPE blocks
98
+ type_declaration: "type" IDENTIFIER data_type
99
+
96
100
  // user_id where state = Mexico
97
101
  _filter_alt: IDENTIFIER "?" conditional
98
102
  _filter_base: "filter"i IDENTIFIER where
99
103
  filter_item: _filter_base | _filter_alt
100
104
 
101
-
102
-
103
105
  // rank/lag/lead
104
106
  WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i | "avg"i | "max"i | "min"i ) /[\s]+/
105
107
 
@@ -223,16 +225,18 @@
223
225
  alt_like: expr "like"i expr
224
226
  _UPPER.1: "upper("i
225
227
  upper: _UPPER expr ")"
226
- _LOWER.1: "lower("i
228
+ _LOWER.1: "lower("i
227
229
  lower: _LOWER expr ")"
228
230
  _SPLIT.1: "split("i
229
231
  fsplit: _SPLIT expr "," string_lit ")"
230
- _STRPOS.1: "strpos("i
232
+ _STRPOS.1: "strpos("i
231
233
  fstrpos: _STRPOS expr "," expr ")"
234
+ _CONTAINS.1: "contains("i
235
+ fcontains: _CONTAINS expr "," expr ")"
232
236
  _SUBSTRING.1: "substring("i
233
237
  fsubstring: _SUBSTRING expr "," expr "," expr ")"
234
238
 
235
- _string_functions: like | ilike | upper | lower | fsplit | fstrpos | fsubstring
239
+ _string_functions: like | ilike | upper | lower | fsplit | fstrpos | fsubstring | fcontains
236
240
 
237
241
  // special aggregate
238
242
  _GROUP.1: "group("i
@@ -348,7 +352,7 @@
348
352
 
349
353
  map_type: "map"i "<" data_type "," data_type ">"
350
354
 
351
- !data_type: "string"i | "number"i | "numeric"i | "map"i | "list"i | "array"i | "any"i | "int"i | "bigint"i | "date"i | "datetime"i | "timestamp"i | "float"i | "bool"i | numeric_type | map_type | struct_type | list_type
355
+ !data_type: ("string"i | "number"i | "numeric"i | "map"i | "list"i | "array"i | "any"i | "int"i | "bigint"i | "date"i | "datetime"i | "timestamp"i | "float"i | "bool"i | numeric_type | map_type | struct_type | list_type) ("::" IDENTIFIER)?
352
356
 
353
357
  PURPOSE: "key"i | "metric"i | CONST
354
358
  PROPERTY: "property"i
@@ -361,7 +365,7 @@
361
365
  show_category: CONCEPTS | DATASOURCES
362
366
 
363
367
  show_statement: "show"i ( show_category | select_statement | persist_statement) _TERMINATOR
364
- COMMENT: /#.*(\n|$)/ | /\/\/.*\n/
368
+ COMMENT: /#.*(\n|$)/ | /\/\/.*\n/
365
369
  %import common.WS
366
370
  %ignore WS
367
371
  %ignore COMMENT
trilogy/render.py ADDED
@@ -0,0 +1,38 @@
1
+ from trilogy.dialect.enums import Dialects
2
+
3
+
4
+ def get_dialect_generator(dialect: Dialects):
5
+ if dialect == Dialects.BIGQUERY:
6
+ from trilogy.dialect.bigquery import BigqueryDialect
7
+
8
+ return BigqueryDialect()
9
+ elif dialect == Dialects.SQL_SERVER:
10
+ from trilogy.dialect.sql_server import SqlServerDialect
11
+
12
+ return SqlServerDialect()
13
+ elif dialect == Dialects.DUCK_DB:
14
+ from trilogy.dialect.duckdb import DuckDBDialect
15
+
16
+ return DuckDBDialect()
17
+ elif dialect == Dialects.PRESTO:
18
+ from trilogy.dialect.presto import PrestoDialect
19
+
20
+ return PrestoDialect()
21
+ elif dialect == Dialects.TRINO:
22
+ from trilogy.dialect.presto import TrinoDialect
23
+
24
+ return TrinoDialect()
25
+ elif dialect == Dialects.POSTGRES:
26
+ from trilogy.dialect.postgres import PostgresDialect
27
+
28
+ return PostgresDialect()
29
+ elif dialect == Dialects.SNOWFLAKE:
30
+ from trilogy.dialect.snowflake import SnowflakeDialect
31
+
32
+ return SnowflakeDialect()
33
+ elif dialect == Dialects.DATAFRAME:
34
+ from trilogy.dialect.dataframe import DataframeDialect
35
+
36
+ return DataframeDialect()
37
+ else:
38
+ raise ValueError(f"Unsupported dialect {dialect}")