pytrilogy 0.0.3.101__py3-none-any.whl → 0.0.3.103__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.101.dist-info → pytrilogy-0.0.3.103.dist-info}/METADATA +2 -1
- {pytrilogy-0.0.3.101.dist-info → pytrilogy-0.0.3.103.dist-info}/RECORD +24 -23
- trilogy/__init__.py +1 -1
- trilogy/core/models/author.py +9 -0
- trilogy/core/models/build.py +11 -4
- trilogy/core/optimization.py +13 -4
- trilogy/core/optimizations/__init__.py +2 -0
- trilogy/core/optimizations/hide_unused_concept.py +51 -0
- trilogy/core/processing/discovery_node_factory.py +0 -5
- trilogy/core/processing/node_generators/rowset_node.py +0 -20
- 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 +0 -1
- trilogy/dialect/base.py +17 -7
- trilogy/dialect/common.py +5 -0
- trilogy/parsing/common.py +11 -1
- trilogy/parsing/parse_engine.py +3 -0
- trilogy/parsing/render.py +125 -26
- trilogy/parsing/trilogy.lark +3 -1
- trilogy/std/display.preql +5 -2
- {pytrilogy-0.0.3.101.dist-info → pytrilogy-0.0.3.103.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.101.dist-info → pytrilogy-0.0.3.103.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.101.dist-info → pytrilogy-0.0.3.103.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.101.dist-info → pytrilogy-0.0.3.103.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.103
|
|
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,5 +1,5 @@
|
|
|
1
|
-
pytrilogy-0.0.3.
|
|
2
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
pytrilogy-0.0.3.103.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=lBanLP2CsDaPdJvJ3K68ncf8sO3sMfgsapECGG5J4fk,304
|
|
3
3
|
trilogy/constants.py,sha256=ohmro6so7PPNp2ruWQKVc0ijjXYPOyRrxB9LI8dr3TU,1746
|
|
4
4
|
trilogy/engine.py,sha256=3MiADf5MKcmxqiHBuRqiYdsXiLj7oitDfVvXvHrfjkA,2178
|
|
5
5
|
trilogy/executor.py,sha256=KgCAQhHPT-j0rPkBbALX0f84W9-Q-bkjHayGuavg99w,16490
|
|
@@ -18,24 +18,25 @@ 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
|
|
25
|
-
trilogy/core/models/author.py,sha256=
|
|
26
|
-
trilogy/core/models/build.py,sha256=
|
|
25
|
+
trilogy/core/models/author.py,sha256=3I7PFpJgoQT9RPOT3DfiqAjEtkcQPJnScs60I2UoyWo,81461
|
|
26
|
+
trilogy/core/models/build.py,sha256=iqk_-3plxX1BdxvUCTebqE9F3x62f40neKGf6Ld4VVU,70858
|
|
27
27
|
trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYycCrP4zF-rHU,5258
|
|
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
31
|
trilogy/core/models/execute.py,sha256=lsNzNjS3nZvoW5CHjYwxDTwBe502NZyytpK1eq8CwW4,42357
|
|
32
|
-
trilogy/core/optimizations/__init__.py,sha256=
|
|
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
36
|
trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
|
|
36
37
|
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
trilogy/core/processing/concept_strategies_v3.py,sha256=tvSN_aiqb1H7LkTl96vj7YK_DKcq_1nDdRJ69wZCLc8,22158
|
|
38
|
-
trilogy/core/processing/discovery_node_factory.py,sha256=
|
|
39
|
+
trilogy/core/processing/discovery_node_factory.py,sha256=r1JAnVhnB9YHEB1TW3racNH9mJvXjKRPZjzZrXsuiqg,15348
|
|
39
40
|
trilogy/core/processing/discovery_utility.py,sha256=Xntgug6VnEF96uw5Zwen1qMEUwKjqrm_ZDUr4i4tc1U,5595
|
|
40
41
|
trilogy/core/processing/discovery_validation.py,sha256=eZ4HfHMpqZLI8MGG2jez8arS8THs6ceuVrQFIY6gXrU,5364
|
|
41
42
|
trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
|
|
@@ -50,17 +51,17 @@ trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZ
|
|
|
50
51
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
|
|
51
52
|
trilogy/core/processing/node_generators/node_merge_node.py,sha256=1joMV7XpQ9Gpe-d5y7JUMBHIqakV5wFJi3Mtvs4UcL4,23415
|
|
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=T11Rqj-tsfubjFvBO0rzIVxtv9tOwwKXjGyut0r9xIY,5919
|
|
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=TQZLEz_xfXpdVyFa9R5BwvikH1OqzJUioOPw8vTETWc,18144
|
|
64
65
|
trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
|
|
65
66
|
trilogy/core/processing/nodes/group_node.py,sha256=njz-5T7OJ3-kaBC7EhdtPra3G77HnI7apjUwMGhUeXo,10569
|
|
66
67
|
trilogy/core/processing/nodes/merge_node.py,sha256=daJywBxh44Gqk-7eTiXbYtY7xo6O6fNvqX-DagTOTmE,16231
|
|
@@ -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=mgARj-aldkFAqdwps_25da03NLIAxU6Xg9Jq_VcOtp0,50181
|
|
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
|
|
@@ -99,26 +100,26 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
|
|
|
99
100
|
trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
|
|
100
101
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
101
102
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
102
|
-
trilogy/parsing/common.py,sha256=
|
|
103
|
+
trilogy/parsing/common.py,sha256=NJLm31J3W9BLWq1ClhNvYE43jrF950698KJ3o0UfSCo,31340
|
|
103
104
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
104
105
|
trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
|
|
105
106
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
106
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
107
|
-
trilogy/parsing/render.py,sha256=
|
|
108
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
107
|
+
trilogy/parsing/parse_engine.py,sha256=T-3Q4UH256IB6cfX85crScZwZ6gAwslgv0fy3WKBdjc,81930
|
|
108
|
+
trilogy/parsing/render.py,sha256=IklKMdXiqQEB6D28PrU1BewlDwD88Hnmqn1xjA9h720,23863
|
|
109
|
+
trilogy/parsing/trilogy.lark,sha256=6eBDD6d4D9N1Nnn4CtmaoB-NpOpjHrEn5oi0JykAlbE,16509
|
|
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
|
|
112
113
|
trilogy/std/date.preql,sha256=HWZm4t4HWyxr5geWRsY05RnHBVDMci8z8YA2cu0-OOw,188
|
|
113
|
-
trilogy/std/display.preql,sha256=
|
|
114
|
+
trilogy/std/display.preql,sha256=S20HW8qbShBc4OZPcHYiRlLdcaBp9dwruozWBoXKscs,293
|
|
114
115
|
trilogy/std/geography.preql,sha256=1A9Sq5PPMBnEPPf7f-rPVYxJfsnWpQ8oV_k4Fm3H2dU,675
|
|
115
116
|
trilogy/std/metric.preql,sha256=DRECGhkMyqfit5Fl4Ut9zbWrJuSMI1iO9HikuyoBpE0,421
|
|
116
117
|
trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
|
|
117
118
|
trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
|
|
118
119
|
trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
|
|
119
120
|
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.
|
|
121
|
+
pytrilogy-0.0.3.103.dist-info/METADATA,sha256=RnMfz8EH2sCtqHEDAraYhAb_V7oPbovtuI3PsL2F3Ms,11839
|
|
122
|
+
pytrilogy-0.0.3.103.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
123
|
+
pytrilogy-0.0.3.103.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
124
|
+
pytrilogy-0.0.3.103.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
125
|
+
pytrilogy-0.0.3.103.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/core/models/author.py
CHANGED
|
@@ -259,6 +259,15 @@ class Parenthetical(
|
|
|
259
259
|
)
|
|
260
260
|
)
|
|
261
261
|
|
|
262
|
+
def with_reference_replacement(self, source, target):
|
|
263
|
+
return Parenthetical.model_construct(
|
|
264
|
+
content=(
|
|
265
|
+
self.content.with_reference_replacement(source, target)
|
|
266
|
+
if isinstance(self.content, Mergeable)
|
|
267
|
+
else self.content
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
262
271
|
@property
|
|
263
272
|
def concept_arguments(self) -> Sequence[ConceptRef]:
|
|
264
273
|
base: List[ConceptRef] = []
|
trilogy/core/models/build.py
CHANGED
|
@@ -1511,7 +1511,10 @@ def requires_concept_nesting(
|
|
|
1511
1511
|
) -> AggregateWrapper | WindowItem | FilterItem | Function | None:
|
|
1512
1512
|
if isinstance(expr, (AggregateWrapper, WindowItem, FilterItem)):
|
|
1513
1513
|
return expr
|
|
1514
|
-
if isinstance(expr, Function) and expr.operator
|
|
1514
|
+
if isinstance(expr, Function) and expr.operator in (
|
|
1515
|
+
FunctionType.GROUP,
|
|
1516
|
+
FunctionType.PARENTHETICAL,
|
|
1517
|
+
):
|
|
1515
1518
|
# group by requires nesting
|
|
1516
1519
|
return expr
|
|
1517
1520
|
return None
|
|
@@ -1696,13 +1699,12 @@ class Factory:
|
|
|
1696
1699
|
return self._build_case_when(base)
|
|
1697
1700
|
|
|
1698
1701
|
def _build_case_when(self, base: CaseWhen) -> BuildCaseWhen:
|
|
1699
|
-
comparison = base.comparison
|
|
1700
1702
|
expr: Concept | FuncArgs = base.expr
|
|
1701
1703
|
validation = requires_concept_nesting(expr)
|
|
1702
1704
|
if validation:
|
|
1703
1705
|
expr, _ = self.instantiate_concept(validation)
|
|
1704
1706
|
return BuildCaseWhen(
|
|
1705
|
-
comparison=self.build(comparison),
|
|
1707
|
+
comparison=self.build(base.comparison),
|
|
1706
1708
|
expr=self.build(expr),
|
|
1707
1709
|
)
|
|
1708
1710
|
|
|
@@ -2019,7 +2021,12 @@ class Factory:
|
|
|
2019
2021
|
return self._build_parenthetical(base)
|
|
2020
2022
|
|
|
2021
2023
|
def _build_parenthetical(self, base: Parenthetical) -> BuildParenthetical:
|
|
2022
|
-
|
|
2024
|
+
validate = requires_concept_nesting(base.content)
|
|
2025
|
+
if validate:
|
|
2026
|
+
content, _ = self.instantiate_concept(validate)
|
|
2027
|
+
return BuildParenthetical(content=self.build(content))
|
|
2028
|
+
else:
|
|
2029
|
+
return BuildParenthetical(content=self.build(base.content))
|
|
2023
2030
|
|
|
2024
2031
|
@build.register
|
|
2025
2032
|
def _(self, base: SelectLineage) -> BuildSelectLineage:
|
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
|
|
@@ -376,11 +376,6 @@ class RootNodeHandler:
|
|
|
376
376
|
|
|
377
377
|
if pseudonyms:
|
|
378
378
|
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
379
|
|
|
385
380
|
logger.info(
|
|
386
381
|
f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
|
|
@@ -64,14 +64,6 @@ def gen_rowset_node(
|
|
|
64
64
|
v for v in concept_pool if v.address in rowset_outputs
|
|
65
65
|
]
|
|
66
66
|
|
|
67
|
-
select_hidden = node.hidden_concepts
|
|
68
|
-
rowset_hidden = [
|
|
69
|
-
x
|
|
70
|
-
for x in rowset_relevant
|
|
71
|
-
if x.address in lineage.rowset.derived_concepts
|
|
72
|
-
and isinstance(x.lineage, BuildRowsetItem)
|
|
73
|
-
and x.lineage.content.address in select_hidden
|
|
74
|
-
]
|
|
75
67
|
additional_relevant = [
|
|
76
68
|
factory.build(x) for x in select.output_components if x.address in enrichment
|
|
77
69
|
]
|
|
@@ -84,18 +76,6 @@ def gen_rowset_node(
|
|
|
84
76
|
)
|
|
85
77
|
node.partial_concepts.append(item)
|
|
86
78
|
|
|
87
|
-
final_hidden = rowset_hidden + [
|
|
88
|
-
x
|
|
89
|
-
for x in node.output_concepts
|
|
90
|
-
if x.address not in local_optional + [concept]
|
|
91
|
-
and x.derivation != Derivation.ROWSET
|
|
92
|
-
and not any(z in lineage.rowset.derived_concepts for z in x.pseudonyms)
|
|
93
|
-
]
|
|
94
|
-
logger.info(
|
|
95
|
-
f"{padding(depth)}{LOGGER_PREFIX} hiding {final_hidden} local optional {local_optional}"
|
|
96
|
-
)
|
|
97
|
-
node.hide_output_concepts(final_hidden)
|
|
98
|
-
|
|
99
79
|
node.grain = BuildGrain.from_concepts(
|
|
100
80
|
[
|
|
101
81
|
x
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
3
|
from trilogy.constants import logger
|
|
4
|
-
from trilogy.core.models.build import
|
|
4
|
+
from trilogy.core.models.build import (
|
|
5
|
+
BuildConcept,
|
|
6
|
+
BuildFunction,
|
|
7
|
+
BuildGrain,
|
|
8
|
+
BuildWhereClause,
|
|
9
|
+
)
|
|
5
10
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
6
|
-
from trilogy.core.processing.nodes import
|
|
11
|
+
from trilogy.core.processing.nodes import (
|
|
12
|
+
History,
|
|
13
|
+
StrategyNode,
|
|
14
|
+
UnnestNode,
|
|
15
|
+
WhereSafetyNode,
|
|
16
|
+
)
|
|
7
17
|
from trilogy.core.processing.utility import padding
|
|
8
18
|
|
|
9
19
|
LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
|
|
@@ -71,7 +81,9 @@ def gen_unnest_node(
|
|
|
71
81
|
return None
|
|
72
82
|
else:
|
|
73
83
|
parent = None
|
|
74
|
-
|
|
84
|
+
logger.info(
|
|
85
|
+
f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} got parent {parent}"
|
|
86
|
+
)
|
|
75
87
|
base = UnnestNode(
|
|
76
88
|
unnest_concepts=[concept] + equivalent_optional,
|
|
77
89
|
input_concepts=arguments + non_equivalent_optional,
|
|
@@ -83,7 +95,7 @@ def gen_unnest_node(
|
|
|
83
95
|
# as unnest operations are not valid in all situations
|
|
84
96
|
# TODO: inline this node when we can detect it's safe
|
|
85
97
|
conditional = conditions.conditional if conditions else None
|
|
86
|
-
new =
|
|
98
|
+
new = WhereSafetyNode(
|
|
87
99
|
input_concepts=base.output_concepts,
|
|
88
100
|
output_concepts=base.output_concepts,
|
|
89
101
|
environment=environment,
|
|
@@ -92,9 +104,13 @@ def gen_unnest_node(
|
|
|
92
104
|
preexisting_conditions=(
|
|
93
105
|
conditional if conditional and local_conditions is False else None
|
|
94
106
|
),
|
|
107
|
+
grain=BuildGrain.from_concepts(
|
|
108
|
+
concepts=base.output_concepts,
|
|
109
|
+
environment=environment,
|
|
110
|
+
),
|
|
95
111
|
)
|
|
96
|
-
qds = new.resolve()
|
|
97
|
-
assert qds.source_map[concept.address] == {base.resolve()}
|
|
98
|
-
for x in equivalent_optional:
|
|
99
|
-
|
|
112
|
+
# qds = new.resolve()
|
|
113
|
+
# assert qds.source_map[concept.address] == {base.resolve()}
|
|
114
|
+
# for x in equivalent_optional:
|
|
115
|
+
# assert qds.source_map[x.address] == {base.resolve()}
|
|
100
116
|
return new
|
|
@@ -489,7 +489,6 @@ class WhereSafetyNode(StrategyNode):
|
|
|
489
489
|
parent = parent.copy()
|
|
490
490
|
# avoid performance hit by not rebuilding until end
|
|
491
491
|
parent.set_output_concepts(self.output_concepts, rebuild=False)
|
|
492
|
-
parent.hide_output_concepts(self.hidden_concepts, rebuild=False)
|
|
493
492
|
|
|
494
493
|
# these conditions
|
|
495
494
|
if self.preexisting_conditions:
|
trilogy/dialect/base.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
1
2
|
from datetime import date, datetime
|
|
2
3
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
|
3
4
|
|
|
@@ -311,6 +312,7 @@ def safe_get_cte_value(
|
|
|
311
312
|
c: BuildConcept,
|
|
312
313
|
quote_char: str,
|
|
313
314
|
render_expr: Callable,
|
|
315
|
+
use_map: dict[str, set[str]],
|
|
314
316
|
) -> Optional[str]:
|
|
315
317
|
address = c.address
|
|
316
318
|
raw = cte.source_map.get(address, None)
|
|
@@ -319,13 +321,17 @@ def safe_get_cte_value(
|
|
|
319
321
|
return None
|
|
320
322
|
if isinstance(raw, str):
|
|
321
323
|
rendered = cte.get_alias(c, raw)
|
|
324
|
+
use_map[raw].add(c.address)
|
|
322
325
|
return f"{quote_char}{raw}{quote_char}.{safe_quote(rendered, quote_char)}"
|
|
323
326
|
if isinstance(raw, list) and len(raw) == 1:
|
|
324
327
|
rendered = cte.get_alias(c, raw[0])
|
|
325
328
|
if isinstance(rendered, FUNCTION_ITEMS):
|
|
326
329
|
# if it's a function, we need to render it as a function
|
|
327
330
|
return f"{render_expr(rendered, cte=cte, raise_invalid=True)}"
|
|
331
|
+
use_map[raw[0]].add(c.address)
|
|
328
332
|
return f"{quote_char}{raw[0]}{quote_char}.{safe_quote(rendered, quote_char)}"
|
|
333
|
+
for x in raw:
|
|
334
|
+
use_map[x].add(c.address)
|
|
329
335
|
return coalesce(
|
|
330
336
|
sorted(
|
|
331
337
|
[
|
|
@@ -350,13 +356,12 @@ class BaseDialect:
|
|
|
350
356
|
|
|
351
357
|
def __init__(self, rendering: Rendering | None = None):
|
|
352
358
|
self.rendering = rendering or CONFIG.rendering
|
|
359
|
+
self.used_map: dict[str, set[str]] = defaultdict(set)
|
|
353
360
|
|
|
354
361
|
def render_order_item(
|
|
355
362
|
self,
|
|
356
363
|
order_item: BuildOrderItem,
|
|
357
364
|
cte: CTE | UnionCTE,
|
|
358
|
-
final: bool = False,
|
|
359
|
-
alias: bool = True,
|
|
360
365
|
) -> str:
|
|
361
366
|
# if final:
|
|
362
367
|
# if not alias:
|
|
@@ -527,6 +532,9 @@ class BaseDialect:
|
|
|
527
532
|
)
|
|
528
533
|
|
|
529
534
|
raw_content = cte.get_alias(c)
|
|
535
|
+
parent = cte.source_map.get(c.address, None)
|
|
536
|
+
if parent:
|
|
537
|
+
self.used_map[parent[0]].add(c.address)
|
|
530
538
|
if isinstance(raw_content, RawColumnExpr):
|
|
531
539
|
rval = raw_content.text
|
|
532
540
|
elif isinstance(raw_content, FUNCTION_ITEMS):
|
|
@@ -540,6 +548,7 @@ class BaseDialect:
|
|
|
540
548
|
c,
|
|
541
549
|
self.QUOTE_CHARACTER,
|
|
542
550
|
self.render_expr,
|
|
551
|
+
self.used_map,
|
|
543
552
|
)
|
|
544
553
|
if not rval:
|
|
545
554
|
# unions won't have a specific source mapped; just use a generic column reference
|
|
@@ -615,6 +624,7 @@ class BaseDialect:
|
|
|
615
624
|
lookup_cte = cte
|
|
616
625
|
if cte_map and not lookup_cte:
|
|
617
626
|
lookup_cte = cte_map.get(e.right.address)
|
|
627
|
+
|
|
618
628
|
assert lookup_cte, "Subselects must be rendered with a CTE in context"
|
|
619
629
|
if e.right.address not in lookup_cte.existence_source_map:
|
|
620
630
|
lookup = lookup_cte.source_map.get(
|
|
@@ -634,6 +644,7 @@ class BaseDialect:
|
|
|
634
644
|
f"Missing source CTE for {e.right.address}"
|
|
635
645
|
)
|
|
636
646
|
assert cte, "CTE must be provided for inlined CTEs"
|
|
647
|
+
self.used_map[target].add(e.right.address)
|
|
637
648
|
if target in cte.inlined_ctes:
|
|
638
649
|
info = cte.inlined_ctes[target]
|
|
639
650
|
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {info.new_base} as {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
|
|
@@ -738,6 +749,7 @@ class BaseDialect:
|
|
|
738
749
|
raise_invalid=raise_invalid,
|
|
739
750
|
)
|
|
740
751
|
elif cte_map:
|
|
752
|
+
self.used_map[cte_map[e.address].name].add(e.address)
|
|
741
753
|
return f"{cte_map[e.address].name}.{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
|
|
742
754
|
return f"{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
|
|
743
755
|
elif isinstance(e, bool):
|
|
@@ -822,10 +834,7 @@ class BaseDialect:
|
|
|
822
834
|
)
|
|
823
835
|
if cte.order_by:
|
|
824
836
|
|
|
825
|
-
ordering = [
|
|
826
|
-
self.render_order_item(i, cte, final=True, alias=False)
|
|
827
|
-
for i in cte.order_by.items
|
|
828
|
-
]
|
|
837
|
+
ordering = [self.render_order_item(i, cte) for i in cte.order_by.items]
|
|
829
838
|
base_statement += "\nORDER BY " + ",".join(ordering)
|
|
830
839
|
return CompiledCTE(name=cte.name, statement=base_statement)
|
|
831
840
|
elif isinstance(cte, RecursiveCTE):
|
|
@@ -950,7 +959,8 @@ class BaseDialect:
|
|
|
950
959
|
self.QUOTE_CHARACTER,
|
|
951
960
|
self.render_expr,
|
|
952
961
|
cte,
|
|
953
|
-
self.
|
|
962
|
+
use_map=self.used_map,
|
|
963
|
+
unnest_mode=self.UNNEST_MODE,
|
|
954
964
|
)
|
|
955
965
|
for join in final_joins
|
|
956
966
|
]
|
trilogy/dialect/common.py
CHANGED
|
@@ -62,10 +62,12 @@ def render_join_concept(
|
|
|
62
62
|
concept: BuildConcept,
|
|
63
63
|
render_expr,
|
|
64
64
|
inlined_ctes: set[str],
|
|
65
|
+
use_map: dict[str, set[str]],
|
|
65
66
|
):
|
|
66
67
|
if cte.name in inlined_ctes:
|
|
67
68
|
base = render_expr(concept, cte)
|
|
68
69
|
return base
|
|
70
|
+
use_map[name].add(concept.address)
|
|
69
71
|
return f"{quote_character}{name}{quote_character}.{quote_character}{concept.safe_address}{quote_character}"
|
|
70
72
|
|
|
71
73
|
|
|
@@ -85,6 +87,7 @@ def render_join(
|
|
|
85
87
|
str,
|
|
86
88
|
],
|
|
87
89
|
cte: CTE,
|
|
90
|
+
use_map: dict[str, set[str]],
|
|
88
91
|
unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
|
|
89
92
|
) -> str | None:
|
|
90
93
|
# {% for key in join.joinkeys %}{{ key.inner }} = {{ key.outer}}{% endfor %}
|
|
@@ -121,6 +124,7 @@ def render_join(
|
|
|
121
124
|
pair.left,
|
|
122
125
|
render_expr_func,
|
|
123
126
|
join.inlined_ctes,
|
|
127
|
+
use_map=use_map,
|
|
124
128
|
),
|
|
125
129
|
render_join_concept(
|
|
126
130
|
right_name,
|
|
@@ -129,6 +133,7 @@ def render_join(
|
|
|
129
133
|
pair.right,
|
|
130
134
|
render_expr_func,
|
|
131
135
|
join.inlined_ctes,
|
|
136
|
+
use_map=use_map,
|
|
132
137
|
),
|
|
133
138
|
modifiers=pair.modifiers
|
|
134
139
|
+ (pair.left.modifiers or [])
|
trilogy/parsing/common.py
CHANGED
|
@@ -249,11 +249,14 @@ def atom_is_relevant(
|
|
|
249
249
|
return atom_is_relevant(atom.left, others, environment) or atom_is_relevant(
|
|
250
250
|
atom.right, others, environment
|
|
251
251
|
)
|
|
252
|
+
elif isinstance(atom, Parenthetical):
|
|
253
|
+
return atom_is_relevant(atom.content, others, environment)
|
|
252
254
|
elif isinstance(atom, ConceptArgs):
|
|
253
255
|
# use atom is relevant here to trigger the early exit behavior for concepts in set
|
|
254
256
|
return any(
|
|
255
257
|
[atom_is_relevant(x, others, environment) for x in atom.concept_arguments]
|
|
256
258
|
)
|
|
259
|
+
|
|
257
260
|
return False
|
|
258
261
|
|
|
259
262
|
|
|
@@ -294,12 +297,18 @@ def concept_is_relevant(
|
|
|
294
297
|
if all([c in others for c in concept.grain.components]):
|
|
295
298
|
return False
|
|
296
299
|
if concept.derivation in (Derivation.BASIC,) and isinstance(
|
|
297
|
-
concept.lineage, Function
|
|
300
|
+
concept.lineage, (Function, CaseWhen)
|
|
298
301
|
):
|
|
299
302
|
relevant = False
|
|
300
303
|
for arg in concept.lineage.arguments:
|
|
301
304
|
relevant = atom_is_relevant(arg, others, environment) or relevant
|
|
305
|
+
|
|
302
306
|
return relevant
|
|
307
|
+
if concept.derivation in (Derivation.BASIC,) and isinstance(
|
|
308
|
+
concept.lineage, Parenthetical
|
|
309
|
+
):
|
|
310
|
+
return atom_is_relevant(concept.lineage.content, others, environment)
|
|
311
|
+
|
|
303
312
|
if concept.granularity == Granularity.SINGLE_ROW:
|
|
304
313
|
return False
|
|
305
314
|
return True
|
|
@@ -346,6 +355,7 @@ def concepts_to_grain_concepts(
|
|
|
346
355
|
if sub.address in seen:
|
|
347
356
|
continue
|
|
348
357
|
if not concept_is_relevant(sub, pconcepts, environment): # type: ignore
|
|
358
|
+
|
|
349
359
|
continue
|
|
350
360
|
seen.add(sub.address)
|
|
351
361
|
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -992,6 +992,9 @@ class ParseToObjects(Transformer):
|
|
|
992
992
|
def order_by(self, args):
|
|
993
993
|
return OrderBy(items=args[0])
|
|
994
994
|
|
|
995
|
+
def over_component(self, args):
|
|
996
|
+
return ConceptRef(address=args[0].value.lstrip(",").strip())
|
|
997
|
+
|
|
995
998
|
def over_list(self, args):
|
|
996
999
|
return [x for x in args]
|
|
997
1000
|
|
trilogy/parsing/render.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from dataclasses import dataclass
|
|
2
4
|
from datetime import date, datetime
|
|
3
5
|
from functools import singledispatchmethod
|
|
4
6
|
from typing import Any
|
|
@@ -23,6 +25,7 @@ from trilogy.core.models.author import (
|
|
|
23
25
|
FunctionCallWrapper,
|
|
24
26
|
Grain,
|
|
25
27
|
OrderBy,
|
|
28
|
+
Ordering,
|
|
26
29
|
OrderItem,
|
|
27
30
|
Parenthetical,
|
|
28
31
|
SubselectComparison,
|
|
@@ -67,23 +70,72 @@ from trilogy.core.statements.author import (
|
|
|
67
70
|
|
|
68
71
|
QUERY_TEMPLATE = Template(
|
|
69
72
|
"""{% if where %}WHERE
|
|
70
|
-
|
|
73
|
+
{{ where }}
|
|
71
74
|
{% endif %}SELECT{%- for select in select_columns %}
|
|
72
|
-
|
|
75
|
+
{{ select }},{% endfor %}{% if having %}
|
|
73
76
|
HAVING
|
|
74
|
-
|
|
77
|
+
{{ having }}
|
|
75
78
|
{% endif %}{%- if order_by %}
|
|
76
79
|
ORDER BY{% for order in order_by %}
|
|
77
|
-
|
|
80
|
+
{{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{%- if limit is not none %}
|
|
78
81
|
LIMIT {{ limit }}{% endif %}
|
|
79
82
|
;"""
|
|
80
83
|
)
|
|
81
84
|
|
|
82
85
|
|
|
86
|
+
@dataclass
|
|
87
|
+
class IndentationContext:
|
|
88
|
+
"""Tracks indentation state during rendering"""
|
|
89
|
+
|
|
90
|
+
depth: int = 0
|
|
91
|
+
indent_string: str = " " # 4 spaces by default
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def current_indent(self) -> str:
|
|
95
|
+
return self.indent_string * self.depth
|
|
96
|
+
|
|
97
|
+
def increase_depth(self, extra_levels: int = 1) -> "IndentationContext":
|
|
98
|
+
return IndentationContext(
|
|
99
|
+
depth=self.depth + extra_levels, indent_string=self.indent_string
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
83
103
|
class Renderer:
|
|
84
104
|
|
|
85
|
-
def __init__(
|
|
105
|
+
def __init__(
|
|
106
|
+
self, environment: Environment | None = None, indent_string: str = " "
|
|
107
|
+
):
|
|
86
108
|
self.environment = environment
|
|
109
|
+
self.indent_context = IndentationContext(indent_string=indent_string)
|
|
110
|
+
|
|
111
|
+
@contextmanager
|
|
112
|
+
def indented(self, levels: int = 1):
|
|
113
|
+
"""Context manager for temporarily increasing indentation"""
|
|
114
|
+
old_context = self.indent_context
|
|
115
|
+
self.indent_context = self.indent_context.increase_depth(levels)
|
|
116
|
+
try:
|
|
117
|
+
yield
|
|
118
|
+
finally:
|
|
119
|
+
self.indent_context = old_context
|
|
120
|
+
|
|
121
|
+
def indent_lines(self, text: str, extra_levels: int = 0) -> str:
|
|
122
|
+
"""Apply current indentation to all lines in text"""
|
|
123
|
+
if not text:
|
|
124
|
+
return text
|
|
125
|
+
|
|
126
|
+
indent = self.indent_context.indent_string * (
|
|
127
|
+
self.indent_context.depth + extra_levels
|
|
128
|
+
)
|
|
129
|
+
lines = text.split("\n")
|
|
130
|
+
indented_lines = []
|
|
131
|
+
|
|
132
|
+
for line in lines:
|
|
133
|
+
if line.strip(): # Only indent non-empty lines
|
|
134
|
+
indented_lines.append(indent + line)
|
|
135
|
+
else:
|
|
136
|
+
indented_lines.append(line) # Keep empty lines as-is
|
|
137
|
+
|
|
138
|
+
return "\n".join(indented_lines)
|
|
87
139
|
|
|
88
140
|
def render_statement_string(self, list_of_statements: list[Any]) -> str:
|
|
89
141
|
new = []
|
|
@@ -98,7 +150,7 @@ class Renderer:
|
|
|
98
150
|
new.append("\n\n")
|
|
99
151
|
else:
|
|
100
152
|
new.append("\n")
|
|
101
|
-
new.append(
|
|
153
|
+
new.append(self.to_string(stmt))
|
|
102
154
|
last_statement_type = stmt_type
|
|
103
155
|
return "".join(new)
|
|
104
156
|
|
|
@@ -192,14 +244,19 @@ class Renderer:
|
|
|
192
244
|
|
|
193
245
|
@to_string.register
|
|
194
246
|
def _(self, arg: Datasource):
|
|
195
|
-
|
|
247
|
+
with self.indented():
|
|
248
|
+
assignments = ",\n".join(
|
|
249
|
+
[self.indent_lines(self.to_string(x)) for x in arg.columns]
|
|
250
|
+
)
|
|
251
|
+
|
|
196
252
|
if arg.non_partial_for:
|
|
197
253
|
non_partial = f"\ncomplete where {self.to_string(arg.non_partial_for)}"
|
|
198
254
|
else:
|
|
199
255
|
non_partial = ""
|
|
256
|
+
|
|
200
257
|
base = f"""datasource {arg.name} (
|
|
201
|
-
|
|
202
|
-
|
|
258
|
+
{assignments}
|
|
259
|
+
)
|
|
203
260
|
{self.to_string(arg.grain) if arg.grain.components else ''}{non_partial}
|
|
204
261
|
{self.to_string(arg.address)}"""
|
|
205
262
|
|
|
@@ -390,26 +447,45 @@ class Renderer:
|
|
|
390
447
|
|
|
391
448
|
@to_string.register
|
|
392
449
|
def _(self, arg: SelectStatement):
|
|
450
|
+
with self.indented():
|
|
451
|
+
select_columns = [
|
|
452
|
+
self.indent_lines(self.to_string(c)) for c in arg.selection
|
|
453
|
+
]
|
|
454
|
+
where_clause = None
|
|
455
|
+
if arg.where_clause:
|
|
456
|
+
where_clause = self.indent_lines(self.to_string(arg.where_clause))
|
|
457
|
+
having_clause = None
|
|
458
|
+
if arg.having_clause:
|
|
459
|
+
having_clause = self.indent_lines(self.to_string(arg.having_clause))
|
|
460
|
+
order_by = None
|
|
461
|
+
if arg.order_by:
|
|
462
|
+
order_by = [
|
|
463
|
+
self.indent_lines(self.to_string(c)) for c in arg.order_by.items
|
|
464
|
+
]
|
|
465
|
+
|
|
393
466
|
return QUERY_TEMPLATE.render(
|
|
394
|
-
select_columns=
|
|
395
|
-
where=
|
|
396
|
-
having=
|
|
397
|
-
order_by=
|
|
398
|
-
[self.to_string(c) for c in arg.order_by.items]
|
|
399
|
-
if arg.order_by
|
|
400
|
-
else None
|
|
401
|
-
),
|
|
467
|
+
select_columns=select_columns,
|
|
468
|
+
where=where_clause,
|
|
469
|
+
having=having_clause,
|
|
470
|
+
order_by=order_by,
|
|
402
471
|
limit=arg.limit,
|
|
403
472
|
)
|
|
404
473
|
|
|
405
474
|
@to_string.register
|
|
406
475
|
def _(self, arg: MultiSelectStatement):
|
|
407
|
-
|
|
476
|
+
# Each select gets its own indentation
|
|
477
|
+
select_parts = []
|
|
478
|
+
for select in arg.selects:
|
|
479
|
+
select_parts.append(
|
|
480
|
+
self.to_string(select)[:-2]
|
|
481
|
+
) # Remove the trailing ";\n"
|
|
482
|
+
|
|
483
|
+
base = "\nMERGE\n".join(select_parts)
|
|
408
484
|
base += self.to_string(arg.align)
|
|
409
485
|
if arg.where_clause:
|
|
410
486
|
base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
|
|
411
487
|
if arg.order_by:
|
|
412
|
-
base += f"\nORDER BY\n
|
|
488
|
+
base += f"\nORDER BY\n{self.to_string(arg.order_by)}"
|
|
413
489
|
if arg.limit:
|
|
414
490
|
base += f"\nLIMIT {arg.limit}"
|
|
415
491
|
base += "\n;"
|
|
@@ -421,7 +497,9 @@ class Renderer:
|
|
|
421
497
|
|
|
422
498
|
@to_string.register
|
|
423
499
|
def _(self, arg: AlignClause):
|
|
424
|
-
|
|
500
|
+
with self.indented():
|
|
501
|
+
align_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
|
|
502
|
+
return "\nALIGN\n" + ",\n".join(align_items)
|
|
425
503
|
|
|
426
504
|
@to_string.register
|
|
427
505
|
def _(self, arg: AlignItem):
|
|
@@ -429,7 +507,13 @@ class Renderer:
|
|
|
429
507
|
|
|
430
508
|
@to_string.register
|
|
431
509
|
def _(self, arg: OrderBy):
|
|
432
|
-
|
|
510
|
+
with self.indented():
|
|
511
|
+
order_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
|
|
512
|
+
return ",\n".join(order_items)
|
|
513
|
+
|
|
514
|
+
@to_string.register
|
|
515
|
+
def _(self, arg: Ordering):
|
|
516
|
+
return arg.value
|
|
433
517
|
|
|
434
518
|
@to_string.register
|
|
435
519
|
def _(self, arg: "WhereClause"):
|
|
@@ -469,7 +553,6 @@ class Renderer:
|
|
|
469
553
|
|
|
470
554
|
@to_string.register
|
|
471
555
|
def _(self, arg: "FilterItem"):
|
|
472
|
-
|
|
473
556
|
return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
|
|
474
557
|
|
|
475
558
|
@to_string.register
|
|
@@ -538,18 +621,34 @@ class Renderer:
|
|
|
538
621
|
if len(args) == 1:
|
|
539
622
|
return f"group({args[0]})"
|
|
540
623
|
return f"group({args[0]}) by {arg_string}"
|
|
541
|
-
inputs = ",".join(args)
|
|
542
624
|
|
|
543
625
|
if arg.operator == FunctionType.CONSTANT:
|
|
544
|
-
return f"{
|
|
626
|
+
return f"{', '.join(args)}"
|
|
545
627
|
if arg.operator == FunctionType.CAST:
|
|
546
628
|
return f"CAST({self.to_string(arg.arguments[0])} AS {self.to_string(arg.arguments[1])})"
|
|
547
629
|
if arg.operator == FunctionType.INDEX_ACCESS:
|
|
548
630
|
return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
|
|
549
631
|
|
|
550
632
|
if arg.operator == FunctionType.CASE:
|
|
551
|
-
|
|
552
|
-
|
|
633
|
+
with self.indented():
|
|
634
|
+
indented_args = [
|
|
635
|
+
self.indent_lines(self.to_string(a)) for a in arg.arguments
|
|
636
|
+
]
|
|
637
|
+
inputs = "\n".join(indented_args)
|
|
638
|
+
return f"CASE\n{inputs}\n{self.indent_context.current_indent}END"
|
|
639
|
+
|
|
640
|
+
if arg.operator == FunctionType.STRUCT:
|
|
641
|
+
# zip arguments to pairs
|
|
642
|
+
input_pairs = zip(arg.arguments[0::2], arg.arguments[1::2])
|
|
643
|
+
with self.indented():
|
|
644
|
+
pair_strings = []
|
|
645
|
+
for k, v in input_pairs:
|
|
646
|
+
pair_line = f"{self.to_string(k)}-> {v}"
|
|
647
|
+
pair_strings.append(self.indent_lines(pair_line))
|
|
648
|
+
inputs = ",\n".join(pair_strings)
|
|
649
|
+
return f"struct(\n{inputs}\n{self.indent_context.current_indent})"
|
|
650
|
+
|
|
651
|
+
inputs = ",".join(args)
|
|
553
652
|
return f"{arg.operator.value}({inputs})"
|
|
554
653
|
|
|
555
654
|
@to_string.register
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -139,7 +139,9 @@
|
|
|
139
139
|
|
|
140
140
|
order_list: _order_atom ("," _order_atom)* ","?
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
over_component: /,\s*[a-zA-Z\_][a-zA-Z0-9\_\.]*/ "END"?
|
|
143
|
+
|
|
144
|
+
over_list: concept_lit over_component*
|
|
143
145
|
|
|
144
146
|
ORDERING_DIRECTION: /ASC|DESC/i
|
|
145
147
|
|
trilogy/std/display.preql
CHANGED
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
type percent float; # Percentage value
|
|
4
4
|
|
|
5
|
-
def calc_percent(a, b, digits=-1) -> case when digits =-1 then
|
|
6
|
-
|
|
5
|
+
def calc_percent(a, b, digits=-1) -> case when digits =-1 then
|
|
6
|
+
case when b = 0 then 0.0::float::percent else
|
|
7
|
+
(a/b)::float::percent end
|
|
8
|
+
else round((case when b = 0 then 0.0::float::percent else
|
|
9
|
+
(a/b)::float::percent end):: float::percent, digits) end;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|