pytrilogy 0.0.3.53__py3-none-any.whl → 0.0.3.55__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.53.dist-info → pytrilogy-0.0.3.55.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/RECORD +31 -29
- {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/constants.py +2 -0
- trilogy/core/enums.py +5 -0
- trilogy/core/functions.py +3 -0
- trilogy/core/models/author.py +6 -0
- trilogy/core/models/execute.py +207 -2
- trilogy/core/optimization.py +28 -11
- trilogy/core/optimizations/inline_datasource.py +5 -7
- trilogy/core/processing/concept_strategies_v3.py +17 -0
- trilogy/core/processing/node_generators/__init__.py +2 -0
- trilogy/core/processing/node_generators/recursive_node.py +87 -0
- trilogy/core/processing/node_generators/rowset_node.py +1 -3
- trilogy/core/processing/node_generators/window_node.py +13 -4
- trilogy/core/processing/nodes/__init__.py +4 -1
- trilogy/core/processing/nodes/base_node.py +32 -2
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/query_processor.py +7 -1
- trilogy/dialect/base.py +11 -2
- trilogy/dialect/bigquery.py +5 -6
- trilogy/dialect/common.py +19 -3
- trilogy/dialect/duckdb.py +1 -1
- trilogy/dialect/snowflake.py +8 -8
- trilogy/parsing/common.py +3 -0
- trilogy/parsing/parse_engine.py +6 -0
- trilogy/parsing/trilogy.lark +3 -1
- {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
pytrilogy-0.0.3.
|
|
2
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
pytrilogy-0.0.3.55.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=8imvlrqcfkFMW4ZwYgNE-dGvIeWPqAyRgtnjVYICUyw,303
|
|
3
3
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
trilogy/constants.py,sha256=
|
|
4
|
+
trilogy/constants.py,sha256=lv_aJWP6dn6e2aF4BAE72jbnNtceFddfqtiDSsvzno0,1692
|
|
5
5
|
trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
|
|
6
6
|
trilogy/executor.py,sha256=GwNhP9UW4565dxnpHbw-VWNE2lX8uroQJQtSpC_j2pI,16298
|
|
7
7
|
trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
|
|
@@ -11,34 +11,34 @@ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
|
|
|
11
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=XeA25YPIkVdgwrcHYyUGlcaNSrI8W3qfY7hHeZTzYKE,7711
|
|
15
15
|
trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
|
|
16
16
|
trilogy/core/environment_helpers.py,sha256=VvPIiFemqaLLpIpLIqprfu63K7muZ1YzNg7UZIUph8w,8267
|
|
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=poVfAwet1xdxTkC7WL38UmGRDpUVO9iSMNWSagl9_r4,29302
|
|
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=
|
|
23
|
-
trilogy/core/query_processor.py,sha256=
|
|
22
|
+
trilogy/core/optimization.py,sha256=ChIAv0kRmw9RKyDGDCdSdbIN5fJGMkIlE6eVfTFsxg4,8867
|
|
23
|
+
trilogy/core/query_processor.py,sha256=jSS1xZFDqBuI0sZBbuYAAuuVGwas7W-mV_v5oFZJFpA,20275
|
|
24
24
|
trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
|
|
25
25
|
trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
-
trilogy/core/models/author.py,sha256=
|
|
26
|
+
trilogy/core/models/author.py,sha256=N0bdexQaGWgdVg20Uc-5p37qnbTtAxfXo7fMLvX-0QA,77417
|
|
27
27
|
trilogy/core/models/build.py,sha256=yBiOQ4Bhjz09pSD1jSGhhf9QFFQuplrvZ0JQB5-iXHk,63104
|
|
28
28
|
trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
|
|
29
29
|
trilogy/core/models/core.py,sha256=wx6hJcFECMG-Ij972ADNkr-3nFXkYESr82ObPiC46_U,10875
|
|
30
30
|
trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
|
|
31
31
|
trilogy/core/models/environment.py,sha256=AVSrvjNcNX535GhCPtYhCRY2Lp_Hj0tdY3VVt_kZb9Q,27260
|
|
32
|
-
trilogy/core/models/execute.py,sha256=
|
|
32
|
+
trilogy/core/models/execute.py,sha256=_JC93S5tpCQM9jpgmmbd6wkLMEfPvaMZwWZBVcgehlI,42931
|
|
33
33
|
trilogy/core/optimizations/__init__.py,sha256=YH2-mGXZnVDnBcWVi8vTbrdw7Qs5TivG4h38rH3js_I,290
|
|
34
34
|
trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
|
|
35
|
-
trilogy/core/optimizations/inline_datasource.py,sha256=
|
|
35
|
+
trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
|
|
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=wOrcy-I_mSRvhUODmZqhRCCZo1wMyyqH6bm1tmMHdBI,44801
|
|
39
39
|
trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
|
|
40
40
|
trilogy/core/processing/utility.py,sha256=rfzdgl-vWkCyhLzXNNuWgPLK59eiYypQb6TdZKymUqk,21469
|
|
41
|
-
trilogy/core/processing/node_generators/__init__.py,sha256=
|
|
41
|
+
trilogy/core/processing/node_generators/__init__.py,sha256=w8TQQgNhyAra6JQHdg1_Ags4BGyxjXYruu6UeC5yOkI,873
|
|
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=PdysdroW9DUADP7f5Wv_GKPUyCTROZV1g3L45fawxi8,9443
|
|
44
44
|
trilogy/core/processing/node_generators/filter_node.py,sha256=0hdfiS2I-Jvr6P-il3jnAJK-g-DMG7_cFbZGCnLnJAo,10032
|
|
@@ -46,20 +46,22 @@ trilogy/core/processing/node_generators/group_node.py,sha256=nIfiMrJQEksUfqAeeA3
|
|
|
46
46
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
|
|
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/
|
|
49
|
+
trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
|
|
50
|
+
trilogy/core/processing/node_generators/rowset_node.py,sha256=2BiSsegbRF9csJ_Xl8P_CxIm4dAAb7dF29u6v_Odr-A,6709
|
|
50
51
|
trilogy/core/processing/node_generators/select_merge_node.py,sha256=lxXhMhDKGbu67QFNbbAT-BO8gbWppIvjn_hAXpLEPe0,19953
|
|
51
52
|
trilogy/core/processing/node_generators/select_node.py,sha256=Y-zO0AFkTrpi2LyebjpyHU7WWANr7nKZSS9rY7DH4Wo,1888
|
|
52
53
|
trilogy/core/processing/node_generators/synonym_node.py,sha256=9LHK2XHDjbyTLjmDQieskG8fqbiSpRnFOkfrutDnOTE,2258
|
|
53
54
|
trilogy/core/processing/node_generators/union_node.py,sha256=VNo6Oey4p8etU9xrOh2oTT2lIOTvY6PULUPRvVa2uxU,2877
|
|
54
55
|
trilogy/core/processing/node_generators/unnest_node.py,sha256=cOEKnMRzXUW3bwmiOlgn3E1-B38osng0dh2pDykwITY,2410
|
|
55
|
-
trilogy/core/processing/node_generators/window_node.py,sha256=
|
|
56
|
+
trilogy/core/processing/node_generators/window_node.py,sha256=GP3Hvkbb0TDA6ef7W7bmvQEHVH-NRIfBT_0W4fcH3g4,6529
|
|
56
57
|
trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
58
|
trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=GMW07bb6hXurhF0hZLYoMAKSIS65tat5hwBjvqqPeSA,6516
|
|
58
|
-
trilogy/core/processing/nodes/__init__.py,sha256=
|
|
59
|
-
trilogy/core/processing/nodes/base_node.py,sha256=
|
|
59
|
+
trilogy/core/processing/nodes/__init__.py,sha256=9FaUt9_gtsC9Y0-I9BeHTnNlghKaA4iIaLwOM8QKwCE,6117
|
|
60
|
+
trilogy/core/processing/nodes/base_node.py,sha256=IdKR2yaQGY1iRgKXgxF1UtlyuJEmPXWRh0rGFXv7Z_U,18111
|
|
60
61
|
trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
|
|
61
62
|
trilogy/core/processing/nodes/group_node.py,sha256=MUvcOg9U5J6TnWBel8eht9PdI9BfAKjUxmfjP_ZXx9o,10484
|
|
62
63
|
trilogy/core/processing/nodes/merge_node.py,sha256=02oWRca0ba41U6PSAB14jwnWWxoyrvxRPLwkli259SY,15865
|
|
64
|
+
trilogy/core/processing/nodes/recursive_node.py,sha256=k0rizxR8KE64ievfHx_GPfQmU8QAP118Laeyq5BLUOk,1526
|
|
63
65
|
trilogy/core/processing/nodes/select_node_v2.py,sha256=Xyfq8lU7rP7JTAd8VV0ATDNal64n4xIBgWQsOuMe_Ak,8824
|
|
64
66
|
trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ_LpvPokpZKOhx4,1425
|
|
65
67
|
trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
|
|
@@ -70,16 +72,16 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
70
72
|
trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
|
|
71
73
|
trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
|
|
72
74
|
trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
trilogy/dialect/base.py,sha256=
|
|
74
|
-
trilogy/dialect/bigquery.py,sha256=
|
|
75
|
-
trilogy/dialect/common.py,sha256=
|
|
75
|
+
trilogy/dialect/base.py,sha256=SwYg3aCLmam70mlkJURYN42IggmbxviFnMUJ72WYE4g,42940
|
|
76
|
+
trilogy/dialect/bigquery.py,sha256=4u4SuQ67_Zwyu0czyQnBMDUVlegqir0SA30iEbZEAwU,3575
|
|
77
|
+
trilogy/dialect/common.py,sha256=IhW0v5zATvZ2K0vr4Ab4TWpYMKKkGangSpIyqaPYEkw,5762
|
|
76
78
|
trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
|
|
77
79
|
trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
|
|
78
|
-
trilogy/dialect/duckdb.py,sha256=
|
|
80
|
+
trilogy/dialect/duckdb.py,sha256=C5TovwacDXo9YDpMTpPxkH7D0AxQERa7JL1RUkDGVng,3898
|
|
79
81
|
trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
|
|
80
82
|
trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
|
|
81
83
|
trilogy/dialect/presto.py,sha256=Mw7_F8h19mWfuZHkHQJizQWbpu1lIHe6t2PA0r88gsY,3392
|
|
82
|
-
trilogy/dialect/snowflake.py,sha256
|
|
84
|
+
trilogy/dialect/snowflake.py,sha256=-PQABpiyY5zrsXtS0MV4Pe0YFu06hhxuMVD0WA9yBsc,3185
|
|
83
85
|
trilogy/dialect/sql_server.py,sha256=z2Vg7Qvw83rbGiEFIvHHLqVWJTWiz2xs76kpQj4HdTU,3131
|
|
84
86
|
trilogy/hooks/__init__.py,sha256=T3SF3phuUDPLXKGRVE_Lf9mzuwoXWyaLolncR_1kY30,144
|
|
85
87
|
trilogy/hooks/base_hook.py,sha256=I_l-NBMNC7hKTDx1JgHZPVOOCvLQ36m2oIGaR5EUMXY,1180
|
|
@@ -87,13 +89,13 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
|
|
|
87
89
|
trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
|
|
88
90
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
91
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
trilogy/parsing/common.py,sha256=
|
|
92
|
+
trilogy/parsing/common.py,sha256=pvkmT67wYE6HwVecSTfW9RRaeiF6CD6iNHo-e-xiSrY,29901
|
|
91
93
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
92
94
|
trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
|
|
93
95
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
94
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
96
|
+
trilogy/parsing/parse_engine.py,sha256=jgmBqKi5JVR1MSOnEDUWOpGtdUih4TmP7l78yCEoS7o,70785
|
|
95
97
|
trilogy/parsing/render.py,sha256=hI4y-xjXrEXvHslY2l2TQ8ic0zAOpN41ADH37J2_FZY,19047
|
|
96
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
98
|
+
trilogy/parsing/trilogy.lark,sha256=se-gnL3UfrdznVvhNbzzcE5VZxZ18iNMbNMFvRjr30I,14304
|
|
97
99
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
100
|
trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
|
|
99
101
|
trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -102,8 +104,8 @@ trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
|
|
|
102
104
|
trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
|
|
103
105
|
trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
|
|
104
106
|
trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
|
|
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.
|
|
107
|
+
pytrilogy-0.0.3.55.dist-info/METADATA,sha256=cLSfLU5-e2rmY39yQSBiG0H52-3RB_RjF7w8nmu9-Pw,9095
|
|
108
|
+
pytrilogy-0.0.3.55.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
|
109
|
+
pytrilogy-0.0.3.55.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
110
|
+
pytrilogy-0.0.3.55.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
111
|
+
pytrilogy-0.0.3.55.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/constants.py
CHANGED
trilogy/core/enums.py
CHANGED
|
@@ -51,6 +51,7 @@ class Derivation(Enum):
|
|
|
51
51
|
ROOT = "root"
|
|
52
52
|
ROWSET = "rowset"
|
|
53
53
|
MULTISELECT = "multiselect"
|
|
54
|
+
RECURSIVE = "recursive"
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
class Granularity(Enum):
|
|
@@ -117,6 +118,7 @@ class FunctionType(Enum):
|
|
|
117
118
|
|
|
118
119
|
# structural
|
|
119
120
|
UNNEST = "unnest"
|
|
121
|
+
RECURSE_EDGE = "recurse_edge"
|
|
120
122
|
|
|
121
123
|
UNION = "union"
|
|
122
124
|
|
|
@@ -233,6 +235,8 @@ class FunctionClass(Enum):
|
|
|
233
235
|
|
|
234
236
|
ONE_TO_MANY = [FunctionType.UNNEST]
|
|
235
237
|
|
|
238
|
+
RECURSIVE = [FunctionType.RECURSE_EDGE]
|
|
239
|
+
|
|
236
240
|
|
|
237
241
|
class Boolean(Enum):
|
|
238
242
|
TRUE = "true"
|
|
@@ -333,6 +337,7 @@ class SourceType(Enum):
|
|
|
333
337
|
MERGE = "merge"
|
|
334
338
|
BASIC = "basic"
|
|
335
339
|
UNION = "union"
|
|
340
|
+
RECURSIVE = "recursive"
|
|
336
341
|
|
|
337
342
|
|
|
338
343
|
class ShowCategory(Enum):
|
trilogy/core/functions.py
CHANGED
|
@@ -190,6 +190,9 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
190
190
|
output_type_function=get_unnest_output_type,
|
|
191
191
|
arg_count=1,
|
|
192
192
|
),
|
|
193
|
+
FunctionType.RECURSE_EDGE: FunctionConfig(
|
|
194
|
+
arg_count=2,
|
|
195
|
+
),
|
|
193
196
|
FunctionType.GROUP: FunctionConfig(
|
|
194
197
|
arg_count=-1,
|
|
195
198
|
output_type_function=lambda args: get_output_type_at_index(args, 0),
|
trilogy/core/models/author.py
CHANGED
|
@@ -1164,6 +1164,12 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
|
|
|
1164
1164
|
and lineage.operator == FunctionType.UNNEST
|
|
1165
1165
|
):
|
|
1166
1166
|
return Derivation.UNNEST
|
|
1167
|
+
elif (
|
|
1168
|
+
lineage
|
|
1169
|
+
and isinstance(lineage, (BuildFunction, Function))
|
|
1170
|
+
and lineage.operator == FunctionType.RECURSE_EDGE
|
|
1171
|
+
):
|
|
1172
|
+
return Derivation.RECURSIVE
|
|
1167
1173
|
elif (
|
|
1168
1174
|
lineage
|
|
1169
1175
|
and isinstance(lineage, (BuildFunction, Function))
|
trilogy/core/models/execute.py
CHANGED
|
@@ -12,9 +12,16 @@ from pydantic import (
|
|
|
12
12
|
model_validator,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
from trilogy.constants import
|
|
15
|
+
from trilogy.constants import (
|
|
16
|
+
CONFIG,
|
|
17
|
+
DEFAULT_NAMESPACE,
|
|
18
|
+
RECURSIVE_GATING_CONCEPT,
|
|
19
|
+
MagicConstants,
|
|
20
|
+
logger,
|
|
21
|
+
)
|
|
16
22
|
from trilogy.core.constants import CONSTANT_DATASET
|
|
17
23
|
from trilogy.core.enums import (
|
|
24
|
+
ComparisonOperator,
|
|
18
25
|
Derivation,
|
|
19
26
|
FunctionType,
|
|
20
27
|
Granularity,
|
|
@@ -24,16 +31,20 @@ from trilogy.core.enums import (
|
|
|
24
31
|
SourceType,
|
|
25
32
|
)
|
|
26
33
|
from trilogy.core.models.build import (
|
|
34
|
+
BuildCaseElse,
|
|
35
|
+
BuildCaseWhen,
|
|
27
36
|
BuildComparison,
|
|
28
37
|
BuildConcept,
|
|
29
38
|
BuildConditional,
|
|
30
39
|
BuildDatasource,
|
|
40
|
+
BuildExpr,
|
|
31
41
|
BuildFunction,
|
|
32
42
|
BuildGrain,
|
|
33
43
|
BuildOrderBy,
|
|
34
44
|
BuildParamaterizedConceptReference,
|
|
35
45
|
BuildParenthetical,
|
|
36
46
|
BuildRowsetItem,
|
|
47
|
+
DataType,
|
|
37
48
|
LooseBuildConceptList,
|
|
38
49
|
)
|
|
39
50
|
from trilogy.core.models.datasource import Address
|
|
@@ -841,6 +852,195 @@ class QueryDatasource(BaseModel):
|
|
|
841
852
|
return self.identifier
|
|
842
853
|
|
|
843
854
|
|
|
855
|
+
class AliasedExpression(BaseModel):
|
|
856
|
+
expr: BuildExpr
|
|
857
|
+
alias: str
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
class RecursiveCTE(CTE):
|
|
861
|
+
|
|
862
|
+
def generate_loop_functions(
|
|
863
|
+
self,
|
|
864
|
+
recursive_derived: BuildConcept,
|
|
865
|
+
left_recurse_concept: BuildConcept,
|
|
866
|
+
right_recurse_concept: BuildConcept,
|
|
867
|
+
) -> tuple[BuildConcept, BuildConcept, BuildConcept]:
|
|
868
|
+
|
|
869
|
+
join_gate = BuildConcept(
|
|
870
|
+
name=RECURSIVE_GATING_CONCEPT,
|
|
871
|
+
namespace=DEFAULT_NAMESPACE,
|
|
872
|
+
grain=recursive_derived.grain,
|
|
873
|
+
build_is_aggregate=False,
|
|
874
|
+
datatype=DataType.BOOL,
|
|
875
|
+
purpose=Purpose.KEY,
|
|
876
|
+
derivation=Derivation.BASIC,
|
|
877
|
+
lineage=BuildFunction(
|
|
878
|
+
operator=FunctionType.CASE,
|
|
879
|
+
arguments=[
|
|
880
|
+
BuildCaseWhen(
|
|
881
|
+
comparison=BuildComparison(
|
|
882
|
+
left=right_recurse_concept,
|
|
883
|
+
right=MagicConstants.NULL,
|
|
884
|
+
operator=ComparisonOperator.IS,
|
|
885
|
+
),
|
|
886
|
+
expr=True,
|
|
887
|
+
),
|
|
888
|
+
BuildCaseElse(expr=False),
|
|
889
|
+
],
|
|
890
|
+
output_datatype=DataType.BOOL,
|
|
891
|
+
output_purpose=Purpose.KEY,
|
|
892
|
+
),
|
|
893
|
+
)
|
|
894
|
+
bottom_join_gate = BuildConcept(
|
|
895
|
+
name=f"{RECURSIVE_GATING_CONCEPT}_two",
|
|
896
|
+
namespace=DEFAULT_NAMESPACE,
|
|
897
|
+
grain=recursive_derived.grain,
|
|
898
|
+
build_is_aggregate=False,
|
|
899
|
+
datatype=DataType.BOOL,
|
|
900
|
+
purpose=Purpose.KEY,
|
|
901
|
+
derivation=Derivation.BASIC,
|
|
902
|
+
lineage=BuildFunction(
|
|
903
|
+
operator=FunctionType.CASE,
|
|
904
|
+
arguments=[
|
|
905
|
+
BuildCaseWhen(
|
|
906
|
+
comparison=BuildComparison(
|
|
907
|
+
left=right_recurse_concept,
|
|
908
|
+
right=MagicConstants.NULL,
|
|
909
|
+
operator=ComparisonOperator.IS,
|
|
910
|
+
),
|
|
911
|
+
expr=True,
|
|
912
|
+
),
|
|
913
|
+
BuildCaseElse(expr=False),
|
|
914
|
+
],
|
|
915
|
+
output_datatype=DataType.BOOL,
|
|
916
|
+
output_purpose=Purpose.KEY,
|
|
917
|
+
),
|
|
918
|
+
)
|
|
919
|
+
bottom_derivation = BuildConcept(
|
|
920
|
+
name=recursive_derived.name + "_bottom",
|
|
921
|
+
namespace=recursive_derived.namespace,
|
|
922
|
+
grain=recursive_derived.grain,
|
|
923
|
+
build_is_aggregate=False,
|
|
924
|
+
datatype=recursive_derived.datatype,
|
|
925
|
+
purpose=recursive_derived.purpose,
|
|
926
|
+
derivation=Derivation.RECURSIVE,
|
|
927
|
+
lineage=BuildFunction(
|
|
928
|
+
operator=FunctionType.CASE,
|
|
929
|
+
arguments=[
|
|
930
|
+
BuildCaseWhen(
|
|
931
|
+
comparison=BuildComparison(
|
|
932
|
+
left=right_recurse_concept,
|
|
933
|
+
right=MagicConstants.NULL,
|
|
934
|
+
operator=ComparisonOperator.IS,
|
|
935
|
+
),
|
|
936
|
+
expr=recursive_derived,
|
|
937
|
+
),
|
|
938
|
+
BuildCaseElse(expr=right_recurse_concept),
|
|
939
|
+
],
|
|
940
|
+
output_datatype=recursive_derived.datatype,
|
|
941
|
+
output_purpose=recursive_derived.purpose,
|
|
942
|
+
),
|
|
943
|
+
)
|
|
944
|
+
return bottom_derivation, join_gate, bottom_join_gate
|
|
945
|
+
|
|
946
|
+
@property
|
|
947
|
+
def internal_ctes(self) -> List[CTE]:
|
|
948
|
+
filtered_output = [
|
|
949
|
+
x for x in self.output_columns if x.name != RECURSIVE_GATING_CONCEPT
|
|
950
|
+
]
|
|
951
|
+
recursive_derived = [
|
|
952
|
+
x for x in self.output_columns if x.derivation == Derivation.RECURSIVE
|
|
953
|
+
][0]
|
|
954
|
+
if not isinstance(recursive_derived.lineage, BuildFunction):
|
|
955
|
+
raise SyntaxError(
|
|
956
|
+
"Recursive CTEs must have a function lineage, found"
|
|
957
|
+
f" {recursive_derived.lineage}"
|
|
958
|
+
)
|
|
959
|
+
left_recurse_concept = recursive_derived.lineage.concept_arguments[0]
|
|
960
|
+
right_recurse_concept = recursive_derived.lineage.concept_arguments[1]
|
|
961
|
+
parent_ctes: List[CTE | UnionCTE]
|
|
962
|
+
if self.parent_ctes:
|
|
963
|
+
base = self.parent_ctes[0]
|
|
964
|
+
loop_input_cte = base
|
|
965
|
+
parent_ctes = [base]
|
|
966
|
+
parent_identifier = base.identifier
|
|
967
|
+
else:
|
|
968
|
+
raise SyntaxError("Recursive CTEs must have a parent CTE currently")
|
|
969
|
+
bottom_derivation, join_gate, bottom_join_gate = self.generate_loop_functions(
|
|
970
|
+
recursive_derived, left_recurse_concept, right_recurse_concept
|
|
971
|
+
)
|
|
972
|
+
base_output = [*filtered_output, join_gate]
|
|
973
|
+
bottom_output = []
|
|
974
|
+
for x in filtered_output:
|
|
975
|
+
if x.address == recursive_derived.address:
|
|
976
|
+
bottom_output.append(bottom_derivation)
|
|
977
|
+
else:
|
|
978
|
+
bottom_output.append(x)
|
|
979
|
+
|
|
980
|
+
bottom_output = [*bottom_output, bottom_join_gate]
|
|
981
|
+
top = CTE(
|
|
982
|
+
name=self.name,
|
|
983
|
+
source=self.source,
|
|
984
|
+
output_columns=base_output,
|
|
985
|
+
source_map=self.source_map,
|
|
986
|
+
grain=self.grain,
|
|
987
|
+
existence_source_map=self.existence_source_map,
|
|
988
|
+
parent_ctes=self.parent_ctes,
|
|
989
|
+
joins=self.joins,
|
|
990
|
+
condition=self.condition,
|
|
991
|
+
partial_concepts=self.partial_concepts,
|
|
992
|
+
hidden_concepts=self.hidden_concepts,
|
|
993
|
+
nullable_concepts=self.nullable_concepts,
|
|
994
|
+
join_derived_concepts=self.join_derived_concepts,
|
|
995
|
+
group_to_grain=self.group_to_grain,
|
|
996
|
+
order_by=self.order_by,
|
|
997
|
+
limit=self.limit,
|
|
998
|
+
)
|
|
999
|
+
top_cte_array: list[CTE | UnionCTE] = [top]
|
|
1000
|
+
bottom_source_map = {
|
|
1001
|
+
left_recurse_concept.address: [top.identifier],
|
|
1002
|
+
right_recurse_concept.address: [parent_identifier],
|
|
1003
|
+
# recursive_derived.address: self.source_map[recursive_derived.address],
|
|
1004
|
+
join_gate.address: [top.identifier],
|
|
1005
|
+
recursive_derived.address: [top.identifier],
|
|
1006
|
+
}
|
|
1007
|
+
bottom = CTE(
|
|
1008
|
+
name=self.name,
|
|
1009
|
+
source=self.source,
|
|
1010
|
+
output_columns=bottom_output,
|
|
1011
|
+
source_map=bottom_source_map,
|
|
1012
|
+
grain=self.grain,
|
|
1013
|
+
existence_source_map=self.existence_source_map,
|
|
1014
|
+
parent_ctes=top_cte_array + parent_ctes,
|
|
1015
|
+
joins=[
|
|
1016
|
+
Join(
|
|
1017
|
+
right_cte=loop_input_cte,
|
|
1018
|
+
jointype=JoinType.INNER,
|
|
1019
|
+
joinkey_pairs=[
|
|
1020
|
+
CTEConceptPair(
|
|
1021
|
+
left=recursive_derived,
|
|
1022
|
+
right=left_recurse_concept,
|
|
1023
|
+
existing_datasource=loop_input_cte.source,
|
|
1024
|
+
modifiers=[],
|
|
1025
|
+
cte=top,
|
|
1026
|
+
)
|
|
1027
|
+
],
|
|
1028
|
+
condition=BuildComparison(
|
|
1029
|
+
left=join_gate, right=True, operator=ComparisonOperator.IS_NOT
|
|
1030
|
+
),
|
|
1031
|
+
)
|
|
1032
|
+
],
|
|
1033
|
+
partial_concepts=self.partial_concepts,
|
|
1034
|
+
hidden_concepts=self.hidden_concepts,
|
|
1035
|
+
nullable_concepts=self.nullable_concepts,
|
|
1036
|
+
join_derived_concepts=self.join_derived_concepts,
|
|
1037
|
+
group_to_grain=self.group_to_grain,
|
|
1038
|
+
order_by=self.order_by,
|
|
1039
|
+
limit=self.limit,
|
|
1040
|
+
)
|
|
1041
|
+
return [top, bottom]
|
|
1042
|
+
|
|
1043
|
+
|
|
844
1044
|
class UnionCTE(BaseModel):
|
|
845
1045
|
name: str
|
|
846
1046
|
source: QueryDatasource
|
|
@@ -891,6 +1091,10 @@ class UnionCTE(BaseModel):
|
|
|
891
1091
|
def condition(self, value):
|
|
892
1092
|
raise NotImplementedError
|
|
893
1093
|
|
|
1094
|
+
@property
|
|
1095
|
+
def identifier(self) -> str:
|
|
1096
|
+
return self.name
|
|
1097
|
+
|
|
894
1098
|
@property
|
|
895
1099
|
def safe_identifier(self):
|
|
896
1100
|
return self.name
|
|
@@ -906,12 +1110,13 @@ class UnionCTE(BaseModel):
|
|
|
906
1110
|
|
|
907
1111
|
|
|
908
1112
|
class Join(BaseModel):
|
|
909
|
-
right_cte: CTE
|
|
1113
|
+
right_cte: CTE | UnionCTE
|
|
910
1114
|
jointype: JoinType
|
|
911
1115
|
left_cte: CTE | None = None
|
|
912
1116
|
joinkey_pairs: List[CTEConceptPair] | None = None
|
|
913
1117
|
inlined_ctes: set[str] = Field(default_factory=set)
|
|
914
1118
|
quote: str | None = None
|
|
1119
|
+
condition: BuildConditional | BuildComparison | BuildParenthetical | None = None
|
|
915
1120
|
|
|
916
1121
|
def inline_cte(self, cte: CTE):
|
|
917
1122
|
self.inlined_ctes.add(cte.name)
|
trilogy/core/optimization.py
CHANGED
|
@@ -3,7 +3,7 @@ from trilogy.core.enums import BooleanOperator, Derivation
|
|
|
3
3
|
from trilogy.core.models.build import (
|
|
4
4
|
BuildConditional,
|
|
5
5
|
)
|
|
6
|
-
from trilogy.core.models.execute import CTE, UnionCTE
|
|
6
|
+
from trilogy.core.models.execute import CTE, RecursiveCTE, UnionCTE
|
|
7
7
|
from trilogy.core.optimizations import (
|
|
8
8
|
InlineDatasource,
|
|
9
9
|
OptimizationRule,
|
|
@@ -120,13 +120,20 @@ def gen_inverse_map(input: list[CTE | UnionCTE]) -> dict[str, list[CTE | UnionCT
|
|
|
120
120
|
return inverse_map
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
SENSITIVE_DERIVATIONS = [
|
|
124
|
+
Derivation.UNNEST,
|
|
125
|
+
Derivation.WINDOW,
|
|
126
|
+
Derivation.RECURSIVE,
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
123
130
|
def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
|
|
124
131
|
# if isinstance(select, (PersistStatement, MultiSelectStatement)):
|
|
125
132
|
# return False
|
|
126
133
|
if len(cte.parent_ctes) != 1:
|
|
127
134
|
return None
|
|
128
135
|
direct_parent = cte.parent_ctes[0]
|
|
129
|
-
if isinstance(direct_parent, UnionCTE):
|
|
136
|
+
if isinstance(direct_parent, (UnionCTE, RecursiveCTE)):
|
|
130
137
|
return None
|
|
131
138
|
|
|
132
139
|
output_addresses = set([x.address for x in cte.output_columns])
|
|
@@ -151,21 +158,31 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
|
|
|
151
158
|
]
|
|
152
159
|
condition_arguments = cte.condition.row_arguments if cte.condition else []
|
|
153
160
|
for x in derived_concepts:
|
|
154
|
-
if x.derivation
|
|
155
|
-
return None
|
|
156
|
-
if x.derivation == Derivation.UNNEST:
|
|
157
|
-
return None
|
|
158
|
-
if x.derivation == Derivation.AGGREGATE:
|
|
161
|
+
if x.derivation in SENSITIVE_DERIVATIONS:
|
|
159
162
|
return None
|
|
160
163
|
for x in parent_derived_concepts:
|
|
161
164
|
if x.address not in condition_arguments:
|
|
162
165
|
continue
|
|
163
|
-
if x.derivation
|
|
164
|
-
return None
|
|
165
|
-
if x.derivation == Derivation.WINDOW:
|
|
166
|
+
if x.derivation in SENSITIVE_DERIVATIONS:
|
|
166
167
|
return None
|
|
168
|
+
for x in condition_arguments:
|
|
169
|
+
# if it's derived in the parent
|
|
170
|
+
if x.address in parent_derived_concepts:
|
|
171
|
+
if x.derivation in SENSITIVE_DERIVATIONS:
|
|
172
|
+
return None
|
|
173
|
+
# this maybe needs to be recursive if we flatten a ton of derivation
|
|
174
|
+
# into one CTE
|
|
175
|
+
if not x.lineage:
|
|
176
|
+
continue
|
|
177
|
+
for z in x.lineage.concept_arguments:
|
|
178
|
+
# if it was preexisting in the parent, it's safe
|
|
179
|
+
if z.address in direct_parent.source.input_concepts:
|
|
180
|
+
continue
|
|
181
|
+
# otherwise if it's dangerous, play it safe.
|
|
182
|
+
if z.derivation in SENSITIVE_DERIVATIONS:
|
|
183
|
+
return None
|
|
167
184
|
logger.info(
|
|
168
|
-
f"[Optimization][EarlyReturn] Removing redundant output CTE with derived_concepts {[x.address for x in derived_concepts]}"
|
|
185
|
+
f"[Optimization][EarlyReturn] Removing redundant output CTE {cte.name} with derived_concepts {[x.address for x in derived_concepts]}"
|
|
169
186
|
)
|
|
170
187
|
return direct_parent
|
|
171
188
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
|
|
3
3
|
from trilogy.constants import CONFIG
|
|
4
|
-
|
|
5
|
-
# from trilogy.core.models.datasource import Datasource
|
|
6
4
|
from trilogy.core.models.build import BuildDatasource
|
|
7
|
-
from trilogy.core.models.execute import
|
|
8
|
-
CTE,
|
|
9
|
-
UnionCTE,
|
|
10
|
-
)
|
|
5
|
+
from trilogy.core.models.execute import CTE, RecursiveCTE, UnionCTE
|
|
11
6
|
from trilogy.core.optimizations.base_optimization import OptimizationRule
|
|
12
7
|
|
|
13
8
|
|
|
@@ -24,7 +19,8 @@ class InlineDatasource(OptimizationRule):
|
|
|
24
19
|
return any(
|
|
25
20
|
self.optimize(x, inverse_map=inverse_map) for x in cte.internal_ctes
|
|
26
21
|
)
|
|
27
|
-
|
|
22
|
+
if isinstance(cte, RecursiveCTE):
|
|
23
|
+
return False
|
|
28
24
|
if not cte.parent_ctes:
|
|
29
25
|
return False
|
|
30
26
|
|
|
@@ -36,6 +32,8 @@ class InlineDatasource(OptimizationRule):
|
|
|
36
32
|
for parent_cte in cte.parent_ctes:
|
|
37
33
|
if isinstance(parent_cte, UnionCTE):
|
|
38
34
|
continue
|
|
35
|
+
if isinstance(parent_cte, RecursiveCTE):
|
|
36
|
+
continue
|
|
39
37
|
if not parent_cte.is_root_datasource:
|
|
40
38
|
self.debug(f"Cannot inline: parent {parent_cte.name} is not root")
|
|
41
39
|
continue
|
|
@@ -24,6 +24,7 @@ from trilogy.core.processing.node_generators import (
|
|
|
24
24
|
gen_group_to_node,
|
|
25
25
|
gen_merge_node,
|
|
26
26
|
gen_multiselect_node,
|
|
27
|
+
gen_recursive_node,
|
|
27
28
|
gen_rowset_node,
|
|
28
29
|
gen_synonym_node,
|
|
29
30
|
gen_union_node,
|
|
@@ -150,6 +151,7 @@ def get_priority_concept(
|
|
|
150
151
|
+ [c for c in remaining_concept if c.derivation == Derivation.FILTER]
|
|
151
152
|
# unnests are weird?
|
|
152
153
|
+ [c for c in remaining_concept if c.derivation == Derivation.UNNEST]
|
|
154
|
+
+ [c for c in remaining_concept if c.derivation == Derivation.RECURSIVE]
|
|
153
155
|
+ [c for c in remaining_concept if c.derivation == Derivation.BASIC]
|
|
154
156
|
# finally our plain selects
|
|
155
157
|
+ [
|
|
@@ -294,6 +296,20 @@ def generate_node(
|
|
|
294
296
|
source_concepts=source_concepts,
|
|
295
297
|
conditions=conditions,
|
|
296
298
|
)
|
|
299
|
+
elif concept.derivation == Derivation.RECURSIVE:
|
|
300
|
+
logger.info(
|
|
301
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating recursive node with optional {[x.address for x in local_optional]} and condition {conditions}"
|
|
302
|
+
)
|
|
303
|
+
return gen_recursive_node(
|
|
304
|
+
concept,
|
|
305
|
+
local_optional,
|
|
306
|
+
history=history,
|
|
307
|
+
environment=environment,
|
|
308
|
+
g=g,
|
|
309
|
+
depth=depth + 1,
|
|
310
|
+
source_concepts=source_concepts,
|
|
311
|
+
conditions=conditions,
|
|
312
|
+
)
|
|
297
313
|
elif concept.derivation == Derivation.UNION:
|
|
298
314
|
logger.info(
|
|
299
315
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating union node with optional {[x.address for x in local_optional]} and condition {conditions}"
|
|
@@ -920,6 +936,7 @@ def _search_concepts(
|
|
|
920
936
|
Derivation.FILTER,
|
|
921
937
|
Derivation.WINDOW,
|
|
922
938
|
Derivation.UNNEST,
|
|
939
|
+
Derivation.RECURSIVE,
|
|
923
940
|
Derivation.ROWSET,
|
|
924
941
|
Derivation.BASIC,
|
|
925
942
|
Derivation.MULTISELECT,
|
|
@@ -4,6 +4,7 @@ from .group_node import gen_group_node
|
|
|
4
4
|
from .group_to_node import gen_group_to_node
|
|
5
5
|
from .multiselect_node import gen_multiselect_node
|
|
6
6
|
from .node_merge_node import gen_merge_node
|
|
7
|
+
from .recursive_node import gen_recursive_node
|
|
7
8
|
from .rowset_node import gen_rowset_node
|
|
8
9
|
from .select_node import gen_select_node
|
|
9
10
|
from .synonym_node import gen_synonym_node
|
|
@@ -24,4 +25,5 @@ __all__ = [
|
|
|
24
25
|
"gen_rowset_node",
|
|
25
26
|
"gen_multiselect_node",
|
|
26
27
|
"gen_synonym_node",
|
|
28
|
+
"gen_recursive_node",
|
|
27
29
|
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from trilogy.constants import DEFAULT_NAMESPACE, RECURSIVE_GATING_CONCEPT, logger
|
|
4
|
+
from trilogy.core.models.build import (
|
|
5
|
+
BuildComparison,
|
|
6
|
+
BuildConcept,
|
|
7
|
+
BuildFunction,
|
|
8
|
+
BuildGrain,
|
|
9
|
+
BuildWhereClause,
|
|
10
|
+
ComparisonOperator,
|
|
11
|
+
DataType,
|
|
12
|
+
Derivation,
|
|
13
|
+
Purpose,
|
|
14
|
+
)
|
|
15
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
16
|
+
from trilogy.core.processing.nodes import History, RecursiveNode, StrategyNode
|
|
17
|
+
from trilogy.core.processing.utility import padding
|
|
18
|
+
|
|
19
|
+
LOGGER_PREFIX = "[GEN_RECURSIVE_NODE]"
|
|
20
|
+
|
|
21
|
+
GATING_CONCEPT = BuildConcept(
|
|
22
|
+
name=RECURSIVE_GATING_CONCEPT,
|
|
23
|
+
namespace=DEFAULT_NAMESPACE,
|
|
24
|
+
grain=BuildGrain(),
|
|
25
|
+
build_is_aggregate=False,
|
|
26
|
+
datatype=DataType.BOOL,
|
|
27
|
+
purpose=Purpose.KEY,
|
|
28
|
+
derivation=Derivation.BASIC,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def gen_recursive_node(
|
|
33
|
+
concept: BuildConcept,
|
|
34
|
+
local_optional: List[BuildConcept],
|
|
35
|
+
history: History,
|
|
36
|
+
environment: BuildEnvironment,
|
|
37
|
+
g,
|
|
38
|
+
depth: int,
|
|
39
|
+
source_concepts,
|
|
40
|
+
conditions: BuildWhereClause | None = None,
|
|
41
|
+
) -> StrategyNode | None:
|
|
42
|
+
arguments = []
|
|
43
|
+
if isinstance(concept.lineage, BuildFunction):
|
|
44
|
+
arguments = concept.lineage.concept_arguments
|
|
45
|
+
logger.info(
|
|
46
|
+
f"{padding(depth)}{LOGGER_PREFIX} Fetching recursive node for {concept.name} with arguments {arguments} and conditions {conditions}"
|
|
47
|
+
)
|
|
48
|
+
parent = source_concepts(
|
|
49
|
+
mandatory_list=arguments,
|
|
50
|
+
environment=environment,
|
|
51
|
+
g=g,
|
|
52
|
+
depth=depth + 1,
|
|
53
|
+
history=history,
|
|
54
|
+
# conditions=conditions,
|
|
55
|
+
)
|
|
56
|
+
if not parent:
|
|
57
|
+
logger.info(
|
|
58
|
+
f"{padding(depth)}{LOGGER_PREFIX} could not find recursive node parents"
|
|
59
|
+
)
|
|
60
|
+
return None
|
|
61
|
+
outputs = (
|
|
62
|
+
[concept]
|
|
63
|
+
+ arguments
|
|
64
|
+
+ [
|
|
65
|
+
GATING_CONCEPT,
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
base = RecursiveNode(
|
|
69
|
+
input_concepts=arguments,
|
|
70
|
+
output_concepts=outputs,
|
|
71
|
+
environment=environment,
|
|
72
|
+
parents=([parent] if (arguments or local_optional) else []),
|
|
73
|
+
# preexisting_conditions=conditions
|
|
74
|
+
)
|
|
75
|
+
# TODO:
|
|
76
|
+
# recursion will result in a union; group up to our final targets
|
|
77
|
+
wrapped_base = StrategyNode(
|
|
78
|
+
input_concepts=outputs,
|
|
79
|
+
output_concepts=outputs,
|
|
80
|
+
environment=environment,
|
|
81
|
+
parents=[base],
|
|
82
|
+
depth=depth,
|
|
83
|
+
conditions=BuildComparison(
|
|
84
|
+
left=GATING_CONCEPT, right=True, operator=ComparisonOperator.IS
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
return wrapped_base
|
|
@@ -95,9 +95,7 @@ def gen_rowset_node(
|
|
|
95
95
|
f"{padding(depth)}{LOGGER_PREFIX} hiding {final_hidden} local optional {local_optional}"
|
|
96
96
|
)
|
|
97
97
|
node.hide_output_concepts(final_hidden)
|
|
98
|
-
|
|
99
|
-
# assume grain to be output of select
|
|
100
|
-
# but don't include anything hidden(the non-rowset concepts)
|
|
98
|
+
|
|
101
99
|
node.grain = BuildGrain.from_concepts(
|
|
102
100
|
[
|
|
103
101
|
x
|
|
@@ -11,7 +11,12 @@ from trilogy.core.models.build_environment import BuildEnvironment
|
|
|
11
11
|
from trilogy.core.processing.node_generators.common import (
|
|
12
12
|
gen_enrichment_node,
|
|
13
13
|
)
|
|
14
|
-
from trilogy.core.processing.nodes import
|
|
14
|
+
from trilogy.core.processing.nodes import (
|
|
15
|
+
History,
|
|
16
|
+
StrategyNode,
|
|
17
|
+
WhereSafetyNode,
|
|
18
|
+
WindowNode,
|
|
19
|
+
)
|
|
15
20
|
from trilogy.core.processing.utility import create_log_lambda, padding
|
|
16
21
|
from trilogy.utility import unique
|
|
17
22
|
|
|
@@ -71,10 +76,13 @@ def gen_window_node(
|
|
|
71
76
|
if equivalent_optional:
|
|
72
77
|
for x in equivalent_optional:
|
|
73
78
|
assert isinstance(x.lineage, WINDOW_TYPES)
|
|
79
|
+
base, parents = resolve_window_parent_concepts(x, environment)
|
|
74
80
|
logger.info(
|
|
75
|
-
f"{padding(depth)}{LOGGER_PREFIX} found equivalent optional {x} with parents {
|
|
81
|
+
f"{padding(depth)}{LOGGER_PREFIX} found equivalent optional {x} with parents {parents}"
|
|
76
82
|
)
|
|
77
83
|
additional_outputs.append(x)
|
|
84
|
+
# also append the base concept it's being grouped over
|
|
85
|
+
targets.append(base)
|
|
78
86
|
|
|
79
87
|
grain_equivalents = [
|
|
80
88
|
x
|
|
@@ -85,7 +93,8 @@ def gen_window_node(
|
|
|
85
93
|
]
|
|
86
94
|
|
|
87
95
|
for x in grain_equivalents:
|
|
88
|
-
|
|
96
|
+
if x.address in additional_outputs:
|
|
97
|
+
continue
|
|
89
98
|
targets.append(x)
|
|
90
99
|
|
|
91
100
|
# finally, the ones we'll need to enrich
|
|
@@ -134,7 +143,7 @@ def gen_window_node(
|
|
|
134
143
|
_window_node.rebuild_cache()
|
|
135
144
|
_window_node.resolve()
|
|
136
145
|
|
|
137
|
-
window_node =
|
|
146
|
+
window_node = WhereSafetyNode(
|
|
138
147
|
input_concepts=[concept] + additional_outputs + parent_concepts + targets,
|
|
139
148
|
output_concepts=[concept] + additional_outputs + parent_concepts + targets,
|
|
140
149
|
environment=environment,
|
|
@@ -6,10 +6,11 @@ from trilogy.core.models.build import BuildConcept, BuildWhereClause
|
|
|
6
6
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
7
7
|
from trilogy.core.models.environment import Environment
|
|
8
8
|
|
|
9
|
-
from .base_node import NodeJoin, StrategyNode
|
|
9
|
+
from .base_node import NodeJoin, StrategyNode, WhereSafetyNode
|
|
10
10
|
from .filter_node import FilterNode
|
|
11
11
|
from .group_node import GroupNode
|
|
12
12
|
from .merge_node import MergeNode
|
|
13
|
+
from .recursive_node import RecursiveNode
|
|
13
14
|
from .select_node_v2 import ConstantNode, SelectNode
|
|
14
15
|
from .union_node import UnionNode
|
|
15
16
|
from .unnest_node import UnnestNode
|
|
@@ -193,4 +194,6 @@ __all__ = [
|
|
|
193
194
|
"UnnestNode",
|
|
194
195
|
"UnionNode",
|
|
195
196
|
"History",
|
|
197
|
+
"WhereSafetyNode",
|
|
198
|
+
"RecursiveNode",
|
|
196
199
|
]
|
|
@@ -291,9 +291,14 @@ class StrategyNode:
|
|
|
291
291
|
def add_output_concept(self, concept: BuildConcept, rebuild: bool = True):
|
|
292
292
|
return self.add_output_concepts([concept], rebuild)
|
|
293
293
|
|
|
294
|
-
def hide_output_concepts(
|
|
294
|
+
def hide_output_concepts(
|
|
295
|
+
self, concepts: List[BuildConcept] | list[str] | set[str], rebuild: bool = True
|
|
296
|
+
):
|
|
295
297
|
for x in concepts:
|
|
296
|
-
|
|
298
|
+
if isinstance(x, BuildConcept):
|
|
299
|
+
self.hidden_concepts.add(x.address)
|
|
300
|
+
else:
|
|
301
|
+
self.hidden_concepts.add(x)
|
|
297
302
|
if rebuild:
|
|
298
303
|
self.rebuild_cache()
|
|
299
304
|
return self
|
|
@@ -471,3 +476,28 @@ class NodeJoin:
|
|
|
471
476
|
f" {self.right_node} on"
|
|
472
477
|
f" {','.join([str(k) for k in self.concepts])}"
|
|
473
478
|
)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class WhereSafetyNode(StrategyNode):
|
|
482
|
+
"""Specialized node to be used to pad certain
|
|
483
|
+
select outputs that can't be immediately used in a where
|
|
484
|
+
clause; eg window functions. Will remove itself if not required."""
|
|
485
|
+
|
|
486
|
+
def resolve(self) -> QueryDatasource:
|
|
487
|
+
if not self.conditions and len(self.parents) == 1:
|
|
488
|
+
parent = self.parents[0]
|
|
489
|
+
parent = parent.copy()
|
|
490
|
+
# avoid performance hit by not rebuilding until end
|
|
491
|
+
parent.set_output_concepts(self.output_concepts, rebuild=False)
|
|
492
|
+
parent.hide_output_concepts(self.hidden_concepts, rebuild=False)
|
|
493
|
+
|
|
494
|
+
# these conditions
|
|
495
|
+
if self.preexisting_conditions:
|
|
496
|
+
parent.set_preexisting_conditions(self.preexisting_conditions)
|
|
497
|
+
# TODO: add a helper for this
|
|
498
|
+
parent.ordering = self.ordering
|
|
499
|
+
|
|
500
|
+
# actually build the node
|
|
501
|
+
parent.rebuild_cache()
|
|
502
|
+
return parent.resolve()
|
|
503
|
+
return super().resolve()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from trilogy.core.enums import SourceType
|
|
4
|
+
from trilogy.core.models.build import BuildConcept
|
|
5
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
6
|
+
from trilogy.core.models.execute import QueryDatasource
|
|
7
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RecursiveNode(StrategyNode):
|
|
11
|
+
"""Union nodes represent combining two keyspaces"""
|
|
12
|
+
|
|
13
|
+
source_type = SourceType.RECURSIVE
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
input_concepts: List[BuildConcept],
|
|
18
|
+
output_concepts: List[BuildConcept],
|
|
19
|
+
environment: BuildEnvironment,
|
|
20
|
+
whole_grain: bool = False,
|
|
21
|
+
parents: List["StrategyNode"] | None = None,
|
|
22
|
+
depth: int = 0,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(
|
|
25
|
+
input_concepts=input_concepts,
|
|
26
|
+
output_concepts=output_concepts,
|
|
27
|
+
environment=environment,
|
|
28
|
+
whole_grain=whole_grain,
|
|
29
|
+
parents=parents,
|
|
30
|
+
depth=depth,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _resolve(self) -> QueryDatasource:
|
|
34
|
+
"""We need to ensure that any filtered values are removed from the output to avoid inappropriate references"""
|
|
35
|
+
base = super()._resolve()
|
|
36
|
+
return base
|
|
37
|
+
|
|
38
|
+
def copy(self) -> "RecursiveNode":
|
|
39
|
+
return RecursiveNode(
|
|
40
|
+
input_concepts=list(self.input_concepts),
|
|
41
|
+
output_concepts=list(self.output_concepts),
|
|
42
|
+
environment=self.environment,
|
|
43
|
+
whole_grain=self.whole_grain,
|
|
44
|
+
parents=self.parents,
|
|
45
|
+
depth=self.depth,
|
|
46
|
+
)
|
trilogy/core/query_processor.py
CHANGED
|
@@ -26,6 +26,7 @@ from trilogy.core.models.execute import (
|
|
|
26
26
|
InstantiatedUnnestJoin,
|
|
27
27
|
Join,
|
|
28
28
|
QueryDatasource,
|
|
29
|
+
RecursiveCTE,
|
|
29
30
|
UnionCTE,
|
|
30
31
|
UnnestJoin,
|
|
31
32
|
)
|
|
@@ -340,7 +341,12 @@ def datasource_to_cte(
|
|
|
340
341
|
base_name, base_alias = resolve_cte_base_name_and_alias_v2(
|
|
341
342
|
human_id, query_datasource, source_map, final_joins
|
|
342
343
|
)
|
|
343
|
-
|
|
344
|
+
cte_class = CTE
|
|
345
|
+
|
|
346
|
+
if query_datasource.source_type == SourceType.RECURSIVE:
|
|
347
|
+
cte_class = RecursiveCTE
|
|
348
|
+
# extra_kwargs['left_recursive_concept'] = query_datasource.left
|
|
349
|
+
cte = cte_class(
|
|
344
350
|
name=human_id,
|
|
345
351
|
source=query_datasource,
|
|
346
352
|
# output columns are what are selected/grouped by
|
trilogy/dialect/base.py
CHANGED
|
@@ -48,7 +48,7 @@ from trilogy.core.models.core import (
|
|
|
48
48
|
)
|
|
49
49
|
from trilogy.core.models.datasource import Datasource, RawColumnExpr
|
|
50
50
|
from trilogy.core.models.environment import Environment
|
|
51
|
-
from trilogy.core.models.execute import CTE, CompiledCTE, UnionCTE
|
|
51
|
+
from trilogy.core.models.execute import CTE, CompiledCTE, RecursiveCTE, UnionCTE
|
|
52
52
|
from trilogy.core.processing.utility import (
|
|
53
53
|
decompose_condition,
|
|
54
54
|
is_scalar_condition,
|
|
@@ -173,6 +173,7 @@ FUNCTION_MAP = {
|
|
|
173
173
|
FunctionType.INDEX_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
|
|
174
174
|
FunctionType.MAP_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
|
|
175
175
|
FunctionType.UNNEST: lambda x: f"unnest({x[0]})",
|
|
176
|
+
FunctionType.RECURSE_EDGE: lambda x: f"CASE WHEN {x[1]} IS NULL THEN {x[0]} ELSE {x[1]} END",
|
|
176
177
|
FunctionType.ATTR_ACCESS: lambda x: f"""{x[0]}.{x[1].replace("'", "")}""",
|
|
177
178
|
FunctionType.STRUCT: lambda x: f"{{{', '.join(struct_arg(x))}}}",
|
|
178
179
|
FunctionType.ARRAY: lambda x: f"[{', '.join(x)}]",
|
|
@@ -247,7 +248,7 @@ FUNCTION_GRAIN_MATCH_MAP = {
|
|
|
247
248
|
|
|
248
249
|
GENERIC_SQL_TEMPLATE = Template(
|
|
249
250
|
"""{%- if ctes %}
|
|
250
|
-
WITH {% for cte in ctes %}
|
|
251
|
+
WITH {% if recursive%} RECURSIVE {% endif %}{% for cte in ctes %}
|
|
251
252
|
{{cte.name}} as (
|
|
252
253
|
{{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
|
|
253
254
|
{%- if full_select -%}
|
|
@@ -734,6 +735,11 @@ class BaseDialect:
|
|
|
734
735
|
]
|
|
735
736
|
base_statement += "\nORDER BY " + ",".join(ordering)
|
|
736
737
|
return CompiledCTE(name=cte.name, statement=base_statement)
|
|
738
|
+
elif isinstance(cte, RecursiveCTE):
|
|
739
|
+
base_statement = "\nUNION ALL\n".join(
|
|
740
|
+
[self.render_cte(child, False).statement for child in cte.internal_ctes]
|
|
741
|
+
)
|
|
742
|
+
return CompiledCTE(name=cte.name, statement=base_statement)
|
|
737
743
|
if self.UNNEST_MODE in (
|
|
738
744
|
UnnestMode.CROSS_APPLY,
|
|
739
745
|
UnnestMode.CROSS_JOIN,
|
|
@@ -1002,9 +1008,12 @@ class BaseDialect:
|
|
|
1002
1008
|
f" {selected}"
|
|
1003
1009
|
)
|
|
1004
1010
|
|
|
1011
|
+
recursive = any(isinstance(x, RecursiveCTE) for x in query.ctes)
|
|
1012
|
+
|
|
1005
1013
|
compiled_ctes = self.generate_ctes(query)
|
|
1006
1014
|
|
|
1007
1015
|
final = self.SQL_TEMPLATE.render(
|
|
1016
|
+
recursive=recursive,
|
|
1008
1017
|
output=(
|
|
1009
1018
|
query.output_to if isinstance(query, ProcessedQueryPersist) else None
|
|
1010
1019
|
),
|
trilogy/dialect/bigquery.py
CHANGED
|
@@ -59,8 +59,9 @@ BQ_SQL_TEMPLATE = Template(
|
|
|
59
59
|
"""{%- if output %}
|
|
60
60
|
CREATE OR REPLACE TABLE {{ output.address.location }} AS
|
|
61
61
|
{% endif %}{%- if ctes %}
|
|
62
|
-
WITH {% for cte in ctes %}
|
|
63
|
-
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{%
|
|
62
|
+
WITH {% if recursive%}RECURSIVE{% endif %}{% for cte in ctes %}
|
|
63
|
+
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% else%}
|
|
64
|
+
{% endif %}{% endfor %}{% endif %}
|
|
64
65
|
{%- if full_select -%}
|
|
65
66
|
{{full_select}}
|
|
66
67
|
{%- else -%}
|
|
@@ -68,10 +69,8 @@ SELECT
|
|
|
68
69
|
{%- for select in select_columns %}
|
|
69
70
|
{{ select }}{% if not loop.last %},{% endif %}{% endfor %}
|
|
70
71
|
{% if base %}FROM
|
|
71
|
-
{{ base }}{% endif %}{% if joins %}
|
|
72
|
-
{
|
|
73
|
-
{{ join }}
|
|
74
|
-
{% endfor %}{% endif %}
|
|
72
|
+
{{ base }}{% endif %}{% if joins %}{% for join in joins %}
|
|
73
|
+
{{ join }}{% endfor %}{% endif %}
|
|
75
74
|
{% if where %}WHERE
|
|
76
75
|
{{ where }}
|
|
77
76
|
{% endif %}
|
trilogy/dialect/common.py
CHANGED
|
@@ -2,15 +2,19 @@ from typing import Callable
|
|
|
2
2
|
|
|
3
3
|
from trilogy.core.enums import Modifier, UnnestMode
|
|
4
4
|
from trilogy.core.models.build import (
|
|
5
|
+
BuildComparison,
|
|
5
6
|
BuildConcept,
|
|
7
|
+
BuildConditional,
|
|
6
8
|
BuildFunction,
|
|
7
9
|
BuildParamaterizedConceptReference,
|
|
10
|
+
BuildParenthetical,
|
|
8
11
|
)
|
|
9
12
|
from trilogy.core.models.datasource import RawColumnExpr
|
|
10
13
|
from trilogy.core.models.execute import (
|
|
11
14
|
CTE,
|
|
12
15
|
InstantiatedUnnestJoin,
|
|
13
16
|
Join,
|
|
17
|
+
UnionCTE,
|
|
14
18
|
)
|
|
15
19
|
|
|
16
20
|
|
|
@@ -49,7 +53,7 @@ def render_unnest(
|
|
|
49
53
|
def render_join_concept(
|
|
50
54
|
name: str,
|
|
51
55
|
quote_character: str,
|
|
52
|
-
cte: CTE,
|
|
56
|
+
cte: CTE | UnionCTE,
|
|
53
57
|
concept: BuildConcept,
|
|
54
58
|
render_expr,
|
|
55
59
|
inlined_ctes: set[str],
|
|
@@ -71,7 +75,16 @@ def render_join(
|
|
|
71
75
|
join: Join | InstantiatedUnnestJoin,
|
|
72
76
|
quote_character: str,
|
|
73
77
|
render_expr_func: Callable[
|
|
74
|
-
[
|
|
78
|
+
[
|
|
79
|
+
BuildConcept
|
|
80
|
+
| BuildParamaterizedConceptReference
|
|
81
|
+
| BuildFunction
|
|
82
|
+
| BuildConditional
|
|
83
|
+
| BuildComparison
|
|
84
|
+
| BuildParenthetical,
|
|
85
|
+
CTE,
|
|
86
|
+
],
|
|
87
|
+
str,
|
|
75
88
|
],
|
|
76
89
|
cte: CTE,
|
|
77
90
|
unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
|
|
@@ -127,4 +140,7 @@ def render_join(
|
|
|
127
140
|
base_joinkeys = ["1=1"]
|
|
128
141
|
|
|
129
142
|
joinkeys = " AND ".join(sorted(base_joinkeys))
|
|
130
|
-
|
|
143
|
+
base = f"{join.jointype.value.upper()} JOIN {right_base} on {joinkeys}"
|
|
144
|
+
if join.condition:
|
|
145
|
+
base = f"{base} and {render_expr_func(join.condition, cte)}"
|
|
146
|
+
return base
|
trilogy/dialect/duckdb.py
CHANGED
|
@@ -58,7 +58,7 @@ DUCKDB_TEMPLATE = Template(
|
|
|
58
58
|
"""{%- if output %}
|
|
59
59
|
CREATE OR REPLACE TABLE {{ output.address.location }} AS
|
|
60
60
|
{% endif %}{%- if ctes %}
|
|
61
|
-
WITH {% for cte in ctes %}
|
|
61
|
+
WITH {% if recursive%}RECURSIVE{% endif %}{% for cte in ctes %}
|
|
62
62
|
{{cte.name}} as (
|
|
63
63
|
{{cte.statement}}){% if not loop.last %},{% else %}
|
|
64
64
|
{% endif %}{% endfor %}{% endif %}
|
trilogy/dialect/snowflake.py
CHANGED
|
@@ -41,12 +41,14 @@ FUNCTION_GRAIN_MATCH_MAP = {
|
|
|
41
41
|
FunctionType.AVG: lambda args: f"{args[0]}",
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
SNOWFLAKE_SQL_TEMPLATE = Template(
|
|
45
46
|
"""{%- if output %}
|
|
46
47
|
CREATE OR REPLACE TABLE {{ output.address.location }} AS
|
|
47
48
|
{% endif %}{%- if ctes %}
|
|
48
|
-
WITH {% for cte in ctes %}
|
|
49
|
-
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{%
|
|
49
|
+
WITH {% if recursive%}RECURSIVE{% endif %}{% for cte in ctes %}
|
|
50
|
+
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% else %}
|
|
51
|
+
{% endfor %}{% endif %}
|
|
50
52
|
{%- if full_select -%}
|
|
51
53
|
{{full_select}}
|
|
52
54
|
{%- else -%}
|
|
@@ -55,10 +57,8 @@ SELECT
|
|
|
55
57
|
{%- for select in select_columns %}
|
|
56
58
|
{{ select }}{% if not loop.last %},{% endif %}{% endfor %}
|
|
57
59
|
{% if base %}FROM
|
|
58
|
-
{{ base }}{% endif %}{% if joins %}
|
|
59
|
-
{
|
|
60
|
-
{{ join }}
|
|
61
|
-
{% endfor %}{% endif %}
|
|
60
|
+
{{ base }}{% endif %}{% if joins %}{% for join in joins %}
|
|
61
|
+
{{ join }}{% endfor %}{% endif %}
|
|
62
62
|
{% if where %}WHERE
|
|
63
63
|
{{ where }}
|
|
64
64
|
{% endif %}
|
|
@@ -84,5 +84,5 @@ class SnowflakeDialect(BaseDialect):
|
|
|
84
84
|
**FUNCTION_GRAIN_MATCH_MAP,
|
|
85
85
|
}
|
|
86
86
|
QUOTE_CHARACTER = '"'
|
|
87
|
-
SQL_TEMPLATE =
|
|
87
|
+
SQL_TEMPLATE = SNOWFLAKE_SQL_TEMPLATE
|
|
88
88
|
UNNEST_MODE = UnnestMode.SNOWFLAKE
|
trilogy/parsing/common.py
CHANGED
|
@@ -476,6 +476,9 @@ def function_to_concept(
|
|
|
476
476
|
elif parent.operator == FunctionType.UNNEST:
|
|
477
477
|
derivation = Derivation.UNNEST
|
|
478
478
|
granularity = Granularity.MULTI_ROW
|
|
479
|
+
elif parent.operator == FunctionType.RECURSE_EDGE:
|
|
480
|
+
derivation = Derivation.RECURSIVE
|
|
481
|
+
granularity = Granularity.MULTI_ROW
|
|
479
482
|
elif parent.operator in FunctionClass.SINGLE_ROW.value:
|
|
480
483
|
derivation = Derivation.CONSTANT
|
|
481
484
|
granularity = Granularity.SINGLE_ROW
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -1657,6 +1657,12 @@ class ParseToObjects(Transformer):
|
|
|
1657
1657
|
def fnullif(self, meta, args):
|
|
1658
1658
|
return self.function_factory.create_function(args, FunctionType.NULLIF, meta)
|
|
1659
1659
|
|
|
1660
|
+
@v_args(meta=True)
|
|
1661
|
+
def frecurse_edge(self, meta, args):
|
|
1662
|
+
return self.function_factory.create_function(
|
|
1663
|
+
args, FunctionType.RECURSE_EDGE, meta
|
|
1664
|
+
)
|
|
1665
|
+
|
|
1660
1666
|
@v_args(meta=True)
|
|
1661
1667
|
def unnest(self, meta, args):
|
|
1662
1668
|
return self.function_factory.create_function(args, FunctionType.UNNEST, meta)
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -247,8 +247,10 @@
|
|
|
247
247
|
fnot: "NOT"i expr
|
|
248
248
|
fbool: "bool"i "(" expr ")"
|
|
249
249
|
fnullif: "nullif"i "(" expr "," expr ")"
|
|
250
|
+
_FRECURSE_EDGE.1: "recurse_edge("i
|
|
251
|
+
frecurse_edge: _FRECURSE_EDGE expr "," expr ")"
|
|
250
252
|
|
|
251
|
-
_generic_functions: fcast | concat | fcoalesce | fnullif | fcase | len | fnot | fbool
|
|
253
|
+
_generic_functions: fcast | concat | fcoalesce | fnullif | fcase | len | fnot | fbool | frecurse_edge
|
|
252
254
|
|
|
253
255
|
//constant
|
|
254
256
|
CURRENT_DATE.1: /current_date\(\)/
|
|
File without changes
|
|
File without changes
|
|
File without changes
|