pytrilogy 0.0.3.32__py3-none-any.whl → 0.0.3.34__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.32.dist-info → pytrilogy-0.0.3.34.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.32.dist-info → pytrilogy-0.0.3.34.dist-info}/RECORD +27 -27
- trilogy/__init__.py +1 -1
- trilogy/authoring/__init__.py +6 -0
- trilogy/core/enums.py +1 -0
- trilogy/core/models/author.py +19 -6
- trilogy/core/models/build.py +1 -1
- trilogy/core/models/build_environment.py +6 -13
- trilogy/core/models/environment.py +2 -1
- trilogy/core/models/execute.py +9 -1
- trilogy/core/optimization.py +8 -0
- trilogy/core/processing/concept_strategies_v3.py +63 -3
- trilogy/core/processing/node_generators/group_node.py +3 -1
- trilogy/core/processing/node_generators/rowset_node.py +5 -1
- trilogy/core/processing/node_generators/select_merge_node.py +2 -0
- trilogy/core/processing/nodes/group_node.py +58 -1
- trilogy/core/processing/nodes/merge_node.py +13 -1
- trilogy/core/processing/utility.py +11 -5
- trilogy/core/statements/author.py +5 -0
- trilogy/parsing/common.py +78 -29
- trilogy/parsing/parse_engine.py +33 -2
- trilogy/parsing/render.py +6 -0
- trilogy/parsing/trilogy.lark +5 -1
- {pytrilogy-0.0.3.32.dist-info → pytrilogy-0.0.3.34.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.32.dist-info → pytrilogy-0.0.3.34.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.32.dist-info → pytrilogy-0.0.3.34.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.32.dist-info → pytrilogy-0.0.3.34.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.34.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=RkaJLhtJbPi75Dpq1wKhAgq3CaTPGqR2AIjV7Ot7NWA,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,10 +8,10 @@ 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
|
-
trilogy/core/enums.py,sha256=
|
|
14
|
+
trilogy/core/enums.py,sha256=fWexUZtssfvP5TiD7eQ66Q_tPUCNCCTGNSzLbVXrnqQ,7358
|
|
15
15
|
trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
|
|
16
16
|
trilogy/core/environment_helpers.py,sha256=5ayyhf4CGBlg_LssPu3DbS_H9H1Kq6Qog5TgR8pwyMk,8518
|
|
17
17
|
trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
|
|
@@ -19,35 +19,35 @@ trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,67
|
|
|
19
19
|
trilogy/core/functions.py,sha256=z5uyC5sAS_vnFBJcky7TjA0QAy-xI4Z9uD_9vK_XP1s,27134
|
|
20
20
|
trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
|
|
21
21
|
trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
|
|
22
|
-
trilogy/core/optimization.py,sha256=
|
|
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=Z5RQr5vsPyxLSXR11w_b8z-tQ54bSM3ks1az9uV3d40,76579
|
|
26
|
+
trilogy/core/models/build.py,sha256=N3Zr47iN4y9gFWS72uYjecmiQfcm8Fl9wT3QS8xKBAA,58185
|
|
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
|
|
44
44
|
trilogy/core/processing/node_generators/filter_node.py,sha256=rlY7TbgjJlGhahYgdCIJpJbaSREAGVJEsyUIGaA38O0,8271
|
|
45
|
-
trilogy/core/processing/node_generators/group_node.py,sha256=
|
|
45
|
+
trilogy/core/processing/node_generators/group_node.py,sha256=kO-ersxIL04rZwX5-vFIFQQnp357PFo_7ZKXoGq3wyc,5989
|
|
46
46
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=E5bEjovSx422d_MlAUCDFdY4P2WJVp61BmWwltkhzA8,3095
|
|
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
|
-
trilogy/core/processing/node_generators/rowset_node.py,sha256=
|
|
50
|
-
trilogy/core/processing/node_generators/select_merge_node.py,sha256=
|
|
49
|
+
trilogy/core/processing/node_generators/rowset_node.py,sha256=YmBs6ZQ7azLXRFEmeoecpGjK4pMHsUCovuBxfb3UKZI,6848
|
|
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
|
|
@@ -58,14 +58,14 @@ trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,s
|
|
|
58
58
|
trilogy/core/processing/nodes/__init__.py,sha256=DqPG3Y8vl5-UTeox6hn1EE6iwPIJpsM-XeZALHSgLZQ,5058
|
|
59
59
|
trilogy/core/processing/nodes/base_node.py,sha256=FHrY8GsTKPuMJklOjILbhGqCt5s1nmlj62Z-molARDA,16835
|
|
60
60
|
trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
|
|
61
|
-
trilogy/core/processing/nodes/group_node.py,sha256=
|
|
62
|
-
trilogy/core/processing/nodes/merge_node.py,sha256=
|
|
61
|
+
trilogy/core/processing/nodes/group_node.py,sha256=MUvcOg9U5J6TnWBel8eht9PdI9BfAKjUxmfjP_ZXx9o,10484
|
|
62
|
+
trilogy/core/processing/nodes/merge_node.py,sha256=djFBJ8Sq1_crP0g7REGCbMFl4so7zPzabkPgAA1TUl4,15720
|
|
63
63
|
trilogy/core/processing/nodes/select_node_v2.py,sha256=Xyfq8lU7rP7JTAd8VV0ATDNal64n4xIBgWQsOuMe_Ak,8824
|
|
64
64
|
trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ_LpvPokpZKOhx4,1425
|
|
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=SWB755fSZo0rxc9MzN1E7-NwmTqA7BhFul0ENqU9P5k,14727
|
|
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,13 +87,13 @@ 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=bhZ_f520D2diRcXC3YeEToPSXBbUBBNq42zII_smdYc,23518
|
|
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
|
|
@@ -102,8 +102,8 @@ trilogy/std/date.preql,sha256=0MHeGLp2mG4QBKtmozcBZ7qVjOAdWOtrliIKn6hz1Pc,95
|
|
|
102
102
|
trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
|
|
103
103
|
trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
|
|
104
104
|
trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
|
|
105
|
-
pytrilogy-0.0.3.
|
|
106
|
-
pytrilogy-0.0.3.
|
|
107
|
-
pytrilogy-0.0.3.
|
|
108
|
-
pytrilogy-0.0.3.
|
|
109
|
-
pytrilogy-0.0.3.
|
|
105
|
+
pytrilogy-0.0.3.34.dist-info/METADATA,sha256=GisPUfJ5BGF4EpuzvClSgxb6ITvhKhX3lv2Ht9brw2M,9100
|
|
106
|
+
pytrilogy-0.0.3.34.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
107
|
+
pytrilogy-0.0.3.34.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
108
|
+
pytrilogy-0.0.3.34.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
109
|
+
pytrilogy-0.0.3.34.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/enums.py
CHANGED
trilogy/core/models/author.py
CHANGED
|
@@ -1089,7 +1089,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
|
|
|
1089
1089
|
pseudonyms=self.pseudonyms,
|
|
1090
1090
|
)
|
|
1091
1091
|
|
|
1092
|
-
@
|
|
1092
|
+
@cached_property
|
|
1093
1093
|
def sources(self) -> List["ConceptRef"]:
|
|
1094
1094
|
if self.lineage:
|
|
1095
1095
|
output: List[ConceptRef] = []
|
|
@@ -1326,7 +1326,7 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
|
1326
1326
|
|
|
1327
1327
|
class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
1328
1328
|
type: WindowType
|
|
1329
|
-
content:
|
|
1329
|
+
content: FuncArgs
|
|
1330
1330
|
order_by: List["OrderItem"]
|
|
1331
1331
|
over: List["ConceptRef"] = Field(default_factory=list)
|
|
1332
1332
|
index: Optional[int] = None
|
|
@@ -1335,7 +1335,7 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1335
1335
|
return self.__repr__()
|
|
1336
1336
|
|
|
1337
1337
|
def __repr__(self):
|
|
1338
|
-
return f"{self.type}
|
|
1338
|
+
return f"{self.type.value} {self.content} by {self.index} over {self.over} order {self.order_by}"
|
|
1339
1339
|
|
|
1340
1340
|
@field_validator("content", mode="before")
|
|
1341
1341
|
def enforce_concept_ref(cls, v):
|
|
@@ -1358,7 +1358,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1358
1358
|
) -> "WindowItem":
|
|
1359
1359
|
output = WindowItem.model_construct(
|
|
1360
1360
|
type=self.type,
|
|
1361
|
-
content=
|
|
1361
|
+
content=(
|
|
1362
|
+
self.content.with_merge(source, target, modifiers)
|
|
1363
|
+
if isinstance(self.content, Mergeable)
|
|
1364
|
+
else self.content
|
|
1365
|
+
),
|
|
1362
1366
|
over=[x.with_merge(source, target, modifiers) for x in self.over],
|
|
1363
1367
|
order_by=[x.with_merge(source, target, modifiers) for x in self.order_by],
|
|
1364
1368
|
index=self.index,
|
|
@@ -1379,7 +1383,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1379
1383
|
def with_namespace(self, namespace: str) -> "WindowItem":
|
|
1380
1384
|
return WindowItem.model_construct(
|
|
1381
1385
|
type=self.type,
|
|
1382
|
-
content=
|
|
1386
|
+
content=(
|
|
1387
|
+
self.content.with_namespace(namespace)
|
|
1388
|
+
if isinstance(self.content, Namespaced)
|
|
1389
|
+
else self.content
|
|
1390
|
+
),
|
|
1383
1391
|
over=[x.with_namespace(namespace) for x in self.over],
|
|
1384
1392
|
order_by=[x.with_namespace(namespace) for x in self.order_by],
|
|
1385
1393
|
index=self.index,
|
|
@@ -1387,7 +1395,8 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1387
1395
|
|
|
1388
1396
|
@property
|
|
1389
1397
|
def concept_arguments(self) -> List[ConceptRef]:
|
|
1390
|
-
output = [
|
|
1398
|
+
output = []
|
|
1399
|
+
output += get_concept_arguments(self.content)
|
|
1391
1400
|
for order in self.order_by:
|
|
1392
1401
|
output += get_concept_arguments(order)
|
|
1393
1402
|
for item in self.over:
|
|
@@ -1787,6 +1796,10 @@ class FunctionCallWrapper(
|
|
|
1787
1796
|
],
|
|
1788
1797
|
)
|
|
1789
1798
|
|
|
1799
|
+
def with_reference_replacement(self, source, target):
|
|
1800
|
+
raise NotImplementedError("Cannot reference replace")
|
|
1801
|
+
return self
|
|
1802
|
+
|
|
1790
1803
|
def with_merge(
|
|
1791
1804
|
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
1792
1805
|
) -> "FunctionCallWrapper":
|
trilogy/core/models/build.py
CHANGED
|
@@ -1646,7 +1646,7 @@ class Factory:
|
|
|
1646
1646
|
|
|
1647
1647
|
@build.register
|
|
1648
1648
|
def _(self, base: WindowItem) -> BuildWindowItem:
|
|
1649
|
-
|
|
1649
|
+
# to do proper discovery, we need to inject virtual intermediate ocncepts
|
|
1650
1650
|
return BuildWindowItem.model_construct(
|
|
1651
1651
|
type=base.type,
|
|
1652
1652
|
content=self.build(base.content),
|
|
@@ -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
|
|
@@ -637,7 +638,7 @@ class QueryDatasource(BaseModel):
|
|
|
637
638
|
and CONFIG.validate_missing
|
|
638
639
|
):
|
|
639
640
|
raise SyntaxError(
|
|
640
|
-
f"On query datasource missing source map for {concept.address} on {key}, have {v}"
|
|
641
|
+
f"On query datasource missing source map for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
|
|
641
642
|
)
|
|
642
643
|
return v
|
|
643
644
|
|
|
@@ -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
|
)
|
trilogy/core/optimization.py
CHANGED
|
@@ -94,6 +94,11 @@ def filter_irrelevant_ctes(
|
|
|
94
94
|
inverse_map = gen_inverse_map(input)
|
|
95
95
|
recurse(root_cte, inverse_map)
|
|
96
96
|
final = [cte for cte in input if cte.name in relevant_ctes]
|
|
97
|
+
filtered = [cte for cte in input if cte.name not in relevant_ctes]
|
|
98
|
+
if filtered:
|
|
99
|
+
logger.info(
|
|
100
|
+
f"[Optimization][Irrelevent CTE filtering] Removing redundant CTEs {[x.name for x in filtered]}"
|
|
101
|
+
)
|
|
97
102
|
if len(final) == len(input):
|
|
98
103
|
return input
|
|
99
104
|
return filter_irrelevant_ctes(final, root_cte)
|
|
@@ -130,6 +135,9 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
|
|
|
130
135
|
if not output_addresses.issubset(parent_output_addresses):
|
|
131
136
|
return None
|
|
132
137
|
if not direct_parent.grain == cte.grain:
|
|
138
|
+
logger.info("grain mismatch, cannot early exit")
|
|
139
|
+
logger.info(direct_parent.grain)
|
|
140
|
+
logger.info(cte.grain)
|
|
133
141
|
return None
|
|
134
142
|
|
|
135
143
|
assert isinstance(cte, CTE)
|
|
@@ -625,6 +625,7 @@ def validate_stack(
|
|
|
625
625
|
for concept in resolved.output_concepts:
|
|
626
626
|
if concept.address in resolved.hidden_concepts:
|
|
627
627
|
continue
|
|
628
|
+
|
|
628
629
|
validate_concept(
|
|
629
630
|
concept,
|
|
630
631
|
node,
|
|
@@ -773,6 +774,8 @@ def _search_concepts(
|
|
|
773
774
|
) -> StrategyNode | None:
|
|
774
775
|
# these are the concepts we need in the output projection
|
|
775
776
|
mandatory_list = unique(mandatory_list, "address")
|
|
777
|
+
# cache our values before an filter injection
|
|
778
|
+
original_mandatory = [*mandatory_list]
|
|
776
779
|
for x in mandatory_list:
|
|
777
780
|
if isinstance(x, UndefinedConcept):
|
|
778
781
|
raise SyntaxError(f"Undefined concept {x.address}")
|
|
@@ -857,7 +860,7 @@ def _search_concepts(
|
|
|
857
860
|
and priority_concept.address not in conditions.row_arguments
|
|
858
861
|
):
|
|
859
862
|
logger.info(
|
|
860
|
-
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"
|
|
861
864
|
)
|
|
862
865
|
local_conditions = conditions
|
|
863
866
|
|
|
@@ -921,7 +924,7 @@ def _search_concepts(
|
|
|
921
924
|
mandatory_completion = [c.address for c in completion_mandatory]
|
|
922
925
|
logger.info(
|
|
923
926
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} finished concept loop for {priority_concept} {priority_concept.derivation} condition {conditions} flag for accepting partial addresses is"
|
|
924
|
-
f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
|
|
927
|
+
f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} synonyms partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
|
|
925
928
|
)
|
|
926
929
|
if complete == ValidationResult.INCOMPLETE_CONDITION:
|
|
927
930
|
cond_dict = {str(node): node.preexisting_conditions for node in stack}
|
|
@@ -933,7 +936,23 @@ def _search_concepts(
|
|
|
933
936
|
if complete == ValidationResult.COMPLETE and (
|
|
934
937
|
not accept_partial or (accept_partial and not partial)
|
|
935
938
|
):
|
|
939
|
+
logger.info(
|
|
940
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} breaking loop, complete"
|
|
941
|
+
)
|
|
936
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
|
+
)
|
|
937
956
|
# if we have attempted on root node, we've tried them all.
|
|
938
957
|
# inject in another search with filter concepts
|
|
939
958
|
if priority_concept.derivation == Derivation.ROOT:
|
|
@@ -948,6 +967,11 @@ def _search_concepts(
|
|
|
948
967
|
if complete == ValidationResult.COMPLETE:
|
|
949
968
|
condition_required = True
|
|
950
969
|
non_virtual = [c for c in completion_mandatory if c.address not in virtual]
|
|
970
|
+
non_virtual_output = [c for c in original_mandatory if c.address not in virtual]
|
|
971
|
+
non_virtual_different = len(completion_mandatory) != len(original_mandatory)
|
|
972
|
+
non_virtual_difference_values = set(
|
|
973
|
+
[x.address for x in completion_mandatory]
|
|
974
|
+
).difference(set([x.address for x in original_mandatory]))
|
|
951
975
|
if not conditions:
|
|
952
976
|
condition_required = False
|
|
953
977
|
non_virtual = [c for c in mandatory_list if c.address not in virtual]
|
|
@@ -966,7 +990,19 @@ def _search_concepts(
|
|
|
966
990
|
)
|
|
967
991
|
if len(stack) == 1:
|
|
968
992
|
output: StrategyNode = stack[0]
|
|
969
|
-
|
|
993
|
+
if non_virtual_different:
|
|
994
|
+
logger.info(
|
|
995
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values"
|
|
996
|
+
)
|
|
997
|
+
output.set_output_concepts(
|
|
998
|
+
[
|
|
999
|
+
x
|
|
1000
|
+
for x in output.output_concepts
|
|
1001
|
+
if x.address in non_virtual_output
|
|
1002
|
+
],
|
|
1003
|
+
rebuild=False,
|
|
1004
|
+
)
|
|
1005
|
+
|
|
970
1006
|
logger.info(
|
|
971
1007
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)}"
|
|
972
1008
|
)
|
|
@@ -995,6 +1031,30 @@ def _search_concepts(
|
|
|
995
1031
|
logger.info(
|
|
996
1032
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
|
|
997
1033
|
)
|
|
1034
|
+
if condition_required and conditions and non_virtual_different:
|
|
1035
|
+
logger.info(
|
|
1036
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Conditions {conditions} were injected, checking if we need a group to restore grain"
|
|
1037
|
+
)
|
|
1038
|
+
result = GroupNode.check_if_required(
|
|
1039
|
+
downstream_concepts=original_mandatory,
|
|
1040
|
+
parents=[output.resolve()],
|
|
1041
|
+
environment=environment,
|
|
1042
|
+
depth=depth,
|
|
1043
|
+
)
|
|
1044
|
+
logger.info(f"gcheck result is {result}")
|
|
1045
|
+
if result.required:
|
|
1046
|
+
logger.info(
|
|
1047
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Adding group node"
|
|
1048
|
+
)
|
|
1049
|
+
return GroupNode(
|
|
1050
|
+
output_concepts=original_mandatory,
|
|
1051
|
+
input_concepts=original_mandatory,
|
|
1052
|
+
environment=environment,
|
|
1053
|
+
parents=[output],
|
|
1054
|
+
partial_concepts=output.partial_concepts,
|
|
1055
|
+
preexisting_conditions=conditions.conditional,
|
|
1056
|
+
depth=depth,
|
|
1057
|
+
)
|
|
998
1058
|
return output
|
|
999
1059
|
|
|
1000
1060
|
# if we can't find it after expanding to a merge, then
|
|
@@ -149,7 +149,9 @@ def gen_group_node(
|
|
|
149
149
|
g=g,
|
|
150
150
|
depth=depth,
|
|
151
151
|
source_concepts=source_concepts,
|
|
152
|
-
log_lambda=create_log_lambda(
|
|
152
|
+
log_lambda=create_log_lambda(
|
|
153
|
+
LOGGER_PREFIX + f" for {concept.address}", depth, logger
|
|
154
|
+
),
|
|
153
155
|
history=history,
|
|
154
156
|
conditions=conditions,
|
|
155
157
|
)
|
|
@@ -117,7 +117,11 @@ def gen_rowset_node(
|
|
|
117
117
|
f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]}"
|
|
118
118
|
)
|
|
119
119
|
if not local_optional or all(
|
|
120
|
-
|
|
120
|
+
(
|
|
121
|
+
x.address in node.output_concepts
|
|
122
|
+
or (z in x.pseudonyms for z in node.output_concepts)
|
|
123
|
+
)
|
|
124
|
+
and x.address not in node.partial_concepts
|
|
121
125
|
for x in local_optional
|
|
122
126
|
):
|
|
123
127
|
logger.info(
|
|
@@ -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
|
|
|
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
4
|
from trilogy.constants import logger
|
|
5
|
-
from trilogy.core.enums import SourceType
|
|
5
|
+
from trilogy.core.enums import Purpose, SourceType
|
|
6
6
|
from trilogy.core.models.build import (
|
|
7
7
|
BuildComparison,
|
|
8
8
|
BuildConcept,
|
|
@@ -78,7 +78,9 @@ class GroupNode(StrategyNode):
|
|
|
78
78
|
downstream_concepts: List[BuildConcept],
|
|
79
79
|
parents: list[QueryDatasource | BuildDatasource],
|
|
80
80
|
environment: BuildEnvironment,
|
|
81
|
+
depth: int = 0,
|
|
81
82
|
) -> GroupRequiredResponse:
|
|
83
|
+
padding = "\t" * depth
|
|
82
84
|
target_grain = BuildGrain.from_concepts(
|
|
83
85
|
downstream_concepts,
|
|
84
86
|
environment=environment,
|
|
@@ -95,12 +97,67 @@ class GroupNode(StrategyNode):
|
|
|
95
97
|
lookups: list[BuildConcept | str] = [
|
|
96
98
|
concept_map[x] if x in concept_map else x for x in comp_grain.components
|
|
97
99
|
]
|
|
100
|
+
|
|
98
101
|
comp_grain = BuildGrain.from_concepts(lookups, environment=environment)
|
|
102
|
+
|
|
99
103
|
# dynamically select if we need to group
|
|
100
104
|
# because sometimes, we are already at required grain
|
|
101
105
|
if comp_grain.issubset(target_grain):
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, is subset, no grain required"
|
|
109
|
+
)
|
|
102
110
|
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
111
|
+
# find out what extra is in the comp grain vs target grain
|
|
112
|
+
difference = [
|
|
113
|
+
environment.concepts[c] for c in (comp_grain - target_grain).components
|
|
114
|
+
]
|
|
115
|
+
logger.info(
|
|
116
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, difference {difference}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# if the difference is all unique properties whose keys are in the source grain
|
|
120
|
+
# we can also suppress the group
|
|
121
|
+
if all(
|
|
122
|
+
[
|
|
123
|
+
x.purpose == Purpose.UNIQUE_PROPERTY
|
|
124
|
+
and x.keys
|
|
125
|
+
and all(z in comp_grain.components for z in x.keys)
|
|
126
|
+
for x in difference
|
|
127
|
+
]
|
|
128
|
+
):
|
|
129
|
+
logger.info(
|
|
130
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
|
|
131
|
+
)
|
|
132
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
133
|
+
if all([x.purpose == Purpose.KEY for x in difference]):
|
|
134
|
+
logger.info(
|
|
135
|
+
f"{padding}{LOGGER_PREFIX} checking if downstream is unique properties of key"
|
|
136
|
+
)
|
|
137
|
+
replaced_grain_raw: list[set[str]] = [
|
|
138
|
+
(
|
|
139
|
+
x.keys or set()
|
|
140
|
+
if x.purpose == Purpose.UNIQUE_PROPERTY
|
|
141
|
+
else set([x.address])
|
|
142
|
+
)
|
|
143
|
+
for x in downstream_concepts
|
|
144
|
+
if x.address in target_grain.components
|
|
145
|
+
]
|
|
146
|
+
# flatten the list of lists
|
|
147
|
+
replaced_grain = [
|
|
148
|
+
item for sublist in replaced_grain_raw for item in sublist
|
|
149
|
+
]
|
|
150
|
+
# if the replaced grain is a subset of the comp grain, we can skip the group
|
|
151
|
+
unique_grain_comp = BuildGrain.from_concepts(
|
|
152
|
+
replaced_grain, environment=environment
|
|
153
|
+
)
|
|
154
|
+
if comp_grain.issubset(unique_grain_comp):
|
|
155
|
+
logger.info(
|
|
156
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
|
|
157
|
+
)
|
|
158
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
103
159
|
|
|
160
|
+
logger.info(f"{padding}{LOGGER_PREFIX} Group requirement check: group required")
|
|
104
161
|
return GroupRequiredResponse(target_grain, comp_grain, True)
|
|
105
162
|
|
|
106
163
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -296,9 +296,21 @@ class MergeNode(StrategyNode):
|
|
|
296
296
|
return dataset
|
|
297
297
|
|
|
298
298
|
pregrain = BuildGrain()
|
|
299
|
+
|
|
299
300
|
for source in final_datasets:
|
|
301
|
+
if all(
|
|
302
|
+
[x.address in self.existence_concepts for x in source.output_concepts]
|
|
303
|
+
):
|
|
304
|
+
logger.info(
|
|
305
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} skipping existence only source with {source.output_concepts} from grain accumulation"
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
300
308
|
pregrain += source.grain
|
|
301
309
|
|
|
310
|
+
pregrain = BuildGrain.from_concepts(
|
|
311
|
+
pregrain.components, environment=self.environment
|
|
312
|
+
)
|
|
313
|
+
|
|
302
314
|
grain = self.grain if self.grain else pregrain
|
|
303
315
|
logger.info(
|
|
304
316
|
f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
|
|
@@ -310,6 +322,7 @@ class MergeNode(StrategyNode):
|
|
|
310
322
|
)
|
|
311
323
|
else:
|
|
312
324
|
joins = []
|
|
325
|
+
|
|
313
326
|
logger.info(
|
|
314
327
|
f"{self.logging_prefix}{LOGGER_PREFIX} Final join count for CTE parent count {len(join_candidates)} is {len(joins)}"
|
|
315
328
|
)
|
|
@@ -343,7 +356,6 @@ class MergeNode(StrategyNode):
|
|
|
343
356
|
nullable_concepts = find_nullable_concepts(
|
|
344
357
|
source_map=source_map, joins=joins, datasources=final_datasets
|
|
345
358
|
)
|
|
346
|
-
|
|
347
359
|
qds = QueryDatasource(
|
|
348
360
|
input_concepts=unique(self.input_concepts, "address"),
|
|
349
361
|
output_concepts=unique(self.output_concepts, "address"),
|
|
@@ -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,
|
|
@@ -384,6 +384,11 @@ class MergeStatementV2(HasUUID, BaseModel):
|
|
|
384
384
|
modifiers: List[Modifier] = Field(default_factory=list)
|
|
385
385
|
|
|
386
386
|
|
|
387
|
+
class KeyMergeStatement(HasUUID, BaseModel):
|
|
388
|
+
keys: set[str]
|
|
389
|
+
target: ConceptRef
|
|
390
|
+
|
|
391
|
+
|
|
387
392
|
class ImportStatement(HasUUID, BaseModel):
|
|
388
393
|
# import abc.def as bar
|
|
389
394
|
# the bit after 'as', eg bar
|
trilogy/parsing/common.py
CHANGED
|
@@ -40,6 +40,7 @@ from trilogy.core.models.author import (
|
|
|
40
40
|
UndefinedConcept,
|
|
41
41
|
WhereClause,
|
|
42
42
|
WindowItem,
|
|
43
|
+
address_with_namespace,
|
|
43
44
|
)
|
|
44
45
|
from trilogy.core.models.core import DataType, arg_to_datatype
|
|
45
46
|
from trilogy.core.models.environment import Environment
|
|
@@ -255,7 +256,7 @@ def concepts_to_grain_concepts(
|
|
|
255
256
|
return v2
|
|
256
257
|
|
|
257
258
|
|
|
258
|
-
def
|
|
259
|
+
def _get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
|
|
259
260
|
from trilogy.core.models.author import get_concept_arguments
|
|
260
261
|
|
|
261
262
|
is_metric = False
|
|
@@ -270,9 +271,16 @@ def get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
|
|
|
270
271
|
return [], True
|
|
271
272
|
elif isinstance(arg, AggregateWrapper) and arg.by:
|
|
272
273
|
return arg.by, True
|
|
274
|
+
elif isinstance(arg, FunctionCallWrapper):
|
|
275
|
+
return get_relevant_parent_concepts(arg.content)
|
|
273
276
|
return get_concept_arguments(arg), False
|
|
274
277
|
|
|
275
278
|
|
|
279
|
+
def get_relevant_parent_concepts(arg):
|
|
280
|
+
results = _get_relevant_parent_concepts(arg)
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
|
|
276
284
|
def function_to_concept(
|
|
277
285
|
parent: Function,
|
|
278
286
|
name: str,
|
|
@@ -415,10 +423,31 @@ def window_item_to_concept(
|
|
|
415
423
|
metadata: Metadata | None = None,
|
|
416
424
|
) -> Concept:
|
|
417
425
|
fmetadata = metadata or Metadata()
|
|
426
|
+
# if isinstance(
|
|
427
|
+
# parent.content,
|
|
428
|
+
# (
|
|
429
|
+
# AggregateWrapper
|
|
430
|
+
# | FunctionCallWrapper
|
|
431
|
+
# | WindowItem
|
|
432
|
+
# | FilterItem
|
|
433
|
+
# | Function
|
|
434
|
+
# | ListWrapper
|
|
435
|
+
# | MapWrapper
|
|
436
|
+
# ),
|
|
437
|
+
# ):
|
|
438
|
+
# new_parent = arbitrary_to_concept(
|
|
439
|
+
# parent.content, environment=environment, namespace=namespace
|
|
440
|
+
# )
|
|
441
|
+
# environment.add_concept(new_parent)
|
|
442
|
+
# parent = parent.model_copy(update={"content": new_parent.reference})
|
|
443
|
+
|
|
444
|
+
if not isinstance(parent.content, ConceptRef):
|
|
445
|
+
raise NotImplementedError(
|
|
446
|
+
f"Window function wiht non ref content {parent.content} not yet supported"
|
|
447
|
+
)
|
|
418
448
|
bcontent = environment.concepts[parent.content.address]
|
|
419
449
|
if isinstance(bcontent, UndefinedConcept):
|
|
420
450
|
return UndefinedConcept(address=f"{namespace}.{name}", metadata=fmetadata)
|
|
421
|
-
|
|
422
451
|
if bcontent.purpose == Purpose.METRIC:
|
|
423
452
|
local_purpose, keys = get_purpose_and_keys(None, (bcontent,), environment)
|
|
424
453
|
else:
|
|
@@ -534,38 +563,58 @@ def align_item_to_concept(
|
|
|
534
563
|
return new
|
|
535
564
|
|
|
536
565
|
|
|
566
|
+
def rowset_concept(
|
|
567
|
+
orig_address: ConceptRef,
|
|
568
|
+
environment: Environment,
|
|
569
|
+
rowset: RowsetDerivationStatement,
|
|
570
|
+
pre_output: list[Concept],
|
|
571
|
+
orig: dict[str, Concept],
|
|
572
|
+
orig_map: dict[str, Concept],
|
|
573
|
+
):
|
|
574
|
+
orig_concept = environment.concepts[orig_address.address]
|
|
575
|
+
name = orig_concept.name
|
|
576
|
+
if isinstance(orig_concept.lineage, FilterItem):
|
|
577
|
+
if orig_concept.lineage.where == rowset.select.where_clause:
|
|
578
|
+
name = environment.concepts[orig_concept.lineage.content.address].name
|
|
579
|
+
base_namespace = (
|
|
580
|
+
f"{rowset.name}.{orig_concept.namespace}"
|
|
581
|
+
if orig_concept.namespace != rowset.namespace
|
|
582
|
+
else rowset.name
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
new_concept = Concept(
|
|
586
|
+
name=name,
|
|
587
|
+
datatype=orig_concept.datatype,
|
|
588
|
+
purpose=orig_concept.purpose,
|
|
589
|
+
lineage=None,
|
|
590
|
+
grain=orig_concept.grain,
|
|
591
|
+
metadata=Metadata(concept_source=ConceptSource.CTE),
|
|
592
|
+
namespace=base_namespace,
|
|
593
|
+
keys=orig_concept.keys,
|
|
594
|
+
derivation=Derivation.ROWSET,
|
|
595
|
+
granularity=orig_concept.granularity,
|
|
596
|
+
pseudonyms={
|
|
597
|
+
address_with_namespace(x, rowset.name) for x in orig_concept.pseudonyms
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
for x in orig_concept.pseudonyms:
|
|
601
|
+
new_address = address_with_namespace(x, rowset.name)
|
|
602
|
+
origa = environment.alias_origin_lookup[x]
|
|
603
|
+
environment.concepts[new_address] = new_concept
|
|
604
|
+
environment.alias_origin_lookup[new_address] = origa.model_copy(
|
|
605
|
+
update={"namespace": f"{rowset.name}.{origa.namespace}"}
|
|
606
|
+
)
|
|
607
|
+
orig[orig_concept.address] = new_concept
|
|
608
|
+
orig_map[new_concept.address] = orig_concept
|
|
609
|
+
pre_output.append(new_concept)
|
|
610
|
+
|
|
611
|
+
|
|
537
612
|
def rowset_to_concepts(rowset: RowsetDerivationStatement, environment: Environment):
|
|
538
613
|
pre_output: list[Concept] = []
|
|
539
614
|
orig: dict[str, Concept] = {}
|
|
540
615
|
orig_map: dict[str, Concept] = {}
|
|
541
616
|
for orig_address in rowset.select.output_components:
|
|
542
|
-
|
|
543
|
-
name = orig_concept.name
|
|
544
|
-
if isinstance(orig_concept.lineage, FilterItem):
|
|
545
|
-
if orig_concept.lineage.where == rowset.select.where_clause:
|
|
546
|
-
name = environment.concepts[orig_concept.lineage.content.address].name
|
|
547
|
-
base_namespace = (
|
|
548
|
-
f"{rowset.name}.{orig_concept.namespace}"
|
|
549
|
-
if orig_concept.namespace != rowset.namespace
|
|
550
|
-
else rowset.name
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
new_concept = Concept(
|
|
554
|
-
name=name,
|
|
555
|
-
datatype=orig_concept.datatype,
|
|
556
|
-
purpose=orig_concept.purpose,
|
|
557
|
-
lineage=None,
|
|
558
|
-
grain=orig_concept.grain,
|
|
559
|
-
# TODO: add proper metadata
|
|
560
|
-
metadata=Metadata(concept_source=ConceptSource.CTE),
|
|
561
|
-
namespace=base_namespace,
|
|
562
|
-
keys=orig_concept.keys,
|
|
563
|
-
derivation=Derivation.ROWSET,
|
|
564
|
-
granularity=orig_concept.granularity,
|
|
565
|
-
)
|
|
566
|
-
orig[orig_concept.address] = new_concept
|
|
567
|
-
orig_map[new_concept.address] = orig_concept
|
|
568
|
-
pre_output.append(new_concept)
|
|
617
|
+
rowset_concept(orig_address, environment, rowset, pre_output, orig, orig_map)
|
|
569
618
|
select_lineage = rowset.select.as_lineage(environment)
|
|
570
619
|
for x in pre_output:
|
|
571
620
|
x.lineage = RowsetItem(
|
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,
|
|
@@ -252,6 +253,8 @@ def rehydrate_lineage(
|
|
|
252
253
|
)
|
|
253
254
|
return lineage
|
|
254
255
|
elif isinstance(lineage, WindowItem):
|
|
256
|
+
# this is temporarily guaranteed until we do some upstream work
|
|
257
|
+
assert isinstance(lineage.content, ConceptRef)
|
|
255
258
|
lineage.content.datatype = environment.concepts[
|
|
256
259
|
lineage.content.address
|
|
257
260
|
].datatype
|
|
@@ -542,6 +545,10 @@ class ParseToObjects(Transformer):
|
|
|
542
545
|
|
|
543
546
|
@v_args(meta=True)
|
|
544
547
|
def concept_property_declaration(self, meta: Meta, args) -> Concept:
|
|
548
|
+
unique = False
|
|
549
|
+
if not args[0] == Purpose.PROPERTY:
|
|
550
|
+
unique = True
|
|
551
|
+
args = args[1:]
|
|
545
552
|
metadata = Metadata()
|
|
546
553
|
modifiers = []
|
|
547
554
|
for arg in args:
|
|
@@ -569,7 +576,7 @@ class ParseToObjects(Transformer):
|
|
|
569
576
|
concept = Concept(
|
|
570
577
|
name=name,
|
|
571
578
|
datatype=args[2],
|
|
572
|
-
purpose=
|
|
579
|
+
purpose=Purpose.PROPERTY if not unique else Purpose.UNIQUE_PROPERTY,
|
|
573
580
|
metadata=metadata,
|
|
574
581
|
grain=Grain(components={x.address for x in parents}),
|
|
575
582
|
namespace=namespace,
|
|
@@ -633,7 +640,8 @@ class ParseToObjects(Transformer):
|
|
|
633
640
|
source_value = source_value.content
|
|
634
641
|
|
|
635
642
|
if isinstance(
|
|
636
|
-
source_value,
|
|
643
|
+
source_value,
|
|
644
|
+
(FilterItem, WindowItem, AggregateWrapper, Function, FunctionCallWrapper),
|
|
637
645
|
):
|
|
638
646
|
concept = arbitrary_to_concept(
|
|
639
647
|
source_value,
|
|
@@ -883,6 +891,29 @@ class ParseToObjects(Transformer):
|
|
|
883
891
|
def over_list(self, args):
|
|
884
892
|
return [x for x in args]
|
|
885
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
|
+
|
|
886
917
|
@v_args(meta=True)
|
|
887
918
|
def merge_statement(self, meta: Meta, args) -> MergeStatementV2 | None:
|
|
888
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
|
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
concept_declaration: PURPOSE IDENTIFIER data_type concept_nullable_modifier? metadata?
|
|
22
23
|
//customer_id.property first_name STRING;
|
|
23
24
|
//<customer_id,country>.property local_alias STRING
|
|
24
|
-
|
|
25
|
+
UNIQUE: "UNIQUE"i
|
|
26
|
+
concept_property_declaration: UNIQUE? PROPERTY (prop_ident | IDENTIFIER) data_type concept_nullable_modifier? metadata?
|
|
25
27
|
//metric post_length <- len(post_text);
|
|
26
28
|
concept_derivation: (PURPOSE | AUTO | PROPERTY ) (prop_ident | IDENTIFIER) "<-" expr
|
|
27
29
|
|
|
@@ -75,6 +77,8 @@
|
|
|
75
77
|
|
|
76
78
|
align_clause: align_item ("AND"i align_item)* "AND"i?
|
|
77
79
|
|
|
80
|
+
key_merge_statement: "merge"i "property"i "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "from"i IDENTIFIER
|
|
81
|
+
|
|
78
82
|
merge_statement: "merge"i WILDCARD_IDENTIFIER "into"i SHORTHAND_MODIFIER? WILDCARD_IDENTIFIER
|
|
79
83
|
|
|
80
84
|
// raw sql statement
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|