pytrilogy 0.0.3.102__py3-none-any.whl → 0.0.3.104__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.102.dist-info → pytrilogy-0.0.3.104.dist-info}/METADATA +2 -1
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/RECORD +33 -31
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -1
- trilogy/core/models/execute.py +1 -6
- trilogy/core/optimization.py +13 -4
- trilogy/core/optimizations/__init__.py +2 -0
- trilogy/core/optimizations/hide_unused_concept.py +51 -0
- trilogy/core/optimizations/predicate_pushdown.py +9 -1
- trilogy/core/processing/concept_strategies_v3.py +35 -14
- trilogy/core/processing/discovery_node_factory.py +6 -6
- trilogy/core/processing/discovery_utility.py +163 -14
- trilogy/core/processing/node_generators/basic_node.py +1 -0
- trilogy/core/processing/node_generators/common.py +1 -0
- trilogy/core/processing/node_generators/filter_node.py +0 -10
- trilogy/core/processing/node_generators/group_node.py +36 -0
- trilogy/core/processing/node_generators/multiselect_node.py +1 -1
- trilogy/core/processing/node_generators/node_merge_node.py +2 -6
- trilogy/core/processing/node_generators/rowset_node.py +1 -21
- trilogy/core/processing/node_generators/union_node.py +1 -1
- trilogy/core/processing/node_generators/unnest_node.py +24 -8
- trilogy/core/processing/nodes/base_node.py +13 -3
- trilogy/core/processing/nodes/group_node.py +9 -91
- trilogy/core/processing/nodes/merge_node.py +9 -0
- trilogy/core/processing/utility.py +8 -0
- trilogy/dialect/base.py +20 -7
- trilogy/dialect/common.py +5 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/display.preql +3 -3
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.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.
|
|
3
|
+
Version: 0.0.3.104
|
|
4
4
|
Summary: Declarative, typed query language that compiles to SQL.
|
|
5
5
|
Home-page:
|
|
6
6
|
Author:
|
|
@@ -19,6 +19,7 @@ Requires-Dist: sqlalchemy<2.0.0
|
|
|
19
19
|
Requires-Dist: networkx
|
|
20
20
|
Requires-Dist: pyodbc
|
|
21
21
|
Requires-Dist: pydantic
|
|
22
|
+
Requires-Dist: duckdb<1.4.0
|
|
22
23
|
Requires-Dist: duckdb-engine
|
|
23
24
|
Requires-Dist: click
|
|
24
25
|
Provides-Extra: postgres
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
pytrilogy-0.0.3.
|
|
2
|
-
trilogy/__init__.py,sha256=
|
|
3
|
-
trilogy/constants.py,sha256=
|
|
1
|
+
pytrilogy-0.0.3.104.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=HyZF9WId40s9G3BjFS2OBMeHI7XNeE-YU1cFLvNOSWk,304
|
|
3
|
+
trilogy/constants.py,sha256=g_zkVCNjGop6coZ1kM8eXXAzCnUN22ldx3TYFz0E9sc,1747
|
|
4
4
|
trilogy/engine.py,sha256=3MiADf5MKcmxqiHBuRqiYdsXiLj7oitDfVvXvHrfjkA,2178
|
|
5
5
|
trilogy/executor.py,sha256=KgCAQhHPT-j0rPkBbALX0f84W9-Q-bkjHayGuavg99w,16490
|
|
6
6
|
trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
|
|
@@ -18,7 +18,7 @@ trilogy/core/exceptions.py,sha256=axkVXYJYQXCCwMHwlyDA232g4tCOwdCZUt7eHeUMDMg,28
|
|
|
18
18
|
trilogy/core/functions.py,sha256=sdV6Z3NUVfwL1d18eNcaAXllVNqzLez23McsJ6xIp7M,33182
|
|
19
19
|
trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
|
|
20
20
|
trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
|
|
21
|
-
trilogy/core/optimization.py,sha256=
|
|
21
|
+
trilogy/core/optimization.py,sha256=Km0ITEx9n6Iv5ReX6tm4uXO5uniSv_ooahycNNiET3g,9212
|
|
22
22
|
trilogy/core/query_processor.py,sha256=uqygDJqkjIH4vLP-lbGRgTN7rRcYEkr3KGqNimNw_80,20345
|
|
23
23
|
trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
|
|
24
24
|
trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -28,42 +28,43 @@ trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYy
|
|
|
28
28
|
trilogy/core/models/core.py,sha256=iT9WdZoiXeglmUHWn6bZyXCTBpkApTGPKtNm_Mhbu_g,12987
|
|
29
29
|
trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
|
|
30
30
|
trilogy/core/models/environment.py,sha256=hwTIRnJgaHUdCYof7U5A9NPitGZ2s9yxqiW5O2SaJ9Y,28759
|
|
31
|
-
trilogy/core/models/execute.py,sha256=
|
|
32
|
-
trilogy/core/optimizations/__init__.py,sha256=
|
|
31
|
+
trilogy/core/models/execute.py,sha256=pdL3voYB4dCQR_KMHwFaofP3ZpRbALRC2ELHueWyTko,42191
|
|
32
|
+
trilogy/core/optimizations/__init__.py,sha256=yspWc25M5SgAuvXYoSt5J8atyPbDlOfsKjIo5yGD9s4,368
|
|
33
33
|
trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
|
|
34
|
+
trilogy/core/optimizations/hide_unused_concept.py,sha256=DbsP8NqQOxmPv9omDOoFNPUGObUkqsRRNrr5d1xDxx4,1962
|
|
34
35
|
trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
|
|
35
|
-
trilogy/core/optimizations/predicate_pushdown.py,sha256=
|
|
36
|
+
trilogy/core/optimizations/predicate_pushdown.py,sha256=5ubatgq1IwWQ4L2FDt4--y168YLuGP-vwqH0m8IeTIw,9786
|
|
36
37
|
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
-
trilogy/core/processing/concept_strategies_v3.py,sha256=
|
|
38
|
-
trilogy/core/processing/discovery_node_factory.py,sha256=
|
|
39
|
-
trilogy/core/processing/discovery_utility.py,sha256=
|
|
38
|
+
trilogy/core/processing/concept_strategies_v3.py,sha256=AcMU1d5uCo8I1PFCkBtmcC6iFmM9vN6xSdKxSVMGfpA,23080
|
|
39
|
+
trilogy/core/processing/discovery_node_factory.py,sha256=p23jiiHyhrW-Q8ndbnRlqMHJKT8ZqPOA89SzE4xaFFo,15445
|
|
40
|
+
trilogy/core/processing/discovery_utility.py,sha256=wIuLsE6yuVykeYZdIqRSagivDNU3-ooiS7z6in4yqho,11518
|
|
40
41
|
trilogy/core/processing/discovery_validation.py,sha256=eZ4HfHMpqZLI8MGG2jez8arS8THs6ceuVrQFIY6gXrU,5364
|
|
41
42
|
trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
|
|
42
|
-
trilogy/core/processing/utility.py,sha256=
|
|
43
|
+
trilogy/core/processing/utility.py,sha256=1_oNnk6lWiy-D7LKYr07kU_v7iAM4i6ITUAS4bIiCr4,23444
|
|
43
44
|
trilogy/core/processing/node_generators/__init__.py,sha256=iVJ-crowPxYeut-hFjyEjfibKIDq7PfB4LEuDAUCjGY,943
|
|
44
|
-
trilogy/core/processing/node_generators/basic_node.py,sha256=
|
|
45
|
-
trilogy/core/processing/node_generators/common.py,sha256=
|
|
45
|
+
trilogy/core/processing/node_generators/basic_node.py,sha256=74LoVZXLinRvSzk2LmI1kwza96TnuH3ELoYRIbHB29A,5578
|
|
46
|
+
trilogy/core/processing/node_generators/common.py,sha256=xF32Kf6B08dZgKs2SOow1HomptSiSC057GCUCHFlS5s,9464
|
|
46
47
|
trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
|
|
47
|
-
trilogy/core/processing/node_generators/filter_node.py,sha256=
|
|
48
|
-
trilogy/core/processing/node_generators/group_node.py,sha256=
|
|
48
|
+
trilogy/core/processing/node_generators/filter_node.py,sha256=ndPznkcFu_cdCNgaRpgot8oqnzdHv4KAIfjeUIzrE2w,10816
|
|
49
|
+
trilogy/core/processing/node_generators/group_node.py,sha256=NdK1rl6Ze94XFWtgeC2dlRiL4pS3lh1ArKGPEltLtnw,8525
|
|
49
50
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
|
|
50
|
-
trilogy/core/processing/node_generators/multiselect_node.py,sha256=
|
|
51
|
-
trilogy/core/processing/node_generators/node_merge_node.py,sha256=
|
|
51
|
+
trilogy/core/processing/node_generators/multiselect_node.py,sha256=a505AEixjsjp5jI8Ng3H5KF_AaehkS6HfRfTef64l_o,7063
|
|
52
|
+
trilogy/core/processing/node_generators/node_merge_node.py,sha256=hNcZxnDLTZyYJWfojg769zH9HB9PfZfESmpN1lcHWXg,23172
|
|
52
53
|
trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
|
|
53
|
-
trilogy/core/processing/node_generators/rowset_node.py,sha256=
|
|
54
|
+
trilogy/core/processing/node_generators/rowset_node.py,sha256=MuVNIexXhqGONho_mewqMOwaYXNUnjjvyPvk_RDGNYE,5943
|
|
54
55
|
trilogy/core/processing/node_generators/select_merge_node.py,sha256=KQvGoNT5ZBWQ_caEomRTtG1PKZC7OPT4PKfY0QmwMGE,22270
|
|
55
56
|
trilogy/core/processing/node_generators/select_node.py,sha256=Ta1G39V94gjX_AgyZDz9OqnwLz4BjY3D6Drx9YpziMQ,3555
|
|
56
57
|
trilogy/core/processing/node_generators/synonym_node.py,sha256=AnAsa_Wj50NJ_IK0HSgab_7klYmKVrv0WI1uUe-GvEY,3766
|
|
57
|
-
trilogy/core/processing/node_generators/union_node.py,sha256=
|
|
58
|
-
trilogy/core/processing/node_generators/unnest_node.py,sha256=
|
|
58
|
+
trilogy/core/processing/node_generators/union_node.py,sha256=NxQbnRRoYMI4WjMeph41yk4E6yipj53qdGuNt-Mozxw,2818
|
|
59
|
+
trilogy/core/processing/node_generators/unnest_node.py,sha256=7uOZzBidEEKeZE0VW_XlgHGhEYf_snEHtV8UgJ_ZjyY,4048
|
|
59
60
|
trilogy/core/processing/node_generators/window_node.py,sha256=A90linr4pkZtTNfn9k2YNLqrJ_SFII3lbHxB-BC6mI8,6688
|
|
60
61
|
trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
62
|
trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=m2YQ4OmG0N2O61a7NEq1ZzbTa7JsCC00lxB2ymjcYRI,8224
|
|
62
63
|
trilogy/core/processing/nodes/__init__.py,sha256=zTge1EzwzEydlcMliIFO_TT7h7lS8l37lyZuQDir1h0,5487
|
|
63
|
-
trilogy/core/processing/nodes/base_node.py,sha256=
|
|
64
|
+
trilogy/core/processing/nodes/base_node.py,sha256=6LPQ5zP_dZJ6-k_dmX9ZSLsHaQMHgqiR5DEylpHYGZA,18478
|
|
64
65
|
trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
|
|
65
|
-
trilogy/core/processing/nodes/group_node.py,sha256=
|
|
66
|
-
trilogy/core/processing/nodes/merge_node.py,sha256=
|
|
66
|
+
trilogy/core/processing/nodes/group_node.py,sha256=sKsRP_BWEKg6z63T1X5ZlkJF2IMif0IEbVWTk-cdOH8,7100
|
|
67
|
+
trilogy/core/processing/nodes/merge_node.py,sha256=uc0tlz30Yt9SnCwLhMcWuPVbXLzm3dzy0XqbyirqqTo,16521
|
|
67
68
|
trilogy/core/processing/nodes/recursive_node.py,sha256=k0rizxR8KE64ievfHx_GPfQmU8QAP118Laeyq5BLUOk,1526
|
|
68
69
|
trilogy/core/processing/nodes/select_node_v2.py,sha256=IWyKyNgFlV8A2S1FUTPdIaogg6PzaHh-HmQo6v24sbg,8862
|
|
69
70
|
trilogy/core/processing/nodes/union_node.py,sha256=hLAXXVWqEgMWi7dlgSHfCF59fon64av14-uPgJzoKzM,1870
|
|
@@ -81,9 +82,9 @@ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGx
|
|
|
81
82
|
trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
|
|
82
83
|
trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
|
|
83
84
|
trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
84
|
-
trilogy/dialect/base.py,sha256=
|
|
85
|
+
trilogy/dialect/base.py,sha256=hFX0_3N-m3ZRTCyv1S650a8OPlx9qjp5Zh8wzTBx6E8,50338
|
|
85
86
|
trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
|
|
86
|
-
trilogy/dialect/common.py,sha256=
|
|
87
|
+
trilogy/dialect/common.py,sha256=cUI7JMmpG_A5KcaxRI-GoyqwLMD6jTf0JJhgcOdwQK4,5833
|
|
87
88
|
trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
|
|
88
89
|
trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
|
|
89
90
|
trilogy/dialect/duckdb.py,sha256=JoUvQ19WvgxoaJkGLM7DPXOd1H0394k3vBiblksQzOI,5631
|
|
@@ -109,16 +110,17 @@ trilogy/parsing/trilogy.lark,sha256=6eBDD6d4D9N1Nnn4CtmaoB-NpOpjHrEn5oi0JykAlbE,
|
|
|
109
110
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
110
111
|
trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
|
|
111
112
|
trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
113
|
+
trilogy/std/color.preql,sha256=sS9AXLDkECuDbNGnBMi2KnUuJukyVZVThKI9mP-ZOZI,50
|
|
112
114
|
trilogy/std/date.preql,sha256=HWZm4t4HWyxr5geWRsY05RnHBVDMci8z8YA2cu0-OOw,188
|
|
113
|
-
trilogy/std/display.preql,sha256=
|
|
115
|
+
trilogy/std/display.preql,sha256=ZJ08crsZnC3kaWwNUrMB1ZH5j6DUUbz8RaUgihA8sm4,299
|
|
114
116
|
trilogy/std/geography.preql,sha256=1A9Sq5PPMBnEPPf7f-rPVYxJfsnWpQ8oV_k4Fm3H2dU,675
|
|
115
117
|
trilogy/std/metric.preql,sha256=DRECGhkMyqfit5Fl4Ut9zbWrJuSMI1iO9HikuyoBpE0,421
|
|
116
118
|
trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
|
|
117
119
|
trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
|
|
118
120
|
trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
|
|
119
121
|
trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
|
|
120
|
-
pytrilogy-0.0.3.
|
|
121
|
-
pytrilogy-0.0.3.
|
|
122
|
-
pytrilogy-0.0.3.
|
|
123
|
-
pytrilogy-0.0.3.
|
|
124
|
-
pytrilogy-0.0.3.
|
|
122
|
+
pytrilogy-0.0.3.104.dist-info/METADATA,sha256=IJmkrwnxe7gz3s89ZYVrDe6SkRY2cf6xNpmj5GTXkSE,11839
|
|
123
|
+
pytrilogy-0.0.3.104.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
124
|
+
pytrilogy-0.0.3.104.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
125
|
+
pytrilogy-0.0.3.104.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
126
|
+
pytrilogy-0.0.3.104.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/constants.py
CHANGED
trilogy/core/models/execute.py
CHANGED
|
@@ -711,8 +711,6 @@ class QueryDatasource(BaseModel):
|
|
|
711
711
|
f" {[c.address for c in self.output_concepts]} concepts and"
|
|
712
712
|
f" {other.name} with {[c.address for c in other.output_concepts]} concepts"
|
|
713
713
|
)
|
|
714
|
-
logger.info(self.source_map)
|
|
715
|
-
logger.info(other.source_map)
|
|
716
714
|
|
|
717
715
|
merged_datasources: dict[str, Union[BuildDatasource, "QueryDatasource"]] = {}
|
|
718
716
|
|
|
@@ -816,10 +814,7 @@ class QueryDatasource(BaseModel):
|
|
|
816
814
|
use_raw_name,
|
|
817
815
|
force_alias=force_alias,
|
|
818
816
|
)
|
|
819
|
-
except ValueError
|
|
820
|
-
from trilogy.constants import logger
|
|
821
|
-
|
|
822
|
-
logger.debug(e)
|
|
817
|
+
except ValueError:
|
|
823
818
|
continue
|
|
824
819
|
existing = [c.with_grain(self.grain) for c in self.output_concepts]
|
|
825
820
|
if concept in existing:
|
trilogy/core/optimization.py
CHANGED
|
@@ -5,6 +5,7 @@ from trilogy.core.models.build import (
|
|
|
5
5
|
)
|
|
6
6
|
from trilogy.core.models.execute import CTE, RecursiveCTE, UnionCTE
|
|
7
7
|
from trilogy.core.optimizations import (
|
|
8
|
+
HideUnusedConcepts,
|
|
8
9
|
InlineDatasource,
|
|
9
10
|
OptimizationRule,
|
|
10
11
|
PredicatePushdown,
|
|
@@ -84,11 +85,18 @@ def filter_irrelevant_ctes(
|
|
|
84
85
|
# child.existence_source_map[x2].append(parent.name)
|
|
85
86
|
# else:
|
|
86
87
|
relevant_ctes.add(cte.name)
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
for parent in cte.parent_ctes:
|
|
90
|
+
if parent.name in relevant_ctes:
|
|
91
|
+
logger.info(
|
|
92
|
+
f"[Optimization][Irrelevent CTE filtering] Already visited {parent.name} when visting {cte.name}, potential recursive dag"
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
recurse(parent, inverse_map)
|
|
89
97
|
if isinstance(cte, UnionCTE):
|
|
90
|
-
for
|
|
91
|
-
recurse(
|
|
98
|
+
for internal in cte.internal_ctes:
|
|
99
|
+
recurse(internal, inverse_map)
|
|
92
100
|
|
|
93
101
|
inverse_map = gen_inverse_map(input)
|
|
94
102
|
recurse(root_cte, inverse_map)
|
|
@@ -220,6 +228,7 @@ def optimize_ctes(
|
|
|
220
228
|
REGISTERED_RULES.append(PredicatePushdown())
|
|
221
229
|
if CONFIG.optimizations.predicate_pushdown:
|
|
222
230
|
REGISTERED_RULES.append(PredicatePushdownRemove())
|
|
231
|
+
REGISTERED_RULES.append(HideUnusedConcepts())
|
|
223
232
|
for rule in REGISTERED_RULES:
|
|
224
233
|
loops = 0
|
|
225
234
|
complete = False
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .base_optimization import OptimizationRule
|
|
2
|
+
from .hide_unused_concept import HideUnusedConcepts
|
|
2
3
|
from .inline_datasource import InlineDatasource
|
|
3
4
|
from .predicate_pushdown import PredicatePushdown, PredicatePushdownRemove
|
|
4
5
|
|
|
@@ -7,4 +8,5 @@ __all__ = [
|
|
|
7
8
|
"InlineDatasource",
|
|
8
9
|
"PredicatePushdown",
|
|
9
10
|
"PredicatePushdownRemove",
|
|
11
|
+
"HideUnusedConcepts",
|
|
10
12
|
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from trilogy.core.models.build import (
|
|
2
|
+
BuildConcept,
|
|
3
|
+
)
|
|
4
|
+
from trilogy.core.models.execute import CTE, UnionCTE
|
|
5
|
+
from trilogy.core.optimizations.base_optimization import OptimizationRule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HideUnusedConcepts(OptimizationRule):
|
|
9
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
|
|
12
|
+
def optimize(
|
|
13
|
+
self, cte: CTE | UnionCTE, inverse_map: dict[str, list[CTE | UnionCTE]]
|
|
14
|
+
) -> bool:
|
|
15
|
+
used = set()
|
|
16
|
+
from trilogy.dialect.base import BaseDialect
|
|
17
|
+
|
|
18
|
+
renderer = BaseDialect()
|
|
19
|
+
children = inverse_map.get(cte.name, [])
|
|
20
|
+
if not children:
|
|
21
|
+
return False
|
|
22
|
+
for v in children:
|
|
23
|
+
self.log(f"Analyzing usage of {cte.name} in {v.name}")
|
|
24
|
+
renderer.render_cte(v)
|
|
25
|
+
used = renderer.used_map.get(cte.name, set())
|
|
26
|
+
self.log(f"Used concepts for {cte.name}: {used} from {renderer.used_map}")
|
|
27
|
+
add_to_hidden: list[BuildConcept] = []
|
|
28
|
+
for concept in cte.output_columns:
|
|
29
|
+
if concept.address not in used:
|
|
30
|
+
add_to_hidden.append(concept)
|
|
31
|
+
newly_hidden = [
|
|
32
|
+
x.address for x in add_to_hidden if x.address not in cte.hidden_concepts
|
|
33
|
+
]
|
|
34
|
+
non_hidden = [
|
|
35
|
+
x for x in cte.output_columns if x.address not in cte.hidden_concepts
|
|
36
|
+
]
|
|
37
|
+
if not newly_hidden or len(non_hidden) <= 1:
|
|
38
|
+
return False
|
|
39
|
+
self.log(
|
|
40
|
+
f"Hiding unused concepts {[x.address for x in add_to_hidden]} from {cte.name} (used: {used}, all: {[x.address for x in cte.output_columns]})"
|
|
41
|
+
)
|
|
42
|
+
candidates = [
|
|
43
|
+
x.address
|
|
44
|
+
for x in cte.output_columns
|
|
45
|
+
if x.address not in used and x.address not in cte.hidden_concepts
|
|
46
|
+
]
|
|
47
|
+
if len(candidates) == len(set([x.address for x in cte.output_columns])):
|
|
48
|
+
# pop one out
|
|
49
|
+
candidates.pop()
|
|
50
|
+
cte.hidden_concepts = set(candidates)
|
|
51
|
+
return True
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from trilogy.core.enums import (
|
|
2
2
|
BooleanOperator,
|
|
3
|
+
SourceType,
|
|
3
4
|
)
|
|
4
5
|
from trilogy.core.models.build import (
|
|
5
6
|
BuildComparison,
|
|
@@ -59,12 +60,19 @@ class PredicatePushdown(OptimizationRule):
|
|
|
59
60
|
)
|
|
60
61
|
return False
|
|
61
62
|
materialized = {k for k, v in parent_cte.source_map.items() if v != []}
|
|
63
|
+
|
|
62
64
|
if not row_conditions or not materialized:
|
|
63
65
|
return False
|
|
64
66
|
output_addresses = {x.address for x in parent_cte.output_columns}
|
|
65
67
|
# if any of the existence conditions are created on the asset, we can't push up to it
|
|
66
68
|
if existence_conditions and existence_conditions.intersection(output_addresses):
|
|
67
69
|
return False
|
|
70
|
+
if existence_conditions:
|
|
71
|
+
self.log(
|
|
72
|
+
f"Not pushing up existence {candidate} to {parent_cte.name} as it is a filter node"
|
|
73
|
+
)
|
|
74
|
+
if parent_cte.source.source_type == SourceType.FILTER:
|
|
75
|
+
return False
|
|
68
76
|
# if it's a root datasource, we can filter on _any_ of the output concepts
|
|
69
77
|
if parent_cte.is_root_datasource:
|
|
70
78
|
extra_check = {
|
|
@@ -81,7 +89,7 @@ class PredicatePushdown(OptimizationRule):
|
|
|
81
89
|
children = inverse_map.get(parent_cte.name, [])
|
|
82
90
|
if all([is_child_of(candidate, child.condition) for child in children]):
|
|
83
91
|
self.log(
|
|
84
|
-
f"All concepts
|
|
92
|
+
f"All concepts [{row_conditions}] and existence conditions [{existence_conditions}] not block pushup of [{output_addresses}]found on {parent_cte.name} with existing {parent_cte.condition} and all it's {len(children)} children include same filter; pushing up {candidate}"
|
|
85
93
|
)
|
|
86
94
|
if parent_cte.condition and not is_scalar_condition(
|
|
87
95
|
parent_cte.condition
|
|
@@ -19,7 +19,7 @@ from trilogy.core.processing.discovery_utility import (
|
|
|
19
19
|
LOGGER_PREFIX,
|
|
20
20
|
depth_to_prefix,
|
|
21
21
|
get_priority_concept,
|
|
22
|
-
|
|
22
|
+
group_if_required_v2,
|
|
23
23
|
)
|
|
24
24
|
from trilogy.core.processing.discovery_validation import (
|
|
25
25
|
ValidationResult,
|
|
@@ -66,7 +66,19 @@ def generate_candidates_restrictive(
|
|
|
66
66
|
|
|
67
67
|
# if it's single row, joins are irrelevant. Fetch without keys.
|
|
68
68
|
if priority_concept.granularity == Granularity.SINGLE_ROW:
|
|
69
|
-
|
|
69
|
+
logger.info("Have single row concept, including only other single row optional")
|
|
70
|
+
optional = (
|
|
71
|
+
[
|
|
72
|
+
x
|
|
73
|
+
for x in candidates
|
|
74
|
+
if x.granularity == Granularity.SINGLE_ROW
|
|
75
|
+
and x.address not in priority_concept.pseudonyms
|
|
76
|
+
and priority_concept.address not in x.pseudonyms
|
|
77
|
+
]
|
|
78
|
+
if priority_concept.derivation == Derivation.AGGREGATE
|
|
79
|
+
else []
|
|
80
|
+
)
|
|
81
|
+
return optional, conditions
|
|
70
82
|
|
|
71
83
|
if conditions and priority_concept.derivation in ROOT_DERIVATIONS:
|
|
72
84
|
logger.info(
|
|
@@ -374,15 +386,21 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
|
|
|
374
386
|
logger.info(
|
|
375
387
|
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values by setting outputs to {[x.address for x in output.output_concepts if x.address in non_virtual_output]}"
|
|
376
388
|
)
|
|
377
|
-
output.set_output_concepts(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
# output.set_output_concepts(
|
|
390
|
+
# [
|
|
391
|
+
# x
|
|
392
|
+
# for x in output.output_concepts
|
|
393
|
+
# if x.address not in non_virtual_difference_values
|
|
394
|
+
# or any(c in non_virtual_output for c in x.pseudonyms)
|
|
395
|
+
# ],
|
|
396
|
+
# rebuild=True,
|
|
397
|
+
# change_visibility=False
|
|
398
|
+
# )
|
|
399
|
+
# output.set_output_concepts(context.original_mandatory)
|
|
400
|
+
|
|
401
|
+
# if isinstance(output, MergeNode):
|
|
402
|
+
# output.force_group = True
|
|
403
|
+
# output.rebuild_cache()
|
|
386
404
|
|
|
387
405
|
logger.info(
|
|
388
406
|
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)}"
|
|
@@ -416,14 +434,17 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
|
|
|
416
434
|
logger.info(
|
|
417
435
|
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node output {[x.address for x in output.usable_outputs]} partial {[c.address for c in output.partial_concepts or []]} with {context.conditions}"
|
|
418
436
|
)
|
|
437
|
+
from trilogy.core.processing.discovery_utility import group_if_required_v2
|
|
438
|
+
|
|
419
439
|
if condition_required and context.conditions and non_virtual_different:
|
|
420
440
|
logger.info(
|
|
421
441
|
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Conditions {context.conditions} were injected, checking if we need a group to restore grain"
|
|
422
442
|
)
|
|
423
|
-
return
|
|
443
|
+
return group_if_required_v2(
|
|
424
444
|
output, context.original_mandatory, context.environment
|
|
425
445
|
)
|
|
426
|
-
|
|
446
|
+
|
|
447
|
+
return group_if_required_v2(output, context.original_mandatory, context.environment)
|
|
427
448
|
|
|
428
449
|
|
|
429
450
|
def _search_concepts(
|
|
@@ -588,4 +609,4 @@ def source_query_concepts(
|
|
|
588
609
|
logger.info(
|
|
589
610
|
f"{depth_to_prefix(0)}{LOGGER_PREFIX} final concepts are {[x.address for x in final]}"
|
|
590
611
|
)
|
|
591
|
-
return
|
|
612
|
+
return group_if_required_v2(root, output_concepts, environment)
|
|
@@ -177,7 +177,12 @@ def _generate_union_node(ctx: NodeGenerationContext) -> StrategyNode | None:
|
|
|
177
177
|
def _generate_aggregate_node(ctx: NodeGenerationContext) -> StrategyNode | None:
|
|
178
178
|
# Filter out constants to avoid multiplication issues
|
|
179
179
|
agg_optional = [
|
|
180
|
-
x
|
|
180
|
+
x
|
|
181
|
+
for x in ctx.local_optional
|
|
182
|
+
if not (
|
|
183
|
+
x.granularity == Granularity.SINGLE_ROW
|
|
184
|
+
and x.derivation != Derivation.AGGREGATE
|
|
185
|
+
)
|
|
181
186
|
]
|
|
182
187
|
|
|
183
188
|
logger.info(
|
|
@@ -376,11 +381,6 @@ class RootNodeHandler:
|
|
|
376
381
|
|
|
377
382
|
if pseudonyms:
|
|
378
383
|
expanded.add_output_concepts(pseudonyms)
|
|
379
|
-
logger.info(
|
|
380
|
-
f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
|
|
381
|
-
f"Hiding pseudonyms {[c.address for c in pseudonyms]}"
|
|
382
|
-
)
|
|
383
|
-
expanded.hide_output_concepts(pseudonyms)
|
|
384
384
|
|
|
385
385
|
logger.info(
|
|
386
386
|
f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
3
|
from trilogy.constants import logger
|
|
4
|
-
from trilogy.core.enums import Derivation
|
|
4
|
+
from trilogy.core.enums import Derivation, Purpose
|
|
5
5
|
from trilogy.core.models.build import (
|
|
6
6
|
BuildConcept,
|
|
7
|
+
BuildDatasource,
|
|
8
|
+
BuildFilterItem,
|
|
9
|
+
BuildGrain,
|
|
7
10
|
BuildRowsetItem,
|
|
8
11
|
)
|
|
9
12
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
13
|
+
from trilogy.core.models.execute import QueryDatasource, UnnestJoin
|
|
10
14
|
from trilogy.core.processing.nodes import GroupNode, MergeNode, StrategyNode
|
|
15
|
+
from trilogy.core.processing.utility import GroupRequiredResponse
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
def depth_to_prefix(depth: int) -> str:
|
|
@@ -17,31 +22,175 @@ def depth_to_prefix(depth: int) -> str:
|
|
|
17
22
|
LOGGER_PREFIX = "[DISCOVERY LOOP]"
|
|
18
23
|
|
|
19
24
|
|
|
20
|
-
def
|
|
25
|
+
def calculate_effective_parent_grain(
|
|
26
|
+
node: QueryDatasource | BuildDatasource,
|
|
27
|
+
) -> BuildGrain:
|
|
28
|
+
# calculate the effective grain of the parent node
|
|
29
|
+
# this is the union of all parent grains
|
|
30
|
+
if isinstance(node, MergeNode):
|
|
31
|
+
grain = BuildGrain()
|
|
32
|
+
qds = node.resolve()
|
|
33
|
+
if not qds.joins:
|
|
34
|
+
return qds.datasources[0].grain
|
|
35
|
+
for join in qds.joins:
|
|
36
|
+
if isinstance(join, UnnestJoin):
|
|
37
|
+
continue
|
|
38
|
+
pairs = join.concept_pairs or []
|
|
39
|
+
for key in pairs:
|
|
40
|
+
left = key.existing_datasource
|
|
41
|
+
grain += left.grain
|
|
42
|
+
keys = [key.right for key in pairs]
|
|
43
|
+
join_grain = BuildGrain.from_concepts(keys)
|
|
44
|
+
if join_grain == join.right_datasource.grain:
|
|
45
|
+
logger.info(f"irrelevant right join {join}, does not change grain")
|
|
46
|
+
else:
|
|
47
|
+
logger.info(
|
|
48
|
+
f"join changes grain, adding {join.right_datasource.grain} to {grain}"
|
|
49
|
+
)
|
|
50
|
+
grain += join.right_datasource.grain
|
|
51
|
+
return grain
|
|
52
|
+
else:
|
|
53
|
+
return node.grain or BuildGrain()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_if_group_required(
|
|
57
|
+
downstream_concepts: List[BuildConcept],
|
|
58
|
+
parents: list[QueryDatasource | BuildDatasource],
|
|
59
|
+
environment: BuildEnvironment,
|
|
60
|
+
depth: int = 0,
|
|
61
|
+
) -> GroupRequiredResponse:
|
|
62
|
+
padding = "\t" * depth
|
|
63
|
+
target_grain = BuildGrain.from_concepts(
|
|
64
|
+
downstream_concepts,
|
|
65
|
+
environment=environment,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
comp_grain = BuildGrain()
|
|
69
|
+
for source in parents:
|
|
70
|
+
# comp_grain += source.grain
|
|
71
|
+
comp_grain += calculate_effective_parent_grain(source)
|
|
72
|
+
|
|
73
|
+
# dynamically select if we need to group
|
|
74
|
+
# we must avoid grouping if we are already at grain
|
|
75
|
+
if comp_grain.issubset(target_grain):
|
|
76
|
+
|
|
77
|
+
logger.info(
|
|
78
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, target: {target_grain}, grain is subset of target, no group node required"
|
|
79
|
+
)
|
|
80
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
81
|
+
# find out what extra is in the comp grain vs target grain
|
|
82
|
+
difference = [
|
|
83
|
+
environment.concepts[c] for c in (comp_grain - target_grain).components
|
|
84
|
+
]
|
|
85
|
+
logger.info(
|
|
86
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: upstream grain: {comp_grain}, desired grain: {target_grain} from , difference {[x.address for x in difference]}"
|
|
87
|
+
)
|
|
88
|
+
for x in difference:
|
|
89
|
+
logger.info(
|
|
90
|
+
f"{padding}{LOGGER_PREFIX} Difference concept {x.address} purpose {x.purpose} keys {x.keys}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# if the difference is all unique properties whose keys are in the source grain
|
|
94
|
+
# we can also suppress the group
|
|
95
|
+
if all(
|
|
96
|
+
[
|
|
97
|
+
x.keys
|
|
98
|
+
and all(
|
|
99
|
+
environment.concepts[z].address in comp_grain.components for z in x.keys
|
|
100
|
+
)
|
|
101
|
+
for x in difference
|
|
102
|
+
]
|
|
103
|
+
):
|
|
104
|
+
logger.info(
|
|
105
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
|
|
106
|
+
)
|
|
107
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
108
|
+
if all([x.purpose == Purpose.KEY for x in difference]):
|
|
109
|
+
logger.info(
|
|
110
|
+
f"{padding}{LOGGER_PREFIX} checking if downstream is unique properties of key"
|
|
111
|
+
)
|
|
112
|
+
replaced_grain_raw: list[set[str]] = [
|
|
113
|
+
(
|
|
114
|
+
x.keys or set()
|
|
115
|
+
if x.purpose == Purpose.UNIQUE_PROPERTY
|
|
116
|
+
else set([x.address])
|
|
117
|
+
)
|
|
118
|
+
for x in downstream_concepts
|
|
119
|
+
if x.address in target_grain.components
|
|
120
|
+
]
|
|
121
|
+
# flatten the list of lists
|
|
122
|
+
replaced_grain = [item for sublist in replaced_grain_raw for item in sublist]
|
|
123
|
+
# if the replaced grain is a subset of the comp grain, we can skip the group
|
|
124
|
+
unique_grain_comp = BuildGrain.from_concepts(
|
|
125
|
+
replaced_grain, environment=environment
|
|
126
|
+
)
|
|
127
|
+
if comp_grain.issubset(unique_grain_comp):
|
|
128
|
+
logger.info(
|
|
129
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
|
|
130
|
+
)
|
|
131
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
132
|
+
logger.info(
|
|
133
|
+
f"{padding}{LOGGER_PREFIX} Checking for grain equivalence for filters and rowsets"
|
|
134
|
+
)
|
|
135
|
+
ngrain = []
|
|
136
|
+
for con in target_grain.components:
|
|
137
|
+
full = environment.concepts[con]
|
|
138
|
+
if full.derivation == Derivation.ROWSET:
|
|
139
|
+
ngrain.append(full.address.split(".", 1)[1])
|
|
140
|
+
elif full.derivation == Derivation.FILTER:
|
|
141
|
+
assert isinstance(full.lineage, BuildFilterItem)
|
|
142
|
+
if isinstance(full.lineage.content, BuildConcept):
|
|
143
|
+
ngrain.append(full.lineage.content.address)
|
|
144
|
+
else:
|
|
145
|
+
ngrain.append(full.address)
|
|
146
|
+
target_grain2 = BuildGrain.from_concepts(
|
|
147
|
+
ngrain,
|
|
148
|
+
environment=environment,
|
|
149
|
+
)
|
|
150
|
+
if comp_grain.issubset(target_grain2):
|
|
151
|
+
logger.info(
|
|
152
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain2}, pre rowset grain is subset of target, no group node required"
|
|
153
|
+
)
|
|
154
|
+
return GroupRequiredResponse(target_grain2, comp_grain, False)
|
|
155
|
+
|
|
156
|
+
logger.info(f"{padding}{LOGGER_PREFIX} Group requirement check: group required")
|
|
157
|
+
return GroupRequiredResponse(target_grain, comp_grain, True)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def group_if_required_v2(
|
|
21
161
|
root: StrategyNode, final: List[BuildConcept], environment: BuildEnvironment
|
|
22
162
|
):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
163
|
+
required = check_if_group_required(
|
|
164
|
+
downstream_concepts=final, parents=[root.resolve()], environment=environment
|
|
165
|
+
)
|
|
166
|
+
targets = [
|
|
167
|
+
x
|
|
168
|
+
for x in root.output_concepts
|
|
169
|
+
if x.address in final or any(c in final for c in x.pseudonyms)
|
|
170
|
+
]
|
|
171
|
+
if required.required:
|
|
32
172
|
if isinstance(root, MergeNode):
|
|
33
173
|
root.force_group = True
|
|
34
|
-
root.set_output_concepts(
|
|
174
|
+
root.set_output_concepts(targets, rebuild=False, change_visibility=False)
|
|
35
175
|
root.rebuild_cache()
|
|
36
176
|
return root
|
|
177
|
+
elif isinstance(root, GroupNode):
|
|
178
|
+
# root.set_output_concepts(final, rebuild=False)
|
|
179
|
+
# root.rebuild_cache()
|
|
180
|
+
return root
|
|
37
181
|
return GroupNode(
|
|
38
|
-
output_concepts=
|
|
39
|
-
input_concepts=
|
|
182
|
+
output_concepts=targets,
|
|
183
|
+
input_concepts=targets,
|
|
40
184
|
environment=environment,
|
|
41
185
|
parents=[root],
|
|
42
186
|
partial_concepts=root.partial_concepts,
|
|
43
187
|
preexisting_conditions=root.preexisting_conditions,
|
|
44
188
|
)
|
|
189
|
+
elif isinstance(root, GroupNode):
|
|
190
|
+
|
|
191
|
+
return root
|
|
192
|
+
else:
|
|
193
|
+
root.set_output_concepts(targets, rebuild=False, change_visibility=False)
|
|
45
194
|
return root
|
|
46
195
|
|
|
47
196
|
|
|
@@ -143,4 +143,5 @@ def gen_basic_node(
|
|
|
143
143
|
logger.info(
|
|
144
144
|
f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept}: input: {[x.address for x in parent_node.input_concepts]} output {[x.address for x in parent_node.output_concepts]} hidden {[x for x in parent_node.hidden_concepts]}"
|
|
145
145
|
)
|
|
146
|
+
|
|
146
147
|
return parent_node
|