pytrilogy 0.0.3.33__py3-none-any.whl → 0.0.3.35__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.33.dist-info → pytrilogy-0.0.3.35.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.33.dist-info → pytrilogy-0.0.3.35.dist-info}/RECORD +23 -23
- trilogy/__init__.py +1 -1
- trilogy/authoring/__init__.py +6 -0
- trilogy/core/functions.py +17 -4
- trilogy/core/models/author.py +21 -5
- trilogy/core/models/build.py +35 -4
- trilogy/core/models/build_environment.py +6 -13
- trilogy/core/models/environment.py +2 -1
- trilogy/core/models/execute.py +8 -0
- trilogy/core/processing/concept_strategies_v3.py +17 -1
- trilogy/core/processing/node_generators/select_merge_node.py +2 -0
- trilogy/core/processing/utility.py +11 -5
- trilogy/core/statements/author.py +5 -1
- trilogy/parsing/common.py +1 -1
- trilogy/parsing/parse_engine.py +24 -0
- trilogy/parsing/render.py +6 -0
- trilogy/parsing/trilogy.lark +3 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/dashboard.preql +0 -5
- {pytrilogy-0.0.3.33.dist-info → pytrilogy-0.0.3.35.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.33.dist-info → pytrilogy-0.0.3.35.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.33.dist-info → pytrilogy-0.0.3.35.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.33.dist-info → pytrilogy-0.0.3.35.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
pytrilogy-0.0.3.
|
|
2
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
pytrilogy-0.0.3.35.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=XKMFtr6LPZcLEaFqOMZ_Bx_NXbihLoadMcLr5wNv0KM,303
|
|
3
3
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
trilogy/constants.py,sha256=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
|
|
5
5
|
trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
|
|
@@ -8,7 +8,7 @@ trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
|
|
|
8
8
|
trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
|
|
10
10
|
trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
|
|
11
|
-
trilogy/authoring/__init__.py,sha256=
|
|
11
|
+
trilogy/authoring/__init__.py,sha256=v9PRuZs4fTnxhpXAnwTxCDwlLasUax6g2FONidcujR4,2369
|
|
12
12
|
trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
|
|
14
14
|
trilogy/core/enums.py,sha256=fWexUZtssfvP5TiD7eQ66Q_tPUCNCCTGNSzLbVXrnqQ,7358
|
|
@@ -16,28 +16,28 @@ trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0
|
|
|
16
16
|
trilogy/core/environment_helpers.py,sha256=5ayyhf4CGBlg_LssPu3DbS_H9H1Kq6Qog5TgR8pwyMk,8518
|
|
17
17
|
trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
|
|
18
18
|
trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,673
|
|
19
|
-
trilogy/core/functions.py,sha256=
|
|
19
|
+
trilogy/core/functions.py,sha256=YjPbZtfQauY6u7puehFLmPsauB5IuGa5xGETKDAgHrU,27396
|
|
20
20
|
trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
|
|
21
21
|
trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
|
|
22
22
|
trilogy/core/optimization.py,sha256=aihzx4-2-mSjx5td1TDTYGvc7e9Zvy-_xEyhPqLS-Ig,8314
|
|
23
23
|
trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
|
|
24
24
|
trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
trilogy/core/models/author.py,sha256=
|
|
26
|
-
trilogy/core/models/build.py,sha256=
|
|
27
|
-
trilogy/core/models/build_environment.py,sha256=
|
|
25
|
+
trilogy/core/models/author.py,sha256=rb-OVaCVBlaE08JhWNFK2sMi9r7yGBT2QK97JE0oKJg,76950
|
|
26
|
+
trilogy/core/models/build.py,sha256=6Mjq4NoBqk5U7IWg8Fbb6AAOdMHazVymfYXOm-wcWv4,59346
|
|
27
|
+
trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
|
|
28
28
|
trilogy/core/models/core.py,sha256=wx6hJcFECMG-Ij972ADNkr-3nFXkYESr82ObPiC46_U,10875
|
|
29
29
|
trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
|
|
30
|
-
trilogy/core/models/environment.py,sha256=
|
|
31
|
-
trilogy/core/models/execute.py,sha256=
|
|
30
|
+
trilogy/core/models/environment.py,sha256=axgk7W3STy5EIrG8fUwl2oh6WCqeBAr7PWy6EOe-_Dc,27002
|
|
31
|
+
trilogy/core/models/execute.py,sha256=mQm5Gydo2Ph0W7w9wm5dQEarS04PC-IKAgNVsdqOZsQ,34524
|
|
32
32
|
trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
|
|
33
33
|
trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
|
|
34
34
|
trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52doLU9sIg3pmU2_S08,1332
|
|
35
35
|
trilogy/core/optimizations/inline_datasource.py,sha256=AHuTGh2x0GQ8usOe0NiFncfTFQ_KogdgDl4uucmhIbI,4241
|
|
36
36
|
trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
|
|
37
37
|
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
trilogy/core/processing/concept_strategies_v3.py,sha256=
|
|
38
|
+
trilogy/core/processing/concept_strategies_v3.py,sha256=m24X5FOOcLTCvY1MY1yUK5qFcWwokMWZ5cAFI2YN9G8,43352
|
|
39
39
|
trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
|
|
40
|
-
trilogy/core/processing/utility.py,sha256=
|
|
40
|
+
trilogy/core/processing/utility.py,sha256=3tC__aT76EzcnIexZfOqCH-3WzvPiCAYWs9TBoMvGjc,20903
|
|
41
41
|
trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW6gjUJCsiXumdbJNozHUf-Y,800
|
|
42
42
|
trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
|
|
43
43
|
trilogy/core/processing/node_generators/common.py,sha256=ZsDzThjm_mAtdQpKAg8QIJiPVZ4KuUkKyilj4eOhSDs,9439
|
|
@@ -47,7 +47,7 @@ trilogy/core/processing/node_generators/group_to_node.py,sha256=E5bEjovSx422d_Ml
|
|
|
47
47
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
|
|
48
48
|
trilogy/core/processing/node_generators/node_merge_node.py,sha256=sv55oynfqgpHEpo1OEtVDri-5fywzPhDlR85qaWikvY,16195
|
|
49
49
|
trilogy/core/processing/node_generators/rowset_node.py,sha256=YmBs6ZQ7azLXRFEmeoecpGjK4pMHsUCovuBxfb3UKZI,6848
|
|
50
|
-
trilogy/core/processing/node_generators/select_merge_node.py,sha256=
|
|
50
|
+
trilogy/core/processing/node_generators/select_merge_node.py,sha256=lxXhMhDKGbu67QFNbbAT-BO8gbWppIvjn_hAXpLEPe0,19953
|
|
51
51
|
trilogy/core/processing/node_generators/select_node.py,sha256=Y-zO0AFkTrpi2LyebjpyHU7WWANr7nKZSS9rY7DH4Wo,1888
|
|
52
52
|
trilogy/core/processing/node_generators/synonym_node.py,sha256=9LHK2XHDjbyTLjmDQieskG8fqbiSpRnFOkfrutDnOTE,2258
|
|
53
53
|
trilogy/core/processing/node_generators/union_node.py,sha256=zuMSmgF170vzlp2BBQEhKbqUMjVl2xQDqUB82Dhv-VU,2536
|
|
@@ -65,7 +65,7 @@ trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ
|
|
|
65
65
|
trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
|
|
66
66
|
trilogy/core/processing/nodes/window_node.py,sha256=JXJ0iVRlSEM2IBr1TANym2RaUf_p5E_l2sNykRzXWDo,1710
|
|
67
67
|
trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
-
trilogy/core/statements/author.py,sha256=
|
|
68
|
+
trilogy/core/statements/author.py,sha256=dOYX2fJ9VHZgP9RhgK7Ssaq4W5vYtMJBkeiZlwqAoDM,14726
|
|
69
69
|
trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
70
|
trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
|
|
71
71
|
trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
|
|
@@ -87,23 +87,23 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
|
|
|
87
87
|
trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
|
|
88
88
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
89
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
trilogy/parsing/common.py,sha256=
|
|
90
|
+
trilogy/parsing/common.py,sha256=0fRM9_9k3Y24oanSTgHHjVI6LSTAaxTvkaq5ESVWR5o,23541
|
|
91
91
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
92
92
|
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
93
93
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
94
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
95
|
-
trilogy/parsing/render.py,sha256=
|
|
96
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
94
|
+
trilogy/parsing/parse_engine.py,sha256=2zUqtpEyTaxNUz4L3IkA9RKHEFif7VDjxF0Y7sFigts,63506
|
|
95
|
+
trilogy/parsing/render.py,sha256=hI4y-xjXrEXvHslY2l2TQ8ic0zAOpN41ADH37J2_FZY,19047
|
|
96
|
+
trilogy/parsing/trilogy.lark,sha256=ErSKUy2sqpmc3OnflRbQnzTeE81y3ISfUJBD_wvypFM,13159
|
|
97
97
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
98
|
trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
|
|
99
99
|
trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
100
|
-
trilogy/std/dashboard.preql,sha256=eJJTs3xPjYKmp-U5SUxCXAAbp55NlmmC3kECaNg4ya4,128
|
|
101
100
|
trilogy/std/date.preql,sha256=0MHeGLp2mG4QBKtmozcBZ7qVjOAdWOtrliIKn6hz1Pc,95
|
|
102
101
|
trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
|
|
103
102
|
trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
|
|
104
103
|
trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
|
|
105
|
-
|
|
106
|
-
pytrilogy-0.0.3.
|
|
107
|
-
pytrilogy-0.0.3.
|
|
108
|
-
pytrilogy-0.0.3.
|
|
109
|
-
pytrilogy-0.0.3.
|
|
104
|
+
trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
|
|
105
|
+
pytrilogy-0.0.3.35.dist-info/METADATA,sha256=0nFaVh5d7D6qAUyxTGqdKlRI93zNMvCtVM-9OVb5ACo,9100
|
|
106
|
+
pytrilogy-0.0.3.35.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
107
|
+
pytrilogy-0.0.3.35.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
108
|
+
pytrilogy-0.0.3.35.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
109
|
+
pytrilogy-0.0.3.35.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/authoring/__init__.py
CHANGED
|
@@ -19,12 +19,15 @@ from trilogy.core.models.author import (
|
|
|
19
19
|
Conditional,
|
|
20
20
|
FilterItem,
|
|
21
21
|
Function,
|
|
22
|
+
FunctionCallWrapper,
|
|
22
23
|
HavingClause,
|
|
23
24
|
MagicConstants,
|
|
24
25
|
Metadata,
|
|
26
|
+
MultiSelectLineage,
|
|
25
27
|
OrderBy,
|
|
26
28
|
OrderItem,
|
|
27
29
|
Parenthetical,
|
|
30
|
+
RowsetItem,
|
|
28
31
|
SubselectComparison,
|
|
29
32
|
WhereClause,
|
|
30
33
|
WindowItem,
|
|
@@ -103,4 +106,7 @@ __all__ = [
|
|
|
103
106
|
"RawSQLStatement",
|
|
104
107
|
"Datasource",
|
|
105
108
|
"DatasourceMetadata",
|
|
109
|
+
"MultiSelectLineage",
|
|
110
|
+
"RowsetItem",
|
|
111
|
+
"FunctionCallWrapper",
|
|
106
112
|
]
|
trilogy/core/functions.py
CHANGED
|
@@ -111,11 +111,14 @@ def validate_case_output(
|
|
|
111
111
|
if output_datatype != DataType.NULL:
|
|
112
112
|
datatypes.add(output_datatype.data_type)
|
|
113
113
|
mapz[str(arg.expr)] = output_datatype
|
|
114
|
-
|
|
114
|
+
known = [x for x in datatypes if x != DataType.UNKNOWN]
|
|
115
|
+
if len(known) == 0:
|
|
116
|
+
return DataType.UNKNOWN
|
|
117
|
+
if not len(known) == 1:
|
|
115
118
|
raise SyntaxError(
|
|
116
119
|
f"All case expressions must have the same output datatype, got {datatypes} from {mapz}"
|
|
117
120
|
)
|
|
118
|
-
return
|
|
121
|
+
return known.pop()
|
|
119
122
|
|
|
120
123
|
|
|
121
124
|
def create_struct_output(
|
|
@@ -672,12 +675,22 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
672
675
|
arg_count=1,
|
|
673
676
|
),
|
|
674
677
|
FunctionType.SUM: FunctionConfig(
|
|
675
|
-
valid_inputs={
|
|
678
|
+
valid_inputs={
|
|
679
|
+
DataType.INTEGER,
|
|
680
|
+
DataType.FLOAT,
|
|
681
|
+
DataType.NUMBER,
|
|
682
|
+
DataType.NUMERIC,
|
|
683
|
+
},
|
|
676
684
|
output_purpose=Purpose.METRIC,
|
|
677
685
|
arg_count=1,
|
|
678
686
|
),
|
|
679
687
|
FunctionType.AVG: FunctionConfig(
|
|
680
|
-
valid_inputs={
|
|
688
|
+
valid_inputs={
|
|
689
|
+
DataType.INTEGER,
|
|
690
|
+
DataType.FLOAT,
|
|
691
|
+
DataType.NUMBER,
|
|
692
|
+
DataType.NUMERIC,
|
|
693
|
+
},
|
|
681
694
|
output_purpose=Purpose.METRIC,
|
|
682
695
|
arg_count=1,
|
|
683
696
|
),
|
trilogy/core/models/author.py
CHANGED
|
@@ -107,6 +107,10 @@ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
|
|
|
107
107
|
) = DataType.UNKNOWN
|
|
108
108
|
metadata: Optional["Metadata"] = None
|
|
109
109
|
|
|
110
|
+
@property
|
|
111
|
+
def reference(self):
|
|
112
|
+
return self
|
|
113
|
+
|
|
110
114
|
@property
|
|
111
115
|
def line_no(self) -> int | None:
|
|
112
116
|
if self.metadata:
|
|
@@ -1089,7 +1093,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
|
|
|
1089
1093
|
pseudonyms=self.pseudonyms,
|
|
1090
1094
|
)
|
|
1091
1095
|
|
|
1092
|
-
@
|
|
1096
|
+
@cached_property
|
|
1093
1097
|
def sources(self) -> List["ConceptRef"]:
|
|
1094
1098
|
if self.lineage:
|
|
1095
1099
|
output: List[ConceptRef] = []
|
|
@@ -1426,7 +1430,7 @@ def get_basic_type(
|
|
|
1426
1430
|
return type
|
|
1427
1431
|
|
|
1428
1432
|
|
|
1429
|
-
class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
1433
|
+
class CaseWhen(Namespaced, DataTyped, ConceptArgs, Mergeable, BaseModel):
|
|
1430
1434
|
comparison: Conditional | SubselectComparison | Comparison
|
|
1431
1435
|
expr: "Expr"
|
|
1432
1436
|
|
|
@@ -1436,6 +1440,10 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
|
1436
1440
|
return v.reference
|
|
1437
1441
|
return v
|
|
1438
1442
|
|
|
1443
|
+
@property
|
|
1444
|
+
def output_datatype(self):
|
|
1445
|
+
return arg_to_datatype(self.expr)
|
|
1446
|
+
|
|
1439
1447
|
def __str__(self):
|
|
1440
1448
|
return self.__repr__()
|
|
1441
1449
|
|
|
@@ -1488,7 +1496,7 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
|
1488
1496
|
)
|
|
1489
1497
|
|
|
1490
1498
|
|
|
1491
|
-
class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
1499
|
+
class CaseElse(Namespaced, ConceptArgs, DataTyped, Mergeable, BaseModel):
|
|
1492
1500
|
expr: "Expr"
|
|
1493
1501
|
# this ensures that it's easily differentiable from CaseWhen
|
|
1494
1502
|
discriminant: ComparisonOperator = ComparisonOperator.ELSE
|
|
@@ -1499,6 +1507,10 @@ class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
|
|
|
1499
1507
|
def __repr__(self):
|
|
1500
1508
|
return f"ELSE {str(self.expr)}"
|
|
1501
1509
|
|
|
1510
|
+
@property
|
|
1511
|
+
def output_datatype(self):
|
|
1512
|
+
return arg_to_datatype(self.expr)
|
|
1513
|
+
|
|
1502
1514
|
@field_validator("expr", mode="before")
|
|
1503
1515
|
def enforce_expr(cls, v):
|
|
1504
1516
|
if isinstance(v, Concept):
|
|
@@ -1837,7 +1849,7 @@ class FunctionCallWrapper(
|
|
|
1837
1849
|
|
|
1838
1850
|
class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
|
|
1839
1851
|
function: Function
|
|
1840
|
-
by: List[ConceptRef] = Field(default_factory=list)
|
|
1852
|
+
by: List[ConceptRef | Concept] = Field(default_factory=list)
|
|
1841
1853
|
|
|
1842
1854
|
def __init__(self, **kwargs):
|
|
1843
1855
|
super().__init__(**kwargs)
|
|
@@ -1863,7 +1875,7 @@ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel)
|
|
|
1863
1875
|
|
|
1864
1876
|
@property
|
|
1865
1877
|
def concept_arguments(self) -> List[ConceptRef]:
|
|
1866
|
-
return self.function.concept_arguments + self.by
|
|
1878
|
+
return self.function.concept_arguments + [x.reference for x in self.by]
|
|
1867
1879
|
|
|
1868
1880
|
@property
|
|
1869
1881
|
def output_datatype(self):
|
|
@@ -2392,4 +2404,8 @@ FuncArgs = (
|
|
|
2392
2404
|
| NumericType
|
|
2393
2405
|
| list
|
|
2394
2406
|
| ListWrapper[Any]
|
|
2407
|
+
| TupleWrapper[Any]
|
|
2408
|
+
| Comparison
|
|
2409
|
+
| Conditional
|
|
2410
|
+
| MagicConstants
|
|
2395
2411
|
)
|
trilogy/core/models/build.py
CHANGED
|
@@ -1538,9 +1538,23 @@ class Factory:
|
|
|
1538
1538
|
|
|
1539
1539
|
@build.register
|
|
1540
1540
|
def _(self, base: CaseWhen) -> BuildCaseWhen:
|
|
1541
|
+
from trilogy.parsing.common import arbitrary_to_concept
|
|
1542
|
+
|
|
1543
|
+
comparison = base.comparison
|
|
1544
|
+
if isinstance(comparison, (AggregateWrapper, FilterItem, WindowItem)):
|
|
1545
|
+
comparison = arbitrary_to_concept(
|
|
1546
|
+
comparison,
|
|
1547
|
+
environment=self.environment,
|
|
1548
|
+
)
|
|
1549
|
+
expr: Concept | FuncArgs = base.expr
|
|
1550
|
+
if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
|
|
1551
|
+
expr = arbitrary_to_concept(
|
|
1552
|
+
expr,
|
|
1553
|
+
environment=self.environment,
|
|
1554
|
+
)
|
|
1541
1555
|
return BuildCaseWhen.model_construct(
|
|
1542
|
-
comparison=self.build(
|
|
1543
|
-
expr=
|
|
1556
|
+
comparison=self.build(comparison),
|
|
1557
|
+
expr=self.build(expr),
|
|
1544
1558
|
)
|
|
1545
1559
|
|
|
1546
1560
|
@build.register
|
|
@@ -1647,10 +1661,27 @@ class Factory:
|
|
|
1647
1661
|
@build.register
|
|
1648
1662
|
def _(self, base: WindowItem) -> BuildWindowItem:
|
|
1649
1663
|
# to do proper discovery, we need to inject virtual intermediate ocncepts
|
|
1664
|
+
from trilogy.parsing.common import arbitrary_to_concept
|
|
1665
|
+
|
|
1666
|
+
content: Concept | FuncArgs = base.content
|
|
1667
|
+
if isinstance(content, (AggregateWrapper, FilterItem, WindowItem)):
|
|
1668
|
+
content = arbitrary_to_concept(
|
|
1669
|
+
content,
|
|
1670
|
+
environment=self.environment,
|
|
1671
|
+
)
|
|
1672
|
+
final_by = []
|
|
1673
|
+
for x in base.order_by:
|
|
1674
|
+
if (
|
|
1675
|
+
isinstance(x.expr, AggregateWrapper)
|
|
1676
|
+
and not x.expr.by
|
|
1677
|
+
and isinstance(content, (ConceptRef, Concept))
|
|
1678
|
+
):
|
|
1679
|
+
x.expr.by = [content]
|
|
1680
|
+
final_by.append(x)
|
|
1650
1681
|
return BuildWindowItem.model_construct(
|
|
1651
1682
|
type=base.type,
|
|
1652
|
-
content=self.build(
|
|
1653
|
-
order_by=[self.build(x) for x in
|
|
1683
|
+
content=self.build(content),
|
|
1684
|
+
order_by=[self.build(x) for x in final_by],
|
|
1654
1685
|
over=[self.build(x) for x in base.over],
|
|
1655
1686
|
index=base.index,
|
|
1656
1687
|
)
|
|
@@ -31,19 +31,12 @@ class BuildEnvironmentConceptDict(dict):
|
|
|
31
31
|
def raise_undefined(
|
|
32
32
|
self, key: str, line_no: int | None = None, file: Path | str | None = None
|
|
33
33
|
) -> Never:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
message
|
|
39
|
-
|
|
40
|
-
if line_no:
|
|
41
|
-
if file:
|
|
42
|
-
raise UndefinedConceptException(
|
|
43
|
-
f"{file}: {line_no}: " + message, matches
|
|
44
|
-
)
|
|
45
|
-
raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
|
|
46
|
-
raise UndefinedConceptException(message, matches)
|
|
34
|
+
# build environment should never check for missing values.
|
|
35
|
+
if line_no is not None:
|
|
36
|
+
message = f"Concept '{key}' not found in environment at line {line_no}."
|
|
37
|
+
else:
|
|
38
|
+
message = f"Concept '{key}' not found in environment."
|
|
39
|
+
raise UndefinedConceptException(message, [])
|
|
47
40
|
|
|
48
41
|
def __getitem__(
|
|
49
42
|
self, key: str, line_no: int | None = None, file: Path | None = None
|
|
@@ -686,7 +686,8 @@ class Environment(BaseModel):
|
|
|
686
686
|
replacements[k] = target
|
|
687
687
|
# we need to update keys and grains of all concepts
|
|
688
688
|
else:
|
|
689
|
-
|
|
689
|
+
if source.address in v.sources or source.address in v.grain.components:
|
|
690
|
+
replacements[k] = v.with_merge(source, target, modifiers)
|
|
690
691
|
self.concepts.update(replacements)
|
|
691
692
|
for k, ds in self.datasources.items():
|
|
692
693
|
if source.address in ds.output_lcl:
|
trilogy/core/models/execute.py
CHANGED
|
@@ -282,6 +282,7 @@ class CTE(BaseModel):
|
|
|
282
282
|
**self.existence_source_map,
|
|
283
283
|
**other.existence_source_map,
|
|
284
284
|
}
|
|
285
|
+
|
|
285
286
|
return self
|
|
286
287
|
|
|
287
288
|
@property
|
|
@@ -764,8 +765,15 @@ class QueryDatasource(BaseModel):
|
|
|
764
765
|
def identifier(self) -> str:
|
|
765
766
|
filters = abs(hash(str(self.condition))) if self.condition else ""
|
|
766
767
|
grain = "_".join([str(c).replace(".", "_") for c in self.grain.components])
|
|
768
|
+
group = ""
|
|
769
|
+
if self.source_type == SourceType.GROUP:
|
|
770
|
+
keys = [
|
|
771
|
+
x.address for x in self.output_concepts if x.purpose != Purpose.METRIC
|
|
772
|
+
]
|
|
773
|
+
group = "_grouped_by_" + "_".join(keys)
|
|
767
774
|
return (
|
|
768
775
|
"_join_".join([d.identifier for d in self.datasources])
|
|
776
|
+
+ group
|
|
769
777
|
+ (f"_at_{grain}" if grain else "_at_abstract")
|
|
770
778
|
+ (f"_filtered_by_{filters}" if filters else "")
|
|
771
779
|
)
|
|
@@ -860,7 +860,7 @@ def _search_concepts(
|
|
|
860
860
|
and priority_concept.address not in conditions.row_arguments
|
|
861
861
|
):
|
|
862
862
|
logger.info(
|
|
863
|
-
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Force including conditions to push filtering above complex condition that is not condition member or parent"
|
|
863
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Force including conditions in {priority_concept.address} to push filtering above complex condition that is not condition member or parent"
|
|
864
864
|
)
|
|
865
865
|
local_conditions = conditions
|
|
866
866
|
|
|
@@ -936,7 +936,23 @@ def _search_concepts(
|
|
|
936
936
|
if complete == ValidationResult.COMPLETE and (
|
|
937
937
|
not accept_partial or (accept_partial and not partial)
|
|
938
938
|
):
|
|
939
|
+
logger.info(
|
|
940
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} breaking loop, complete"
|
|
941
|
+
)
|
|
939
942
|
break
|
|
943
|
+
elif complete == ValidationResult.COMPLETE and accept_partial and partial:
|
|
944
|
+
if len(attempted) == len(mandatory_list):
|
|
945
|
+
logger.info(
|
|
946
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Breaking as we have attempted all nodes"
|
|
947
|
+
)
|
|
948
|
+
break
|
|
949
|
+
logger.info(
|
|
950
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {attempted} all {len(mandatory_list)}"
|
|
951
|
+
)
|
|
952
|
+
else:
|
|
953
|
+
logger.info(
|
|
954
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Not complete, continuing search"
|
|
955
|
+
)
|
|
940
956
|
# if we have attempted on root node, we've tried them all.
|
|
941
957
|
# inject in another search with filter concepts
|
|
942
958
|
if priority_concept.derivation == Derivation.ROOT:
|
|
@@ -344,12 +344,14 @@ def create_datasource_node(
|
|
|
344
344
|
for c in datasource.columns
|
|
345
345
|
if not c.is_complete and c.concept.address in all_concepts
|
|
346
346
|
]
|
|
347
|
+
|
|
347
348
|
partial_lcl = LooseBuildConceptList(concepts=partial_concepts)
|
|
348
349
|
nullable_concepts = [
|
|
349
350
|
c.concept
|
|
350
351
|
for c in datasource.columns
|
|
351
352
|
if c.is_nullable and c.concept.address in all_concepts
|
|
352
353
|
]
|
|
354
|
+
|
|
353
355
|
nullable_lcl = LooseBuildConceptList(concepts=nullable_concepts)
|
|
354
356
|
partial_is_full = conditions and (conditions == datasource.non_partial_for)
|
|
355
357
|
|
|
@@ -81,7 +81,7 @@ class JoinOrderOutput:
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def resolve_join_order_v2(
|
|
84
|
-
g: nx.Graph, partials: dict[str, list[str]]
|
|
84
|
+
g: nx.Graph, partials: dict[str, list[str]], nullables: dict[str, list[str]]
|
|
85
85
|
) -> list[JoinOrderOutput]:
|
|
86
86
|
datasources = [x for x in g.nodes if x.startswith("ds~")]
|
|
87
87
|
concepts = [x for x in g.nodes if x.startswith("c~")]
|
|
@@ -108,7 +108,7 @@ def resolve_join_order_v2(
|
|
|
108
108
|
root = next_pivots[0]
|
|
109
109
|
pivots = [x for x in pivots if x != root]
|
|
110
110
|
else:
|
|
111
|
-
root = pivots.pop()
|
|
111
|
+
root = pivots.pop(0)
|
|
112
112
|
|
|
113
113
|
# sort so less partials is last and eligible lefts are
|
|
114
114
|
def score_key(x: str) -> tuple[int, int, str]:
|
|
@@ -119,6 +119,8 @@ def resolve_join_order_v2(
|
|
|
119
119
|
# if it has the concept as a partial, lower weight
|
|
120
120
|
if root in partials.get(x, []):
|
|
121
121
|
base -= 1
|
|
122
|
+
if root in nullables.get(x, []):
|
|
123
|
+
base -= 1
|
|
122
124
|
return (base, len(x), x)
|
|
123
125
|
|
|
124
126
|
# get remainig un-joined datasets
|
|
@@ -159,9 +161,11 @@ def resolve_join_order_v2(
|
|
|
159
161
|
)
|
|
160
162
|
right_is_partial = any(key in partials.get(right, []) for key in common)
|
|
161
163
|
# we don't care if left is nullable for join type (just keys), but if we did
|
|
162
|
-
#
|
|
164
|
+
# left_is_nullable = any(
|
|
165
|
+
# key in nullables.get(left_candidate, []) for key in common
|
|
166
|
+
# )
|
|
163
167
|
right_is_nullable = any(
|
|
164
|
-
key in
|
|
168
|
+
key in nullables.get(right, []) for key in common
|
|
165
169
|
)
|
|
166
170
|
if left_is_partial:
|
|
167
171
|
join_type = JoinType.FULL
|
|
@@ -356,6 +360,7 @@ def get_node_joins(
|
|
|
356
360
|
) -> List[BaseJoin]:
|
|
357
361
|
graph = nx.Graph()
|
|
358
362
|
partials: dict[str, list[str]] = {}
|
|
363
|
+
nullables: dict[str, list[str]] = {}
|
|
359
364
|
ds_node_map: dict[str, QueryDatasource | BuildDatasource] = {}
|
|
360
365
|
concept_map: dict[str, BuildConcept] = {}
|
|
361
366
|
for datasource in datasources:
|
|
@@ -363,6 +368,7 @@ def get_node_joins(
|
|
|
363
368
|
ds_node_map[ds_node] = datasource
|
|
364
369
|
graph.add_node(ds_node, type=NodeType.NODE)
|
|
365
370
|
partials[ds_node] = [f"c~{c.address}" for c in datasource.partial_concepts]
|
|
371
|
+
nullables[ds_node] = [f"c~{c.address}" for c in datasource.nullable_concepts]
|
|
366
372
|
for concept in datasource.output_concepts:
|
|
367
373
|
if concept.address in datasource.hidden_concepts:
|
|
368
374
|
continue
|
|
@@ -374,7 +380,7 @@ def get_node_joins(
|
|
|
374
380
|
environment=environment,
|
|
375
381
|
)
|
|
376
382
|
|
|
377
|
-
joins = resolve_join_order_v2(graph, partials=partials)
|
|
383
|
+
joins = resolve_join_order_v2(graph, partials=partials, nullables=nullables)
|
|
378
384
|
return [
|
|
379
385
|
BaseJoin(
|
|
380
386
|
left_datasource=ds_node_map[j.left] if j.left else None,
|
|
@@ -135,7 +135,6 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
|
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
output.grain = output.calculate_grain(environment)
|
|
138
|
-
|
|
139
138
|
for x in selection:
|
|
140
139
|
if x.is_undefined and environment.concepts.fail_on_missing:
|
|
141
140
|
environment.concepts.raise_undefined(
|
|
@@ -384,6 +383,11 @@ class MergeStatementV2(HasUUID, BaseModel):
|
|
|
384
383
|
modifiers: List[Modifier] = Field(default_factory=list)
|
|
385
384
|
|
|
386
385
|
|
|
386
|
+
class KeyMergeStatement(HasUUID, BaseModel):
|
|
387
|
+
keys: set[str]
|
|
388
|
+
target: ConceptRef
|
|
389
|
+
|
|
390
|
+
|
|
387
391
|
class ImportStatement(HasUUID, BaseModel):
|
|
388
392
|
# import abc.def as bar
|
|
389
393
|
# the bit after 'as', eg bar
|
trilogy/parsing/common.py
CHANGED
|
@@ -270,7 +270,7 @@ def _get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
|
|
|
270
270
|
elif isinstance(arg, AggregateWrapper) and not arg.by:
|
|
271
271
|
return [], True
|
|
272
272
|
elif isinstance(arg, AggregateWrapper) and arg.by:
|
|
273
|
-
return arg.by, True
|
|
273
|
+
return [x.reference for x in arg.by], True
|
|
274
274
|
elif isinstance(arg, FunctionCallWrapper):
|
|
275
275
|
return get_relevant_parent_concepts(arg.content)
|
|
276
276
|
return get_concept_arguments(arg), False
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -116,6 +116,7 @@ from trilogy.core.statements.author import (
|
|
|
116
116
|
CopyStatement,
|
|
117
117
|
FunctionDeclaration,
|
|
118
118
|
ImportStatement,
|
|
119
|
+
KeyMergeStatement,
|
|
119
120
|
Limit,
|
|
120
121
|
MergeStatementV2,
|
|
121
122
|
MultiSelectStatement,
|
|
@@ -890,6 +891,29 @@ class ParseToObjects(Transformer):
|
|
|
890
891
|
def over_list(self, args):
|
|
891
892
|
return [x for x in args]
|
|
892
893
|
|
|
894
|
+
@v_args(meta=True)
|
|
895
|
+
def key_merge_statement(self, meta: Meta, args) -> KeyMergeStatement | None:
|
|
896
|
+
key_inputs = args[:-1]
|
|
897
|
+
target = args[-1]
|
|
898
|
+
keys = [self.environment.concepts[grain] for grain in key_inputs]
|
|
899
|
+
target_c = self.environment.concepts[target]
|
|
900
|
+
new = KeyMergeStatement(
|
|
901
|
+
keys=set([x.address for x in keys]),
|
|
902
|
+
target=target_c.reference,
|
|
903
|
+
)
|
|
904
|
+
internal = Concept(
|
|
905
|
+
name="_" + target_c.address.replace(".", "_"),
|
|
906
|
+
namespace=self.environment.namespace,
|
|
907
|
+
purpose=Purpose.PROPERTY,
|
|
908
|
+
keys=set([x.address for x in keys]),
|
|
909
|
+
datatype=target_c.datatype,
|
|
910
|
+
grain=Grain(components={x.address for x in keys}),
|
|
911
|
+
)
|
|
912
|
+
self.environment.add_concept(internal)
|
|
913
|
+
# always a full merge
|
|
914
|
+
self.environment.merge_concept(target_c, internal, [])
|
|
915
|
+
return new
|
|
916
|
+
|
|
893
917
|
@v_args(meta=True)
|
|
894
918
|
def merge_statement(self, meta: Meta, args) -> MergeStatementV2 | None:
|
|
895
919
|
modifiers = []
|
trilogy/parsing/render.py
CHANGED
|
@@ -51,6 +51,7 @@ from trilogy.core.statements.author import (
|
|
|
51
51
|
CopyStatement,
|
|
52
52
|
FunctionDeclaration,
|
|
53
53
|
ImportStatement,
|
|
54
|
+
KeyMergeStatement,
|
|
54
55
|
MergeStatementV2,
|
|
55
56
|
MultiSelectStatement,
|
|
56
57
|
PersistStatement,
|
|
@@ -525,6 +526,11 @@ class Renderer:
|
|
|
525
526
|
return f"MERGE {self.to_string(arg.sources[0])} into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{self.to_string(arg.targets[arg.sources[0].address])};"
|
|
526
527
|
return f"MERGE {arg.source_wildcard}.* into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{arg.target_wildcard}.*;"
|
|
527
528
|
|
|
529
|
+
@to_string.register
|
|
530
|
+
def _(self, arg: KeyMergeStatement):
|
|
531
|
+
keys = ", ".join(sorted(list(arg.keys)))
|
|
532
|
+
return f"MERGE PROPERTY <{keys}> from {arg.target.address};"
|
|
533
|
+
|
|
528
534
|
@to_string.register
|
|
529
535
|
def _(self, arg: Modifier):
|
|
530
536
|
if arg == Modifier.PARTIAL:
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| rowset_derivation_statement
|
|
11
11
|
| import_statement
|
|
12
12
|
| copy_statement
|
|
13
|
+
| key_merge_statement
|
|
13
14
|
| merge_statement
|
|
14
15
|
| rawsql_statement
|
|
15
16
|
|
|
@@ -76,6 +77,8 @@
|
|
|
76
77
|
|
|
77
78
|
align_clause: align_item ("AND"i align_item)* "AND"i?
|
|
78
79
|
|
|
80
|
+
key_merge_statement: "merge"i "property"i "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "from"i IDENTIFIER
|
|
81
|
+
|
|
79
82
|
merge_statement: "merge"i WILDCARD_IDENTIFIER "into"i SHORTHAND_MODIFIER? WILDCARD_IDENTIFIER
|
|
80
83
|
|
|
81
84
|
// raw sql statement
|
trilogy/std/report.preql
ADDED
trilogy/std/dashboard.preql
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|