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.

Files changed (31) hide show
  1. {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/RECORD +31 -29
  3. {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/WHEEL +1 -1
  4. trilogy/__init__.py +1 -1
  5. trilogy/constants.py +2 -0
  6. trilogy/core/enums.py +5 -0
  7. trilogy/core/functions.py +3 -0
  8. trilogy/core/models/author.py +6 -0
  9. trilogy/core/models/execute.py +207 -2
  10. trilogy/core/optimization.py +28 -11
  11. trilogy/core/optimizations/inline_datasource.py +5 -7
  12. trilogy/core/processing/concept_strategies_v3.py +17 -0
  13. trilogy/core/processing/node_generators/__init__.py +2 -0
  14. trilogy/core/processing/node_generators/recursive_node.py +87 -0
  15. trilogy/core/processing/node_generators/rowset_node.py +1 -3
  16. trilogy/core/processing/node_generators/window_node.py +13 -4
  17. trilogy/core/processing/nodes/__init__.py +4 -1
  18. trilogy/core/processing/nodes/base_node.py +32 -2
  19. trilogy/core/processing/nodes/recursive_node.py +46 -0
  20. trilogy/core/query_processor.py +7 -1
  21. trilogy/dialect/base.py +11 -2
  22. trilogy/dialect/bigquery.py +5 -6
  23. trilogy/dialect/common.py +19 -3
  24. trilogy/dialect/duckdb.py +1 -1
  25. trilogy/dialect/snowflake.py +8 -8
  26. trilogy/parsing/common.py +3 -0
  27. trilogy/parsing/parse_engine.py +6 -0
  28. trilogy/parsing/trilogy.lark +3 -1
  29. {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/entry_points.txt +0 -0
  30. {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/licenses/LICENSE.md +0 -0
  31. {pytrilogy-0.0.3.53.dist-info → pytrilogy-0.0.3.55.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.53
3
+ Version: 0.0.3.55
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,7 +1,7 @@
1
- pytrilogy-0.0.3.53.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=4T41znLlekPuyFkWm5zr0OjLiQwfA1YBEpTjY1Xl-6s,303
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=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
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=QVylGAe6epdGGpOKkeJ4cbx0mIZb0aARAKhsoZaGhoA,7576
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=IvqHyuO__o6Th8tkDWjb9cDxQDly6l3ZEfJ9y8YrTRU,29227
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=O7ag0IVQlJyWdAXBi_hHeU3Df5DRyd75Vlz6pks2J10,8197
23
- trilogy/core/query_processor.py,sha256=NNzYPKN5HzivQFXugSbJC_MaupkwOYii7A_vnXuBIK4,20063
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=KKW_3A1hdwq7D2dFwI6xZanukPuCQQ23R4GzE5VRJ6c,77206
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=F7-9VyUz5MC__VUSXd4U7gUb23Dc5PH5FdMUt6FqCPM,35214
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=AHuTGh2x0GQ8usOe0NiFncfTFQ_KogdgDl4uucmhIbI,4241
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=8Wos5d9_tzfnzSbejb36QL4uoGPQ3GiwP27u_a4JrcE,44097
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=o8rOFHPSo-s_59hREwXMW6gjUJCsiXumdbJNozHUf-Y,800
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/rowset_node.py,sha256=YmBs6ZQ7azLXRFEmeoecpGjK4pMHsUCovuBxfb3UKZI,6848
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=RUHgpYovQObFod1xRIMWtDzMcxwlm4-1Fdrf_Cuw5W4,6346
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=xPFF7x3TFs1Z4IcfthCykZgrksb-UhN-pc_oIigfFSo,6014
59
- trilogy/core/processing/nodes/base_node.py,sha256=z-aZEVjnLdFm6TpmneEm2bnRXj-tRFr7mN7DYG4zH9A,16967
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=Y4m4RQdYI3usjeTLJKUM_SIIyuxXZfe2hboG5JSxDLU,42412
74
- trilogy/dialect/bigquery.py,sha256=MyUumO8CeyPLh2JquoKPp6yQSZYEzUQ2mM07LCps-CA,3526
75
- trilogy/dialect/common.py,sha256=EFf2Ye7XcwTti7IsFRwMo_4AW2CF8eaxSk8XA0mA5qw,5400
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=IQzaRaCv5c6TUDERhbsLM4uTW0aGkO_DrAMR5k_j7TU,3861
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=vc0374Og0O5OIB7-Z7jbwoJJg0iomjvnUqHlxM8B0rg,3120
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=g1RmQF4fS_OgkcC6j4hnKIcn_ap0fFa_kzNUlH5D0nA,29760
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=mQzUSWOX2QdUng2ozAkqLDfMLI_NoqPjAiauhe_mHz4,70606
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=ybs65Ckb89PCitK4hcwy6znqElcWvIeMDQzsI2p_3YI,14197
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.53.dist-info/METADATA,sha256=AXRI2M3hD9xHGN8YuQ_nxnylPcy6zSWmKS3cBjZjtek,9095
106
- pytrilogy-0.0.3.53.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
107
- pytrilogy-0.0.3.53.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
108
- pytrilogy-0.0.3.53.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
109
- pytrilogy-0.0.3.53.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.53"
7
+ __version__ = "0.0.3.55"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -7,6 +7,8 @@ logger = getLogger("trilogy")
7
7
 
8
8
  DEFAULT_NAMESPACE = "local"
9
9
 
10
+ RECURSIVE_GATING_CONCEPT = "_terminal"
11
+
10
12
  VIRTUAL_CONCEPT_PREFIX = "_virt"
11
13
 
12
14
  ENV_CACHE_NAME = ".preql_cache.json"
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),
@@ -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))
@@ -12,9 +12,16 @@ from pydantic import (
12
12
  model_validator,
13
13
  )
14
14
 
15
- from trilogy.constants import CONFIG, logger
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)
@@ -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 == Derivation.WINDOW:
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 == Derivation.UNNEST:
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
- assert node.resolution_cache
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 History, StrategyNode, WindowNode
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 {resolve_window_parent_concepts(x, environment)[1]}"
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
- logger.info("Appending grain equivalent %s", x)
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 = StrategyNode(
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(self, concepts: List[BuildConcept], rebuild: bool = True):
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
- self.hidden_concepts.add(x.address)
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
+ )
@@ -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
- cte = CTE(
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
  ),
@@ -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 %},{% endif %}{% endfor %}{% endif %}
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
- {% for join in joins %}
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
- [BuildConcept | BuildParamaterizedConceptReference | BuildFunction, CTE], str
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
- return f"{join.jointype.value.upper()} JOIN {right_base} on {joinkeys}"
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 %}
@@ -41,12 +41,14 @@ FUNCTION_GRAIN_MATCH_MAP = {
41
41
  FunctionType.AVG: lambda args: f"{args[0]}",
42
42
  }
43
43
 
44
- BQ_SQL_TEMPLATE = Template(
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 %}{% endfor %}{% 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
- {% for join in joins %}
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 = BQ_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
@@ -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)
@@ -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\(\)/