pytrilogy 0.0.3.6__py3-none-any.whl → 0.0.3.8__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.
- {pytrilogy-0.0.3.6.dist-info → pytrilogy-0.0.3.8.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.6.dist-info → pytrilogy-0.0.3.8.dist-info}/RECORD +23 -22
- trilogy/__init__.py +1 -1
- trilogy/core/functions.py +26 -6
- trilogy/core/models/author.py +104 -39
- trilogy/core/models/build.py +20 -3
- trilogy/core/models/datasource.py +6 -0
- trilogy/core/models/environment.py +2 -1
- trilogy/core/processing/node_generators/group_node.py +1 -1
- trilogy/core/processing/node_generators/multiselect_node.py +1 -1
- trilogy/core/statements/author.py +2 -0
- trilogy/dialect/config.py +15 -0
- trilogy/dialect/dataframe.py +47 -0
- trilogy/dialect/enums.py +12 -1
- trilogy/engine.py +11 -4
- trilogy/executor.py +10 -2
- trilogy/parsing/common.py +57 -48
- trilogy/parsing/parse_engine.py +53 -33
- trilogy/parsing/trilogy.lark +6 -4
- {pytrilogy-0.0.3.6.dist-info → pytrilogy-0.0.3.8.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.3.6.dist-info → pytrilogy-0.0.3.8.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.6.dist-info → pytrilogy-0.0.3.8.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.6.dist-info → pytrilogy-0.0.3.8.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
trilogy/__init__.py,sha256=OtihFBeuWR7p7GBhiNMfyCpmu-x2kn_W2xSawos5ILM,302
|
|
2
2
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
|
|
4
|
-
trilogy/engine.py,sha256=
|
|
5
|
-
trilogy/executor.py,sha256=
|
|
4
|
+
trilogy/engine.py,sha256=3etkm2RSVKO0IkgPKkrcs33X5gN_fIMyqMNfChcsR1E,1318
|
|
5
|
+
trilogy/executor.py,sha256=YgSCeeYVecI9526LGSLVe2apOo7Ddsttvs_nDC9yElQ,17194
|
|
6
6
|
trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
|
|
7
7
|
trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
|
|
@@ -14,18 +14,18 @@ trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0
|
|
|
14
14
|
trilogy/core/environment_helpers.py,sha256=oOpewPwMp8xOtx2ayeeyuLNUwr-cli7UanHKot5ebNY,7627
|
|
15
15
|
trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
|
|
16
16
|
trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,673
|
|
17
|
-
trilogy/core/functions.py,sha256=
|
|
17
|
+
trilogy/core/functions.py,sha256=rIkZGzw9hpIkXvuqQ1qPWFJO1W_NPYc6T9t0wTZ55M0,24784
|
|
18
18
|
trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
|
|
19
19
|
trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
|
|
20
20
|
trilogy/core/optimization.py,sha256=xGO8piVsLrpqrx-Aid_Y56_5slSv4eZmlP64hCHRiEc,7957
|
|
21
21
|
trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
|
|
22
22
|
trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
trilogy/core/models/author.py,sha256=
|
|
24
|
-
trilogy/core/models/build.py,sha256=
|
|
23
|
+
trilogy/core/models/author.py,sha256=S6riJx8Z4_orbohjK3fcZh5Ei6nCPYAS2FGDsI2njHk,69488
|
|
24
|
+
trilogy/core/models/build.py,sha256=z2QO7l2E2-1hHimmOGsLl42sTnEb2x9o45zkvOoJYpM,56687
|
|
25
25
|
trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
|
|
26
26
|
trilogy/core/models/core.py,sha256=yie1uuq62uOQ5fjob9NMJbdvQPrCErXUT7JTCuYRyjI,9697
|
|
27
|
-
trilogy/core/models/datasource.py,sha256=
|
|
28
|
-
trilogy/core/models/environment.py,sha256=
|
|
27
|
+
trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
|
|
28
|
+
trilogy/core/models/environment.py,sha256=qFZ0_Op6zIhKc5oVS4EVYZ67f29wJhKP_xoEMV4kkuU,25991
|
|
29
29
|
trilogy/core/models/execute.py,sha256=ABylFQgtavjjCfFkEsFdUwfMB4UBQLHjdzQ9E67QlAE,33521
|
|
30
30
|
trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
|
|
31
31
|
trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
|
|
@@ -40,9 +40,9 @@ trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW
|
|
|
40
40
|
trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
|
|
41
41
|
trilogy/core/processing/node_generators/common.py,sha256=ZsDzThjm_mAtdQpKAg8QIJiPVZ4KuUkKyilj4eOhSDs,9439
|
|
42
42
|
trilogy/core/processing/node_generators/filter_node.py,sha256=rlY7TbgjJlGhahYgdCIJpJbaSREAGVJEsyUIGaA38O0,8271
|
|
43
|
-
trilogy/core/processing/node_generators/group_node.py,sha256=
|
|
43
|
+
trilogy/core/processing/node_generators/group_node.py,sha256=3-TXVnRO9_jqE_e1kWLqbgtBShW8WFtKwQk8oOtOULs,5894
|
|
44
44
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=E5bEjovSx422d_MlAUCDFdY4P2WJVp61BmWwltkhzA8,3095
|
|
45
|
-
trilogy/core/processing/node_generators/multiselect_node.py,sha256=
|
|
45
|
+
trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
|
|
46
46
|
trilogy/core/processing/node_generators/node_merge_node.py,sha256=sv55oynfqgpHEpo1OEtVDri-5fywzPhDlR85qaWikvY,16195
|
|
47
47
|
trilogy/core/processing/node_generators/rowset_node.py,sha256=8yeMWiyi9IFnza7qPn9YYC3WpA53weq3AY5WisIui8Y,6705
|
|
48
48
|
trilogy/core/processing/node_generators/select_merge_node.py,sha256=VHCPMbnKfg7AOfoYa6PKxpNni-j5JEfliNUiltmZhds,19698
|
|
@@ -63,7 +63,7 @@ trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ
|
|
|
63
63
|
trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
|
|
64
64
|
trilogy/core/processing/nodes/window_node.py,sha256=STvwheVttxSWVHB-yUQUSo-Pyz7Uk8G1txFDAbWMp-s,1380
|
|
65
65
|
trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
|
-
trilogy/core/statements/author.py,sha256=
|
|
66
|
+
trilogy/core/statements/author.py,sha256=9wKZDwQ-BeaUCMjD9l0ffMMv8zivaYcAg12UhVFi-0Y,14248
|
|
67
67
|
trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
68
|
trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
|
|
69
69
|
trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
|
|
@@ -71,9 +71,10 @@ trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
71
71
|
trilogy/dialect/base.py,sha256=u00kIIl98as1QzcduiiyyoBzxRGVeBxfeO5hWlRCAJU,40222
|
|
72
72
|
trilogy/dialect/bigquery.py,sha256=mKC3zoEU232h9RtIXJjqiZ72lWH8a6S28p6wAZKrAfg,2952
|
|
73
73
|
trilogy/dialect/common.py,sha256=cbTo_vamdp8pj9spSjGSH-bSZpy4FpNJ12k5vMvyT2Y,3942
|
|
74
|
-
trilogy/dialect/config.py,sha256=
|
|
74
|
+
trilogy/dialect/config.py,sha256=EGYRQIbrkeMuud5Bkds7jSD5dCJR5hEYZUYcy-lYZl4,3308
|
|
75
|
+
trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
|
|
75
76
|
trilogy/dialect/duckdb.py,sha256=2tH_OetgLJoKf_f4bdeeB0ozGC8f0h_xQ271I8qD-Oo,3690
|
|
76
|
-
trilogy/dialect/enums.py,sha256=
|
|
77
|
+
trilogy/dialect/enums.py,sha256=1KDgds_DC31hGxZzNI_TIggxXF7m9rIjn9KLgNf5WQU,4425
|
|
77
78
|
trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
|
|
78
79
|
trilogy/dialect/presto.py,sha256=bAxaDcLL21fivPg7hmBd3HJmd0yYJdPdwNgNA5ga7DE,3391
|
|
79
80
|
trilogy/dialect/snowflake.py,sha256=wmao9p26jX5yIX5SC8sRAZTXkPGTvq6ixO693QTfhz8,2989
|
|
@@ -84,18 +85,18 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
|
|
|
84
85
|
trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
|
|
85
86
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
86
87
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
|
-
trilogy/parsing/common.py,sha256=
|
|
88
|
+
trilogy/parsing/common.py,sha256=RwO9CdNYy3KxJCjg5Ta_xJwfZHV2PuRErxg66Cs50ws,20490
|
|
88
89
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
89
90
|
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
90
91
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
91
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
92
|
+
trilogy/parsing/parse_engine.py,sha256=uZ6MYjg6kkTm5HFfOKLGvVvzHiGgH-vY7lV-AIlIBgY,55701
|
|
92
93
|
trilogy/parsing/render.py,sha256=o_XuQWhcwx1lD9eGVqkqZEwkmQK0HdmWWokGBtdeH4I,17837
|
|
93
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
94
|
+
trilogy/parsing/trilogy.lark,sha256=wZpqI1louDqm-t-TpmzW749dPA9w2EIAyowyEJIeXAM,12620
|
|
94
95
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
95
96
|
trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
|
|
96
|
-
pytrilogy-0.0.3.
|
|
97
|
-
pytrilogy-0.0.3.
|
|
98
|
-
pytrilogy-0.0.3.
|
|
99
|
-
pytrilogy-0.0.3.
|
|
100
|
-
pytrilogy-0.0.3.
|
|
101
|
-
pytrilogy-0.0.3.
|
|
97
|
+
pytrilogy-0.0.3.8.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
98
|
+
pytrilogy-0.0.3.8.dist-info/METADATA,sha256=DAu0XOCyEgXpZj9-Znl0IbtouGXHELjt2EUZnp3IgEs,8983
|
|
99
|
+
pytrilogy-0.0.3.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
100
|
+
pytrilogy-0.0.3.8.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
|
|
101
|
+
pytrilogy-0.0.3.8.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
102
|
+
pytrilogy-0.0.3.8.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/core/functions.py
CHANGED
|
@@ -503,32 +503,52 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
503
503
|
arg_count=1,
|
|
504
504
|
),
|
|
505
505
|
FunctionType.ADD: FunctionConfig(
|
|
506
|
-
valid_inputs={
|
|
506
|
+
valid_inputs={
|
|
507
|
+
DataType.INTEGER,
|
|
508
|
+
DataType.FLOAT,
|
|
509
|
+
DataType.NUMBER,
|
|
510
|
+
DataType.NUMERIC,
|
|
511
|
+
},
|
|
507
512
|
output_purpose=Purpose.PROPERTY,
|
|
508
513
|
output_type=DataType.INTEGER,
|
|
509
514
|
arg_count=InfiniteFunctionArgs,
|
|
510
515
|
),
|
|
511
516
|
FunctionType.SUBTRACT: FunctionConfig(
|
|
512
|
-
valid_inputs={
|
|
517
|
+
valid_inputs={
|
|
518
|
+
DataType.INTEGER,
|
|
519
|
+
DataType.FLOAT,
|
|
520
|
+
DataType.NUMBER,
|
|
521
|
+
DataType.NUMERIC,
|
|
522
|
+
},
|
|
513
523
|
output_purpose=Purpose.PROPERTY,
|
|
514
524
|
output_type=DataType.INTEGER,
|
|
515
525
|
arg_count=InfiniteFunctionArgs,
|
|
516
526
|
),
|
|
517
527
|
FunctionType.MULTIPLY: FunctionConfig(
|
|
518
|
-
valid_inputs={
|
|
528
|
+
valid_inputs={
|
|
529
|
+
DataType.INTEGER,
|
|
530
|
+
DataType.FLOAT,
|
|
531
|
+
DataType.NUMBER,
|
|
532
|
+
DataType.NUMERIC,
|
|
533
|
+
},
|
|
519
534
|
output_purpose=Purpose.PROPERTY,
|
|
520
535
|
output_type=DataType.INTEGER,
|
|
521
536
|
arg_count=InfiniteFunctionArgs,
|
|
522
537
|
),
|
|
523
538
|
FunctionType.DIVIDE: FunctionConfig(
|
|
524
|
-
valid_inputs={
|
|
539
|
+
valid_inputs={
|
|
540
|
+
DataType.INTEGER,
|
|
541
|
+
DataType.FLOAT,
|
|
542
|
+
DataType.NUMBER,
|
|
543
|
+
DataType.NUMERIC,
|
|
544
|
+
},
|
|
525
545
|
output_purpose=Purpose.PROPERTY,
|
|
526
546
|
output_type=DataType.INTEGER,
|
|
527
547
|
arg_count=InfiniteFunctionArgs,
|
|
528
548
|
),
|
|
529
549
|
FunctionType.MOD: FunctionConfig(
|
|
530
550
|
valid_inputs=[
|
|
531
|
-
{DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
|
|
551
|
+
{DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
|
|
532
552
|
{DataType.INTEGER},
|
|
533
553
|
],
|
|
534
554
|
output_purpose=Purpose.PROPERTY,
|
|
@@ -537,7 +557,7 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
537
557
|
),
|
|
538
558
|
FunctionType.ROUND: FunctionConfig(
|
|
539
559
|
valid_inputs=[
|
|
540
|
-
{DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
|
|
560
|
+
{DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
|
|
541
561
|
{DataType.INTEGER},
|
|
542
562
|
],
|
|
543
563
|
output_purpose=Purpose.PROPERTY,
|
trilogy/core/models/author.py
CHANGED
|
@@ -75,6 +75,9 @@ class Mergeable(ABC):
|
|
|
75
75
|
def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
|
|
76
76
|
raise NotImplementedError
|
|
77
77
|
|
|
78
|
+
def with_reference_replacement(self, source: str, target: Expr):
|
|
79
|
+
raise NotImplementedError(type(self))
|
|
80
|
+
|
|
78
81
|
|
|
79
82
|
class ConceptArgs(ABC):
|
|
80
83
|
@property
|
|
@@ -152,6 +155,11 @@ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
|
|
|
152
155
|
metadata=self.metadata,
|
|
153
156
|
)
|
|
154
157
|
|
|
158
|
+
def with_reference_replacement(self, source: str, target: Expr):
|
|
159
|
+
if self.address == source:
|
|
160
|
+
return target
|
|
161
|
+
return self
|
|
162
|
+
|
|
155
163
|
|
|
156
164
|
class UndefinedConcept(ConceptRef):
|
|
157
165
|
pass
|
|
@@ -409,7 +417,7 @@ class Grain(Namespaced, BaseModel):
|
|
|
409
417
|
) -> Grain:
|
|
410
418
|
from trilogy.parsing.common import concepts_to_grain_concepts
|
|
411
419
|
|
|
412
|
-
|
|
420
|
+
x = Grain.model_construct(
|
|
413
421
|
components={
|
|
414
422
|
c.address
|
|
415
423
|
for c in concepts_to_grain_concepts(concepts, environment=environment)
|
|
@@ -417,6 +425,8 @@ class Grain(Namespaced, BaseModel):
|
|
|
417
425
|
where_clause=where_clause,
|
|
418
426
|
)
|
|
419
427
|
|
|
428
|
+
return x
|
|
429
|
+
|
|
420
430
|
def with_namespace(self, namespace: str) -> "Grain":
|
|
421
431
|
return Grain.model_construct(
|
|
422
432
|
components={address_with_namespace(c, namespace) for c in self.components},
|
|
@@ -662,6 +672,21 @@ class Comparison(ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
662
672
|
operator=self.operator,
|
|
663
673
|
)
|
|
664
674
|
|
|
675
|
+
def with_reference_replacement(self, source, target):
|
|
676
|
+
return self.__class__.model_construct(
|
|
677
|
+
left=(
|
|
678
|
+
self.left.with_reference_replacement(source, target)
|
|
679
|
+
if isinstance(self.left, Mergeable)
|
|
680
|
+
else self.left
|
|
681
|
+
),
|
|
682
|
+
right=(
|
|
683
|
+
self.right.with_reference_replacement(source, target)
|
|
684
|
+
if isinstance(self.right, Mergeable)
|
|
685
|
+
else self.right
|
|
686
|
+
),
|
|
687
|
+
operator=self.operator,
|
|
688
|
+
)
|
|
689
|
+
|
|
665
690
|
def with_namespace(self, namespace: str):
|
|
666
691
|
return self.__class__.model_construct(
|
|
667
692
|
left=(
|
|
@@ -1233,19 +1258,9 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
|
1233
1258
|
order=self.order,
|
|
1234
1259
|
)
|
|
1235
1260
|
|
|
1236
|
-
@property
|
|
1237
|
-
def output(self):
|
|
1238
|
-
return self.expr.output
|
|
1239
|
-
|
|
1240
1261
|
@property
|
|
1241
1262
|
def concept_arguments(self) -> Sequence[ConceptRef]:
|
|
1242
|
-
|
|
1243
|
-
x = self.expr
|
|
1244
|
-
if isinstance(x, ConceptRef):
|
|
1245
|
-
base += [x]
|
|
1246
|
-
elif isinstance(x, ConceptArgs):
|
|
1247
|
-
base += x.concept_arguments
|
|
1248
|
-
return base
|
|
1263
|
+
return get_concept_arguments(self.expr)
|
|
1249
1264
|
|
|
1250
1265
|
@property
|
|
1251
1266
|
def row_arguments(self) -> Sequence[ConceptRef]:
|
|
@@ -1316,31 +1331,19 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1316
1331
|
|
|
1317
1332
|
@property
|
|
1318
1333
|
def concept_arguments(self) -> List[ConceptRef]:
|
|
1319
|
-
return self.arguments
|
|
1320
|
-
|
|
1321
|
-
@property
|
|
1322
|
-
def arguments(self) -> List[ConceptRef]:
|
|
1323
1334
|
output = [self.content]
|
|
1324
1335
|
for order in self.order_by:
|
|
1325
|
-
output +=
|
|
1336
|
+
output += get_concept_arguments(order)
|
|
1326
1337
|
for item in self.over:
|
|
1327
|
-
output +=
|
|
1338
|
+
output += get_concept_arguments(item)
|
|
1328
1339
|
return output
|
|
1329
1340
|
|
|
1330
|
-
@property
|
|
1331
|
-
def output(self) -> ConceptRef:
|
|
1332
|
-
return self.content
|
|
1333
|
-
|
|
1334
1341
|
@property
|
|
1335
1342
|
def output_datatype(self):
|
|
1336
1343
|
if self.type in (WindowType.RANK, WindowType.ROW_NUMBER):
|
|
1337
1344
|
return DataType.INTEGER
|
|
1338
1345
|
return self.content.output_datatype
|
|
1339
1346
|
|
|
1340
|
-
@property
|
|
1341
|
-
def output_purpose(self):
|
|
1342
|
-
return Purpose.PROPERTY
|
|
1343
|
-
|
|
1344
1347
|
|
|
1345
1348
|
def get_basic_type(
|
|
1346
1349
|
type: DataType | ListType | StructType | MapType | NumericType,
|
|
@@ -1407,12 +1410,28 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
|
1407
1410
|
),
|
|
1408
1411
|
)
|
|
1409
1412
|
|
|
1413
|
+
def with_reference_replacement(self, source, target):
|
|
1414
|
+
return CaseWhen.model_construct(
|
|
1415
|
+
comparison=self.comparison.with_reference_replacement(source, target),
|
|
1416
|
+
expr=(
|
|
1417
|
+
self.expr.with_reference_replacement(source, target)
|
|
1418
|
+
if isinstance(self.expr, Mergeable)
|
|
1419
|
+
else self.expr
|
|
1420
|
+
),
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1410
1423
|
|
|
1411
1424
|
class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
1412
1425
|
expr: "Expr"
|
|
1413
1426
|
# this ensures that it's easily differentiable from CaseWhen
|
|
1414
1427
|
discriminant: ComparisonOperator = ComparisonOperator.ELSE
|
|
1415
1428
|
|
|
1429
|
+
def __str__(self):
|
|
1430
|
+
return self.__repr__()
|
|
1431
|
+
|
|
1432
|
+
def __repr__(self):
|
|
1433
|
+
return f"ELSE {str(self.expr)}"
|
|
1434
|
+
|
|
1416
1435
|
@field_validator("expr", mode="before")
|
|
1417
1436
|
def enforce_expr(cls, v):
|
|
1418
1437
|
if isinstance(v, Concept):
|
|
@@ -1435,6 +1454,19 @@ class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
|
1435
1454
|
),
|
|
1436
1455
|
)
|
|
1437
1456
|
|
|
1457
|
+
def with_reference_replacement(self, source, target):
|
|
1458
|
+
return CaseElse.model_construct(
|
|
1459
|
+
discriminant=self.discriminant,
|
|
1460
|
+
expr=(
|
|
1461
|
+
self.expr.with_reference_replacement(
|
|
1462
|
+
source,
|
|
1463
|
+
target,
|
|
1464
|
+
)
|
|
1465
|
+
if isinstance(self.expr, Mergeable)
|
|
1466
|
+
else self.expr
|
|
1467
|
+
),
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1438
1470
|
def with_namespace(self, namespace: str) -> CaseElse:
|
|
1439
1471
|
return CaseElse.model_construct(
|
|
1440
1472
|
discriminant=self.discriminant,
|
|
@@ -1510,8 +1542,6 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1510
1542
|
|
|
1511
1543
|
def __init__(self, **kwargs):
|
|
1512
1544
|
super().__init__(**kwargs)
|
|
1513
|
-
if "datatype" in str(self):
|
|
1514
|
-
raise SyntaxError(str(self))
|
|
1515
1545
|
|
|
1516
1546
|
def __repr__(self):
|
|
1517
1547
|
return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
|
|
@@ -1597,6 +1627,29 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1597
1627
|
)
|
|
1598
1628
|
return v
|
|
1599
1629
|
|
|
1630
|
+
def with_reference_replacement(self, source: str, target: Expr):
|
|
1631
|
+
return Function.model_construct(
|
|
1632
|
+
operator=self.operator,
|
|
1633
|
+
arguments=[
|
|
1634
|
+
(
|
|
1635
|
+
c.with_reference_replacement(
|
|
1636
|
+
source,
|
|
1637
|
+
target,
|
|
1638
|
+
)
|
|
1639
|
+
if isinstance(
|
|
1640
|
+
c,
|
|
1641
|
+
Mergeable,
|
|
1642
|
+
)
|
|
1643
|
+
else c
|
|
1644
|
+
)
|
|
1645
|
+
for c in self.arguments
|
|
1646
|
+
],
|
|
1647
|
+
output_datatype=self.output_datatype,
|
|
1648
|
+
output_purpose=self.output_purpose,
|
|
1649
|
+
valid_inputs=self.valid_inputs,
|
|
1650
|
+
arg_count=self.arg_count,
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1600
1653
|
def with_namespace(self, namespace: str) -> "Function":
|
|
1601
1654
|
return Function.model_construct(
|
|
1602
1655
|
operator=self.operator,
|
|
@@ -1658,10 +1711,13 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1658
1711
|
return base_grain
|
|
1659
1712
|
|
|
1660
1713
|
|
|
1661
|
-
class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
1714
|
+
class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
|
|
1662
1715
|
function: Function
|
|
1663
1716
|
by: List[ConceptRef] = Field(default_factory=list)
|
|
1664
1717
|
|
|
1718
|
+
def __init__(self, **kwargs):
|
|
1719
|
+
super().__init__(**kwargs)
|
|
1720
|
+
|
|
1665
1721
|
@field_validator("by", mode="before")
|
|
1666
1722
|
@classmethod
|
|
1667
1723
|
def enforce_concept_ref(cls, v):
|
|
@@ -1693,10 +1749,6 @@ class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
|
1693
1749
|
def output_purpose(self):
|
|
1694
1750
|
return self.function.output_purpose
|
|
1695
1751
|
|
|
1696
|
-
@property
|
|
1697
|
-
def arguments(self):
|
|
1698
|
-
return self.function.arguments
|
|
1699
|
-
|
|
1700
1752
|
def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
|
|
1701
1753
|
return AggregateWrapper.model_construct(
|
|
1702
1754
|
function=self.function.with_merge(source, target, modifiers=modifiers),
|
|
@@ -1707,6 +1759,16 @@ class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
|
1707
1759
|
),
|
|
1708
1760
|
)
|
|
1709
1761
|
|
|
1762
|
+
def with_reference_replacement(self, source, target):
|
|
1763
|
+
return AggregateWrapper.model_construct(
|
|
1764
|
+
function=self.function.with_reference_replacement(source, target),
|
|
1765
|
+
by=(
|
|
1766
|
+
[c.with_reference_replacement(source, target) for c in self.by]
|
|
1767
|
+
if self.by
|
|
1768
|
+
else []
|
|
1769
|
+
),
|
|
1770
|
+
)
|
|
1771
|
+
|
|
1710
1772
|
def with_namespace(self, namespace: str) -> "AggregateWrapper":
|
|
1711
1773
|
return AggregateWrapper.model_construct(
|
|
1712
1774
|
function=self.function.with_namespace(namespace),
|
|
@@ -1796,11 +1858,6 @@ class RowsetItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
|
1796
1858
|
rowset=self.rowset.with_namespace(namespace),
|
|
1797
1859
|
)
|
|
1798
1860
|
|
|
1799
|
-
@property
|
|
1800
|
-
def arguments(self) -> List[ConceptRef]:
|
|
1801
|
-
output = [self.content]
|
|
1802
|
-
return output
|
|
1803
|
-
|
|
1804
1861
|
@property
|
|
1805
1862
|
def output(self) -> ConceptRef:
|
|
1806
1863
|
return self.content
|
|
@@ -1831,7 +1888,10 @@ class OrderBy(Mergeable, Namespaced, BaseModel):
|
|
|
1831
1888
|
|
|
1832
1889
|
@property
|
|
1833
1890
|
def concept_arguments(self):
|
|
1834
|
-
|
|
1891
|
+
base = []
|
|
1892
|
+
for x in self.items:
|
|
1893
|
+
base += x.concept_arguments
|
|
1894
|
+
return base
|
|
1835
1895
|
|
|
1836
1896
|
|
|
1837
1897
|
class AlignClause(Namespaced, BaseModel):
|
|
@@ -2088,6 +2148,11 @@ class Comment(BaseModel):
|
|
|
2088
2148
|
text: str
|
|
2089
2149
|
|
|
2090
2150
|
|
|
2151
|
+
class ArgBinding(BaseModel):
|
|
2152
|
+
name: str
|
|
2153
|
+
default: Expr | None = None
|
|
2154
|
+
|
|
2155
|
+
|
|
2091
2156
|
Expr = (
|
|
2092
2157
|
MagicConstants
|
|
2093
2158
|
| bool
|
trilogy/core/models/build.py
CHANGED
|
@@ -1639,9 +1639,26 @@ class Factory:
|
|
|
1639
1639
|
|
|
1640
1640
|
@build.register
|
|
1641
1641
|
def _(self, base: Comparison) -> BuildComparison:
|
|
1642
|
+
from trilogy.parsing.common import arbitrary_to_concept
|
|
1643
|
+
|
|
1644
|
+
left = base.left
|
|
1645
|
+
if isinstance(left, AggregateWrapper):
|
|
1646
|
+
left_c = arbitrary_to_concept(
|
|
1647
|
+
left,
|
|
1648
|
+
environment=self.environment,
|
|
1649
|
+
)
|
|
1650
|
+
left = left_c # type: ignore
|
|
1651
|
+
right = base.right
|
|
1652
|
+
if isinstance(right, AggregateWrapper):
|
|
1653
|
+
right_c = arbitrary_to_concept(
|
|
1654
|
+
right,
|
|
1655
|
+
environment=self.environment,
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
right = right_c # type: ignore
|
|
1642
1659
|
return BuildComparison.model_construct(
|
|
1643
|
-
left=(self.build(
|
|
1644
|
-
right=(self.build(
|
|
1660
|
+
left=(self.build(left)),
|
|
1661
|
+
right=(self.build(right)),
|
|
1645
1662
|
operator=base.operator,
|
|
1646
1663
|
)
|
|
1647
1664
|
|
|
@@ -1660,7 +1677,7 @@ class Factory:
|
|
|
1660
1677
|
)
|
|
1661
1678
|
|
|
1662
1679
|
@build.register
|
|
1663
|
-
def _(self, base: RowsetItem):
|
|
1680
|
+
def _(self, base: RowsetItem) -> BuildRowsetItem:
|
|
1664
1681
|
|
|
1665
1682
|
factory = Factory(
|
|
1666
1683
|
environment=self.environment,
|
|
@@ -117,6 +117,12 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
117
117
|
where: Optional[WhereClause] = None
|
|
118
118
|
non_partial_for: Optional[WhereClause] = None
|
|
119
119
|
|
|
120
|
+
@property
|
|
121
|
+
def safe_address(self) -> str:
|
|
122
|
+
if isinstance(self.address, Address):
|
|
123
|
+
return self.address.location
|
|
124
|
+
return self.address
|
|
125
|
+
|
|
120
126
|
def __eq__(self, other):
|
|
121
127
|
if not isinstance(other, Datasource):
|
|
122
128
|
return False
|
|
@@ -9,6 +9,7 @@ from typing import (
|
|
|
9
9
|
TYPE_CHECKING,
|
|
10
10
|
Annotated,
|
|
11
11
|
Any,
|
|
12
|
+
Callable,
|
|
12
13
|
Dict,
|
|
13
14
|
ItemsView,
|
|
14
15
|
List,
|
|
@@ -189,7 +190,7 @@ class Environment(BaseModel):
|
|
|
189
190
|
datasources: Annotated[
|
|
190
191
|
EnvironmentDatasourceDict, PlainValidator(validate_datasources)
|
|
191
192
|
] = Field(default_factory=EnvironmentDatasourceDict)
|
|
192
|
-
functions: Dict[str,
|
|
193
|
+
functions: Dict[str, Callable[..., Any]] = Field(default_factory=dict)
|
|
193
194
|
data_types: Dict[str, DataType] = Field(default_factory=dict)
|
|
194
195
|
named_statements: Dict[str, SelectLineage] = Field(default_factory=dict)
|
|
195
196
|
imports: Dict[str, list[Import]] = Field(
|
|
@@ -37,7 +37,7 @@ def gen_group_node(
|
|
|
37
37
|
resolve_function_parent_concepts(concept, environment=environment), "address"
|
|
38
38
|
)
|
|
39
39
|
logger.info(
|
|
40
|
-
f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
|
|
40
|
+
f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} {concept.lineage} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
# if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
|
|
@@ -76,7 +76,7 @@ def gen_multiselect_node(
|
|
|
76
76
|
for select in lineage.selects:
|
|
77
77
|
|
|
78
78
|
snode: StrategyNode = get_query_node(history.base_environment, select)
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
logger.info(
|
|
81
81
|
f"{padding(depth)}{LOGGER_PREFIX} Fetched parent node with outputs {select.output_components}"
|
|
82
82
|
)
|
|
@@ -127,7 +127,9 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
|
|
|
127
127
|
order_by=order_by,
|
|
128
128
|
meta=meta or Metadata(),
|
|
129
129
|
)
|
|
130
|
+
|
|
130
131
|
output.grain = output.calculate_grain(environment)
|
|
132
|
+
|
|
131
133
|
for x in selection:
|
|
132
134
|
|
|
133
135
|
if x.is_undefined and environment.concepts.fail_on_missing:
|
trilogy/dialect/config.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
try:
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
except ImportError:
|
|
7
|
+
DataFrame = Any
|
|
8
|
+
|
|
9
|
+
|
|
1
10
|
class DialectConfig:
|
|
2
11
|
def __init__(self):
|
|
3
12
|
pass
|
|
@@ -104,3 +113,9 @@ class TrinoConfig(PrestoConfig):
|
|
|
104
113
|
if self.schema:
|
|
105
114
|
return f"trino://{self.username}:{self.password}@{self.host}:{self.port}/{self.catalog}/{self.schema}"
|
|
106
115
|
return f"trino://{self.username}:{self.password}@{self.host}:{self.port}/{self.catalog}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DataFrameConfig(DuckDBConfig):
|
|
119
|
+
def __init__(self, dataframes: dict[str, "DataFrame"]):
|
|
120
|
+
super().__init__()
|
|
121
|
+
self.dataframes = dataframes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import text
|
|
4
|
+
|
|
5
|
+
from trilogy.core.models.environment import Environment
|
|
6
|
+
from trilogy.dialect.duckdb import DuckDBDialect
|
|
7
|
+
from trilogy.engine import ExecutionEngine
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
try:
|
|
11
|
+
from pandas import DataFrame
|
|
12
|
+
except ImportError:
|
|
13
|
+
DataFrame = Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataframeDialect(DuckDBDialect):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DataframeConnectionWrapper(ExecutionEngine):
|
|
21
|
+
def __init__(self, engine: ExecutionEngine, dataframes: dict[str, "DataFrame"]):
|
|
22
|
+
self.engine = engine
|
|
23
|
+
self.dataframes = dataframes
|
|
24
|
+
self.connection = None
|
|
25
|
+
|
|
26
|
+
def setup(self, env: Environment, connection):
|
|
27
|
+
self._register_dataframes(env, connection)
|
|
28
|
+
|
|
29
|
+
def _register_dataframes(self, env: Environment, connection):
|
|
30
|
+
for ds in env.datasources.values():
|
|
31
|
+
if ds.safe_address in self.dataframes:
|
|
32
|
+
connection.execute(
|
|
33
|
+
text("register(:name, :df)"),
|
|
34
|
+
{"name": ds.safe_address, "df": self.dataframes[ds.safe_address]},
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Dataframe {ds.safe_address} not found in dataframes on connection config, have {self.dataframes.keys()}"
|
|
39
|
+
)
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def add_dataframe(self, name: str, df: "DataFrame", connection, env: Environment):
|
|
43
|
+
self.dataframes[name] = df
|
|
44
|
+
self._register_dataframes(env, connection)
|
|
45
|
+
|
|
46
|
+
def connect(self) -> Any:
|
|
47
|
+
return self.engine.connect()
|
trilogy/dialect/enums.py
CHANGED
|
@@ -16,7 +16,7 @@ def default_factory(conf: DialectConfig, config_type):
|
|
|
16
16
|
|
|
17
17
|
if not isinstance(conf, config_type):
|
|
18
18
|
raise TypeError(
|
|
19
|
-
f"Invalid dialect configuration for type {type(config_type).__name__}"
|
|
19
|
+
f"Invalid dialect configuration for type {type(config_type).__name__}, is {type(conf)}"
|
|
20
20
|
)
|
|
21
21
|
if conf.connect_args:
|
|
22
22
|
return create_engine(
|
|
@@ -33,6 +33,7 @@ class Dialects(Enum):
|
|
|
33
33
|
TRINO = "trino"
|
|
34
34
|
POSTGRES = "postgres"
|
|
35
35
|
SNOWFLAKE = "snowflake"
|
|
36
|
+
DATAFRAME = "dataframe"
|
|
36
37
|
|
|
37
38
|
@classmethod
|
|
38
39
|
def _missing_(cls, value):
|
|
@@ -88,6 +89,16 @@ class Dialects(Enum):
|
|
|
88
89
|
from trilogy.dialect.config import TrinoConfig
|
|
89
90
|
|
|
90
91
|
return _engine_factory(conf, TrinoConfig)
|
|
92
|
+
elif self == Dialects.DATAFRAME:
|
|
93
|
+
from trilogy.dialect.config import DataFrameConfig
|
|
94
|
+
from trilogy.dialect.dataframe import DataframeConnectionWrapper
|
|
95
|
+
|
|
96
|
+
if not conf:
|
|
97
|
+
conf = DataFrameConfig(dataframes={})
|
|
98
|
+
|
|
99
|
+
base = _engine_factory(conf, DataFrameConfig)
|
|
100
|
+
|
|
101
|
+
return DataframeConnectionWrapper(base, dataframes=conf.dataframes)
|
|
91
102
|
else:
|
|
92
103
|
raise ValueError(
|
|
93
104
|
f"Unsupported dialect {self} for default engine creation; create one explicitly."
|
trilogy/engine.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from typing import Protocol
|
|
1
|
+
from typing import Any, Protocol
|
|
2
2
|
|
|
3
3
|
from sqlalchemy.engine import Connection, CursorResult, Engine
|
|
4
4
|
|
|
5
|
+
from trilogy.core.models.environment import Environment
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class EngineResult(Protocol):
|
|
7
9
|
pass
|
|
@@ -13,7 +15,7 @@ class EngineResult(Protocol):
|
|
|
13
15
|
class EngineConnection(Protocol):
|
|
14
16
|
pass
|
|
15
17
|
|
|
16
|
-
def execute(self, statement: str) -> EngineResult:
|
|
18
|
+
def execute(self, statement: str, parameters: Any | None = None) -> EngineResult:
|
|
17
19
|
pass
|
|
18
20
|
|
|
19
21
|
|
|
@@ -23,6 +25,9 @@ class ExecutionEngine(Protocol):
|
|
|
23
25
|
def connect(self) -> EngineConnection:
|
|
24
26
|
pass
|
|
25
27
|
|
|
28
|
+
def setup(self, env: Environment, connection):
|
|
29
|
+
pass
|
|
30
|
+
|
|
26
31
|
|
|
27
32
|
### Begin default SQLAlchemy implementation
|
|
28
33
|
class SqlAlchemyResult(EngineResult):
|
|
@@ -37,8 +42,10 @@ class SqlAlchemyConnection(EngineConnection):
|
|
|
37
42
|
def __init__(self, connection: Connection):
|
|
38
43
|
self.connection = connection
|
|
39
44
|
|
|
40
|
-
def execute(
|
|
41
|
-
|
|
45
|
+
def execute(
|
|
46
|
+
self, statement: str, parameters: Any | None = None
|
|
47
|
+
) -> SqlAlchemyResult:
|
|
48
|
+
return SqlAlchemyResult(self.connection.execute(statement, parameters))
|
|
42
49
|
|
|
43
50
|
|
|
44
51
|
class SqlAlchemyEngine(ExecutionEngine):
|
trilogy/executor.py
CHANGED
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, Generator, List, Optional, Protocol
|
|
5
5
|
|
|
6
6
|
from sqlalchemy import text
|
|
7
|
-
from sqlalchemy.engine import CursorResult
|
|
7
|
+
from sqlalchemy.engine import CursorResult
|
|
8
8
|
|
|
9
9
|
from trilogy.constants import logger
|
|
10
10
|
from trilogy.core.enums import FunctionType, Granularity, IOType
|
|
@@ -33,6 +33,7 @@ from trilogy.core.statements.execute import (
|
|
|
33
33
|
)
|
|
34
34
|
from trilogy.dialect.base import BaseDialect
|
|
35
35
|
from trilogy.dialect.enums import Dialects
|
|
36
|
+
from trilogy.engine import ExecutionEngine
|
|
36
37
|
from trilogy.hooks.base_hook import BaseHook
|
|
37
38
|
from trilogy.parser import parse_text
|
|
38
39
|
|
|
@@ -71,7 +72,7 @@ class Executor(object):
|
|
|
71
72
|
def __init__(
|
|
72
73
|
self,
|
|
73
74
|
dialect: Dialects,
|
|
74
|
-
engine:
|
|
75
|
+
engine: ExecutionEngine,
|
|
75
76
|
environment: Optional[Environment] = None,
|
|
76
77
|
hooks: List[BaseHook] | None = None,
|
|
77
78
|
):
|
|
@@ -109,9 +110,16 @@ class Executor(object):
|
|
|
109
110
|
from trilogy.dialect.snowflake import SnowflakeDialect
|
|
110
111
|
|
|
111
112
|
self.generator = SnowflakeDialect()
|
|
113
|
+
elif self.dialect == Dialects.DATAFRAME:
|
|
114
|
+
from trilogy.dialect.dataframe import DataframeDialect
|
|
115
|
+
|
|
116
|
+
self.generator = DataframeDialect()
|
|
112
117
|
else:
|
|
113
118
|
raise ValueError(f"Unsupported dialect {self.dialect}")
|
|
114
119
|
self.connection = self.engine.connect()
|
|
120
|
+
# TODO: make generic
|
|
121
|
+
if self.dialect == Dialects.DATAFRAME:
|
|
122
|
+
self.engine.setup(self.environment, self.connection)
|
|
115
123
|
|
|
116
124
|
def execute_statement(self, statement) -> Optional[CursorResult]:
|
|
117
125
|
if not isinstance(
|
trilogy/parsing/common.py
CHANGED
|
@@ -46,6 +46,62 @@ from trilogy.core.statements.author import RowsetDerivationStatement, SelectStat
|
|
|
46
46
|
from trilogy.utility import string_to_hash, unique
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def process_function_arg(
|
|
50
|
+
arg,
|
|
51
|
+
meta: Meta | None,
|
|
52
|
+
environment: Environment,
|
|
53
|
+
):
|
|
54
|
+
# if a function has an anonymous function argument
|
|
55
|
+
# create an implicit concept
|
|
56
|
+
if isinstance(arg, Parenthetical):
|
|
57
|
+
processed = process_function_args([arg.content], meta, environment)
|
|
58
|
+
return Function(
|
|
59
|
+
operator=FunctionType.PARENTHETICAL,
|
|
60
|
+
arguments=processed,
|
|
61
|
+
output_datatype=arg_to_datatype(processed[0]),
|
|
62
|
+
output_purpose=function_args_to_output_purpose(processed),
|
|
63
|
+
)
|
|
64
|
+
elif isinstance(arg, Function):
|
|
65
|
+
# if it's not an aggregate function, we can skip the virtual concepts
|
|
66
|
+
# to simplify anonymous function handling
|
|
67
|
+
if (
|
|
68
|
+
arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
69
|
+
and arg.operator != FunctionType.UNNEST
|
|
70
|
+
):
|
|
71
|
+
return arg
|
|
72
|
+
id_hash = string_to_hash(str(arg))
|
|
73
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_{arg.operator.value}_{id_hash}"
|
|
74
|
+
if f"{environment.namespace}.{name}" in environment.concepts:
|
|
75
|
+
return environment.concepts[f"{environment.namespace}.{name}"]
|
|
76
|
+
concept = function_to_concept(
|
|
77
|
+
arg,
|
|
78
|
+
name=name,
|
|
79
|
+
environment=environment,
|
|
80
|
+
)
|
|
81
|
+
# to satisfy mypy, concept will always have metadata
|
|
82
|
+
if concept.metadata and meta:
|
|
83
|
+
concept.metadata.line_number = meta.line
|
|
84
|
+
environment.add_concept(concept, meta=meta)
|
|
85
|
+
return concept
|
|
86
|
+
elif isinstance(
|
|
87
|
+
arg, (FilterItem, WindowItem, AggregateWrapper, ListWrapper, MapWrapper)
|
|
88
|
+
):
|
|
89
|
+
id_hash = string_to_hash(str(arg))
|
|
90
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}"
|
|
91
|
+
if f"{environment.namespace}.{name}" in environment.concepts:
|
|
92
|
+
return environment.concepts[f"{environment.namespace}.{name}"]
|
|
93
|
+
concept = arbitrary_to_concept(
|
|
94
|
+
arg,
|
|
95
|
+
name=name,
|
|
96
|
+
environment=environment,
|
|
97
|
+
)
|
|
98
|
+
if concept.metadata and meta:
|
|
99
|
+
concept.metadata.line_number = meta.line
|
|
100
|
+
environment.add_concept(concept, meta=meta)
|
|
101
|
+
return concept
|
|
102
|
+
return arg
|
|
103
|
+
|
|
104
|
+
|
|
49
105
|
def process_function_args(
|
|
50
106
|
args,
|
|
51
107
|
meta: Meta | None,
|
|
@@ -53,54 +109,7 @@ def process_function_args(
|
|
|
53
109
|
) -> List[Concept | Function | str | int | float | date | datetime]:
|
|
54
110
|
final: List[Concept | Function | str | int | float | date | datetime] = []
|
|
55
111
|
for arg in args:
|
|
56
|
-
|
|
57
|
-
# create an implicit concept
|
|
58
|
-
if isinstance(arg, Parenthetical):
|
|
59
|
-
processed = process_function_args([arg.content], meta, environment)
|
|
60
|
-
final.append(
|
|
61
|
-
Function(
|
|
62
|
-
operator=FunctionType.PARENTHETICAL,
|
|
63
|
-
arguments=processed,
|
|
64
|
-
output_datatype=arg_to_datatype(processed[0]),
|
|
65
|
-
output_purpose=function_args_to_output_purpose(processed),
|
|
66
|
-
)
|
|
67
|
-
)
|
|
68
|
-
elif isinstance(arg, Function):
|
|
69
|
-
# if it's not an aggregate function, we can skip the virtual concepts
|
|
70
|
-
# to simplify anonymous function handling
|
|
71
|
-
if (
|
|
72
|
-
arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
73
|
-
and arg.operator != FunctionType.UNNEST
|
|
74
|
-
):
|
|
75
|
-
final.append(arg)
|
|
76
|
-
continue
|
|
77
|
-
id_hash = string_to_hash(str(arg))
|
|
78
|
-
concept = function_to_concept(
|
|
79
|
-
arg,
|
|
80
|
-
name=f"{VIRTUAL_CONCEPT_PREFIX}_{arg.operator.value}_{id_hash}",
|
|
81
|
-
environment=environment,
|
|
82
|
-
)
|
|
83
|
-
# to satisfy mypy, concept will always have metadata
|
|
84
|
-
if concept.metadata and meta:
|
|
85
|
-
concept.metadata.line_number = meta.line
|
|
86
|
-
environment.add_concept(concept, meta=meta)
|
|
87
|
-
final.append(concept)
|
|
88
|
-
elif isinstance(
|
|
89
|
-
arg, (FilterItem, WindowItem, AggregateWrapper, ListWrapper, MapWrapper)
|
|
90
|
-
):
|
|
91
|
-
id_hash = string_to_hash(str(arg))
|
|
92
|
-
concept = arbitrary_to_concept(
|
|
93
|
-
arg,
|
|
94
|
-
name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
|
|
95
|
-
environment=environment,
|
|
96
|
-
)
|
|
97
|
-
if concept.metadata and meta:
|
|
98
|
-
concept.metadata.line_number = meta.line
|
|
99
|
-
environment.add_concept(concept, meta=meta)
|
|
100
|
-
final.append(concept)
|
|
101
|
-
|
|
102
|
-
else:
|
|
103
|
-
final.append(arg)
|
|
112
|
+
final.append(process_function_arg(arg, meta, environment))
|
|
104
113
|
return final
|
|
105
114
|
|
|
106
115
|
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -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 List, Optional, Tuple, Union
|
|
7
|
+
from typing import Callable, List, Optional, Tuple, Union
|
|
8
8
|
|
|
9
9
|
from lark import Lark, ParseTree, Transformer, Tree, v_args
|
|
10
10
|
from lark.exceptions import (
|
|
@@ -49,6 +49,7 @@ from trilogy.core.models.author import (
|
|
|
49
49
|
AggregateWrapper,
|
|
50
50
|
AlignClause,
|
|
51
51
|
AlignItem,
|
|
52
|
+
ArgBinding,
|
|
52
53
|
CaseElse,
|
|
53
54
|
CaseWhen,
|
|
54
55
|
Comment,
|
|
@@ -61,6 +62,7 @@ from trilogy.core.models.author import (
|
|
|
61
62
|
Function,
|
|
62
63
|
Grain,
|
|
63
64
|
HavingClause,
|
|
65
|
+
Mergeable,
|
|
64
66
|
Metadata,
|
|
65
67
|
OrderBy,
|
|
66
68
|
OrderItem,
|
|
@@ -1075,27 +1077,45 @@ class ParseToObjects(Transformer):
|
|
|
1075
1077
|
return HavingClause(conditional=root)
|
|
1076
1078
|
|
|
1077
1079
|
@v_args(meta=True)
|
|
1078
|
-
def function_binding_list(self, meta: Meta, args) ->
|
|
1080
|
+
def function_binding_list(self, meta: Meta, args) -> list[ArgBinding]:
|
|
1079
1081
|
return args
|
|
1080
1082
|
|
|
1081
1083
|
@v_args(meta=True)
|
|
1082
|
-
def function_binding_item(self, meta: Meta, args) ->
|
|
1083
|
-
|
|
1084
|
+
def function_binding_item(self, meta: Meta, args) -> ArgBinding:
|
|
1085
|
+
if len(args) == 2:
|
|
1086
|
+
return ArgBinding(name=args[0], default=args[1])
|
|
1087
|
+
return ArgBinding(name=args[0], default=None)
|
|
1084
1088
|
|
|
1085
1089
|
@v_args(meta=True)
|
|
1086
|
-
def raw_function(self, meta: Meta, args) ->
|
|
1090
|
+
def raw_function(self, meta: Meta, args) -> Callable[[list[Expr]], Expr]:
|
|
1087
1091
|
identity = args[0]
|
|
1088
|
-
|
|
1092
|
+
function_arguments: list[ArgBinding] = args[1]
|
|
1089
1093
|
output = args[2]
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1094
|
+
|
|
1095
|
+
def function_factory(*creation_args: list[Expr]):
|
|
1096
|
+
nout = output.copy(deep=True)
|
|
1097
|
+
creation_arg_list: list[Expr] = list(creation_args)
|
|
1098
|
+
if len(creation_args) < len(function_arguments):
|
|
1099
|
+
for binding in function_arguments[len(creation_arg_list) :]:
|
|
1100
|
+
if binding.default is None:
|
|
1101
|
+
raise ValueError(f"Missing argument {binding.name}")
|
|
1102
|
+
creation_arg_list.append(binding.default)
|
|
1103
|
+
if isinstance(nout, Mergeable):
|
|
1104
|
+
for idx, x in enumerate(creation_arg_list):
|
|
1105
|
+
# these will always be local namespace
|
|
1106
|
+
nout = nout.with_reference_replacement(
|
|
1107
|
+
f"{DEFAULT_NAMESPACE}.{function_arguments[idx].name}", x
|
|
1108
|
+
)
|
|
1109
|
+
return nout
|
|
1110
|
+
|
|
1111
|
+
self.environment.functions[identity] = function_factory
|
|
1112
|
+
return function_factory
|
|
1113
|
+
|
|
1114
|
+
def custom_function(self, args):
|
|
1115
|
+
name = args[0]
|
|
1116
|
+
args = args[1:]
|
|
1117
|
+
remapped = self.environment.functions[name](*args)
|
|
1118
|
+
return remapped
|
|
1099
1119
|
|
|
1100
1120
|
@v_args(meta=True)
|
|
1101
1121
|
def function(self, meta: Meta, args) -> Function:
|
|
@@ -1141,24 +1161,24 @@ class ParseToObjects(Transformer):
|
|
|
1141
1161
|
def comparison(self, args) -> Comparison:
|
|
1142
1162
|
if args[1] == ComparisonOperator.IN:
|
|
1143
1163
|
raise SyntaxError
|
|
1144
|
-
if isinstance(args[0], AggregateWrapper):
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
else:
|
|
1152
|
-
|
|
1153
|
-
if isinstance(args[2], AggregateWrapper):
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
else:
|
|
1161
|
-
|
|
1164
|
+
# if isinstance(args[0], AggregateWrapper):
|
|
1165
|
+
# left_c = arbitrary_to_concept(
|
|
1166
|
+
# args[0],
|
|
1167
|
+
# environment=self.environment,
|
|
1168
|
+
# )
|
|
1169
|
+
# self.environment.add_concept(left_c)
|
|
1170
|
+
# left = left_c.reference
|
|
1171
|
+
# else:
|
|
1172
|
+
left = args[0]
|
|
1173
|
+
# if isinstance(args[2], AggregateWrapper):
|
|
1174
|
+
# right_c = arbitrary_to_concept(
|
|
1175
|
+
# args[2],
|
|
1176
|
+
# environment=self.environment,
|
|
1177
|
+
# )
|
|
1178
|
+
# self.environment.add_concept(right_c)
|
|
1179
|
+
# right = right_c.reference
|
|
1180
|
+
# else:
|
|
1181
|
+
right = args[2]
|
|
1162
1182
|
return Comparison(left=left, right=right, operator=args[1])
|
|
1163
1183
|
|
|
1164
1184
|
def between_comparison(self, args) -> Conditional:
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
|
|
90
90
|
// FUNCTION blocks
|
|
91
91
|
function: raw_function
|
|
92
|
-
function_binding_item: IDENTIFIER "
|
|
93
|
-
function_binding_list: function_binding_item
|
|
94
|
-
raw_function: "
|
|
92
|
+
function_binding_item: IDENTIFIER ("=" literal)?
|
|
93
|
+
function_binding_list: (function_binding_item ",")* function_binding_item ","?
|
|
94
|
+
raw_function: "def" IDENTIFIER "(" function_binding_list ")" "->" expr
|
|
95
95
|
|
|
96
96
|
// user_id where state = Mexico
|
|
97
97
|
_filter_alt: IDENTIFIER "?" conditional
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
map_key_access: expr "[" string_lit "]"
|
|
179
179
|
attr_access: expr "." string_lit
|
|
180
180
|
|
|
181
|
-
expr: _constant_functions | window_item | filter_item | subselect_comparison | between_comparison | fgroup | aggregate_functions | unnest | union | _static_functions | literal | concept_lit | index_access | map_key_access | attr_access | parenthetical | expr_tuple | comparison | alt_like
|
|
181
|
+
expr: _constant_functions | window_item | filter_item | subselect_comparison | between_comparison | fgroup | aggregate_functions | unnest | union | _static_functions | literal | concept_lit | index_access | map_key_access | attr_access | parenthetical | expr_tuple | comparison | alt_like | custom_function
|
|
182
182
|
|
|
183
183
|
// functions
|
|
184
184
|
|
|
@@ -297,6 +297,8 @@
|
|
|
297
297
|
|
|
298
298
|
_static_functions: _string_functions | _math_functions | _generic_functions | _date_functions
|
|
299
299
|
|
|
300
|
+
custom_function: "@" IDENTIFIER "(" (expr ",")* expr ")"
|
|
301
|
+
|
|
300
302
|
// base language constructs
|
|
301
303
|
concept_lit: IDENTIFIER
|
|
302
304
|
IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\.]*/
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|