pytrilogy 0.0.2.11__py3-none-any.whl → 0.0.2.12__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.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/RECORD +27 -27
- trilogy/__init__.py +1 -1
- trilogy/core/enums.py +0 -1
- trilogy/core/environment_helpers.py +44 -6
- trilogy/core/models.py +21 -21
- trilogy/core/optimization.py +31 -3
- trilogy/core/optimizations/__init__.py +2 -1
- trilogy/core/optimizations/predicate_pushdown.py +60 -42
- trilogy/core/processing/concept_strategies_v3.py +6 -4
- trilogy/core/processing/node_generators/basic_node.py +15 -9
- trilogy/core/processing/node_generators/node_merge_node.py +22 -1
- trilogy/core/processing/node_generators/unnest_node.py +10 -3
- trilogy/core/processing/nodes/base_node.py +7 -2
- trilogy/core/processing/nodes/group_node.py +0 -1
- trilogy/core/processing/nodes/merge_node.py +11 -4
- trilogy/core/processing/nodes/unnest_node.py +13 -9
- trilogy/core/processing/utility.py +3 -1
- trilogy/core/query_processor.py +9 -2
- trilogy/dialect/base.py +95 -52
- trilogy/dialect/common.py +3 -3
- trilogy/parsing/common.py +58 -1
- trilogy/parsing/parse_engine.py +70 -122
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
trilogy/__init__.py,sha256=qXNp3R3OFRd_QUIgMqKJ4RGQoBEMVcm6s2DHSehlVWU,291
|
|
2
2
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
trilogy/constants.py,sha256=HRQq4i3cpSEJCywt61QKEzRO1jd4tEPZNSBuxUA_7yg,922
|
|
4
4
|
trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
|
|
@@ -8,50 +8,50 @@ trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
8
8
|
trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
|
|
9
9
|
trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
|
|
11
|
-
trilogy/core/enums.py,sha256=
|
|
11
|
+
trilogy/core/enums.py,sha256=WwQPOLfSdL27qhqXw4JTkMyUNvj57T3Xz9M3JUZzhZ8,5940
|
|
12
12
|
trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
|
|
13
|
-
trilogy/core/environment_helpers.py,sha256=
|
|
13
|
+
trilogy/core/environment_helpers.py,sha256=1miP4is4FEoci01KSAy2VZVYmlmT5TOCOALBekd2muQ,7211
|
|
14
14
|
trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
|
|
15
15
|
trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
|
|
16
16
|
trilogy/core/functions.py,sha256=ARJAyBjeS415-54k3G_bx807rkPZonEulMaLRxSP7vU,10371
|
|
17
17
|
trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
|
|
18
18
|
trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
|
|
19
|
-
trilogy/core/models.py,sha256=
|
|
20
|
-
trilogy/core/optimization.py,sha256=
|
|
21
|
-
trilogy/core/query_processor.py,sha256=
|
|
22
|
-
trilogy/core/optimizations/__init__.py,sha256=
|
|
19
|
+
trilogy/core/models.py,sha256=kJcSNv6JX79KBEDTZlIyc1nAsn34fWUQLWZN3y7oTVs,143710
|
|
20
|
+
trilogy/core/optimization.py,sha256=7E-Ol51u6ZAxF56F_bzLxgRO-Hu6Yl1ZbPopZJB2tqk,7533
|
|
21
|
+
trilogy/core/query_processor.py,sha256=Y8C03J9PSyXQoARiMmFomYhnP13L61XjRKNOD7nIops,19520
|
|
22
|
+
trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
|
|
23
23
|
trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
|
|
24
24
|
trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
|
|
25
25
|
trilogy/core/optimizations/inline_datasource.py,sha256=AATzQ6YrtW_1-aQFjQyTYqEYKBoMFhek7ADfBr4uUdQ,3634
|
|
26
|
-
trilogy/core/optimizations/predicate_pushdown.py,sha256=
|
|
26
|
+
trilogy/core/optimizations/predicate_pushdown.py,sha256=3hSS1i1itR5lEmLwebuhz4FiPIyFjr2pBeydZfHHPSk,8433
|
|
27
27
|
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
trilogy/core/processing/concept_strategies_v3.py,sha256=
|
|
28
|
+
trilogy/core/processing/concept_strategies_v3.py,sha256=ae6FmwiKNiEbOU2GhnzggFMh82MhqxBj9bgr0ituT2w,25633
|
|
29
29
|
trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
|
|
30
|
-
trilogy/core/processing/utility.py,sha256=
|
|
30
|
+
trilogy/core/processing/utility.py,sha256=QKaZL5yJzGJBWCirgB1cAKgcDOibhyk7ETvHveb3GOE,14604
|
|
31
31
|
trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
|
|
32
|
-
trilogy/core/processing/node_generators/basic_node.py,sha256=
|
|
32
|
+
trilogy/core/processing/node_generators/basic_node.py,sha256=EfCCYleCXVWeoCOUih1VtfUXewg1oyG7EdUMRQOyyMk,3135
|
|
33
33
|
trilogy/core/processing/node_generators/common.py,sha256=lDBRq9X6dQ_xSwXxLLNDq2pW8D-XwAY-ylTJLMugkLw,9525
|
|
34
34
|
trilogy/core/processing/node_generators/filter_node.py,sha256=Ij2WqyOsu-TFxhAcL50PLMGpghsSWXJnWEJ8yTqOwrY,8228
|
|
35
35
|
trilogy/core/processing/node_generators/group_node.py,sha256=Du-9uFXD0M-aHq2MV7v5R3QCrAL0JZBFMW-YQwgb6Bw,3135
|
|
36
36
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=nzITnhaALIT7FMonyo16nNo-kSrLfefa9sZBYecrvkU,2887
|
|
37
37
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=vP84dnLQy6dtypi6mUbt9sMAcmmrTgQ1Oz4GI6X1IEo,6421
|
|
38
|
-
trilogy/core/processing/node_generators/node_merge_node.py,sha256=
|
|
38
|
+
trilogy/core/processing/node_generators/node_merge_node.py,sha256=D_jsnfoLMrQc08_JvT0wEDvjyzJAxBpdcZFyDN-feV0,13192
|
|
39
39
|
trilogy/core/processing/node_generators/rowset_node.py,sha256=6KVnuk75mRzWJ-jIk7e8azN8BIPPuCn-VxPlxDqfPVE,4616
|
|
40
40
|
trilogy/core/processing/node_generators/select_node.py,sha256=E8bKOAUpwLwZy1iiaFVD5sM4XK-eFpHgijdyIWLMyH4,18904
|
|
41
|
-
trilogy/core/processing/node_generators/unnest_node.py,sha256=
|
|
41
|
+
trilogy/core/processing/node_generators/unnest_node.py,sha256=aZeixbOzMtXi7BPahKr9bOkIhTciyD9Klsj0kZ56F6s,2189
|
|
42
42
|
trilogy/core/processing/node_generators/window_node.py,sha256=lFfmEjX_mLB7MuOM6CuKNnks1CabokGImpwhbQzjnkE,3283
|
|
43
43
|
trilogy/core/processing/nodes/__init__.py,sha256=jyduHk96j5fpju72sc8swOiBjR3Md866kt8JZGkp3ZU,4866
|
|
44
|
-
trilogy/core/processing/nodes/base_node.py,sha256=
|
|
44
|
+
trilogy/core/processing/nodes/base_node.py,sha256=szquAzrIkCTXlVhAVSHt6HSJ7rw3b8lfjeO5eFIcEU8,13067
|
|
45
45
|
trilogy/core/processing/nodes/filter_node.py,sha256=DBOSGFfkiILrZa1BlLv2uxUSkgWtSIKiZplqyKXPjg8,2132
|
|
46
|
-
trilogy/core/processing/nodes/group_node.py,sha256=
|
|
47
|
-
trilogy/core/processing/nodes/merge_node.py,sha256=
|
|
46
|
+
trilogy/core/processing/nodes/group_node.py,sha256=wE6tgyCUL74v76O8jACDm4oYMov4dAlwzLa5xMYReAA,6294
|
|
47
|
+
trilogy/core/processing/nodes/merge_node.py,sha256=34eEH-denk9kkzD8FcZvxgDSMUB9K5e4lSeNpbqSt7I,14456
|
|
48
48
|
trilogy/core/processing/nodes/select_node_v2.py,sha256=QuXNcwgjTRYamOoIooGrp4ie6INcqA9whtC5LZWjD8s,7180
|
|
49
|
-
trilogy/core/processing/nodes/unnest_node.py,sha256=
|
|
49
|
+
trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
|
|
50
50
|
trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
|
|
51
51
|
trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
trilogy/dialect/base.py,sha256=
|
|
52
|
+
trilogy/dialect/base.py,sha256=kQek_ufZC9HDVOKlWasvIx6xyew8wv3JNIU6r53_IR4,32842
|
|
53
53
|
trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
|
|
54
|
-
trilogy/dialect/common.py,sha256=
|
|
54
|
+
trilogy/dialect/common.py,sha256=LnOtsq4vUTTKB5QUk594QSoNDfOoOF08KQJZZamou84,3359
|
|
55
55
|
trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
|
|
56
56
|
trilogy/dialect/duckdb.py,sha256=u_gpL35kouWxoBLas1h0ABYY2QzlVtEh22hm5h0lCOM,3182
|
|
57
57
|
trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
|
|
@@ -65,18 +65,18 @@ trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2
|
|
|
65
65
|
trilogy/hooks/query_debugger.py,sha256=NDChfkPmmW-KINa4TaQmDe_adGiwsKFdGLDSYpbodeU,4282
|
|
66
66
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
67
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
-
trilogy/parsing/common.py,sha256=
|
|
68
|
+
trilogy/parsing/common.py,sha256=rLF7SFj_qtLh92ox-cHrtVSyjgl1aTaa7qZJdR1RDuA,8182
|
|
69
69
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
70
70
|
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
71
71
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
72
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
72
|
+
trilogy/parsing/parse_engine.py,sha256=upL2pmq34vwNqzOtmb0EB22tbEZ4TTiK46J5qPq-_yw,62841
|
|
73
73
|
trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
|
|
74
74
|
trilogy/parsing/trilogy.lark,sha256=QNJnExOdvJyKTrQA4ffh-SGIz7rYd93kf2Ccs0m3cn4,11498
|
|
75
75
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
76
|
trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
|
|
77
|
-
pytrilogy-0.0.2.
|
|
78
|
-
pytrilogy-0.0.2.
|
|
79
|
-
pytrilogy-0.0.2.
|
|
80
|
-
pytrilogy-0.0.2.
|
|
81
|
-
pytrilogy-0.0.2.
|
|
82
|
-
pytrilogy-0.0.2.
|
|
77
|
+
pytrilogy-0.0.2.12.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
78
|
+
pytrilogy-0.0.2.12.dist-info/METADATA,sha256=i7Cd69-1p3XsssoHNy1IqbvtZaNeaKpQErvJp940JMw,7907
|
|
79
|
+
pytrilogy-0.0.2.12.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
|
80
|
+
pytrilogy-0.0.2.12.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
|
|
81
|
+
pytrilogy-0.0.2.12.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
82
|
+
pytrilogy-0.0.2.12.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/core/enums.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
from trilogy.core.models import
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
DataType,
|
|
3
|
+
Concept,
|
|
4
|
+
Environment,
|
|
5
|
+
Function,
|
|
6
|
+
Metadata,
|
|
7
|
+
StructType,
|
|
8
|
+
)
|
|
9
|
+
from trilogy.core.functions import AttrAccess
|
|
2
10
|
from trilogy.core.enums import Purpose, FunctionType, ConceptSource
|
|
3
11
|
from trilogy.constants import DEFAULT_NAMESPACE
|
|
12
|
+
from trilogy.parsing.common import process_function_args, arg_to_datatype, Meta
|
|
4
13
|
|
|
5
14
|
|
|
6
15
|
def generate_date_concepts(concept: Concept, environment: Environment):
|
|
@@ -142,15 +151,44 @@ def generate_key_concepts(concept: Concept, environment: Environment):
|
|
|
142
151
|
environment.add_concept(new_concept, add_derived=False)
|
|
143
152
|
|
|
144
153
|
|
|
145
|
-
def generate_related_concepts(
|
|
154
|
+
def generate_related_concepts(
|
|
155
|
+
concept: Concept,
|
|
156
|
+
environment: Environment,
|
|
157
|
+
meta: Meta | None = None,
|
|
158
|
+
add_derived: bool = False,
|
|
159
|
+
):
|
|
146
160
|
"""Auto populate common derived concepts on types"""
|
|
147
|
-
if concept.purpose == Purpose.KEY:
|
|
161
|
+
if concept.purpose == Purpose.KEY and add_derived:
|
|
148
162
|
generate_key_concepts(concept, environment)
|
|
149
|
-
|
|
163
|
+
|
|
164
|
+
# datatype types
|
|
165
|
+
if concept.datatype == DataType.DATE and add_derived:
|
|
150
166
|
generate_date_concepts(concept, environment)
|
|
151
|
-
elif concept.datatype == DataType.DATETIME:
|
|
167
|
+
elif concept.datatype == DataType.DATETIME and add_derived:
|
|
152
168
|
generate_date_concepts(concept, environment)
|
|
153
169
|
generate_datetime_concepts(concept, environment)
|
|
154
|
-
elif concept.datatype == DataType.TIMESTAMP:
|
|
170
|
+
elif concept.datatype == DataType.TIMESTAMP and add_derived:
|
|
155
171
|
generate_date_concepts(concept, environment)
|
|
156
172
|
generate_datetime_concepts(concept, environment)
|
|
173
|
+
|
|
174
|
+
if isinstance(concept.datatype, StructType):
|
|
175
|
+
for key, value in concept.datatype.fields_map.items():
|
|
176
|
+
args = process_function_args(
|
|
177
|
+
[concept, key], meta=meta, environment=environment
|
|
178
|
+
)
|
|
179
|
+
auto = Concept(
|
|
180
|
+
name=key,
|
|
181
|
+
datatype=arg_to_datatype(value),
|
|
182
|
+
purpose=Purpose.PROPERTY,
|
|
183
|
+
namespace=(
|
|
184
|
+
environment.namespace + "." + concept.name
|
|
185
|
+
if environment.namespace
|
|
186
|
+
and environment.namespace != DEFAULT_NAMESPACE
|
|
187
|
+
else concept.name
|
|
188
|
+
),
|
|
189
|
+
lineage=AttrAccess(args),
|
|
190
|
+
)
|
|
191
|
+
environment.add_concept(auto, meta=meta)
|
|
192
|
+
if isinstance(value, Concept):
|
|
193
|
+
environment.merge_concept(auto, value, modifiers=[])
|
|
194
|
+
assert value.pseudonyms is not None
|
trilogy/core/models.py
CHANGED
|
@@ -300,7 +300,7 @@ class MapType(BaseModel):
|
|
|
300
300
|
|
|
301
301
|
class StructType(BaseModel):
|
|
302
302
|
fields: List[ALL_TYPES]
|
|
303
|
-
fields_map: Dict[str, Concept | int | float | str]
|
|
303
|
+
fields_map: Dict[str, Concept | int | float | str]
|
|
304
304
|
|
|
305
305
|
@property
|
|
306
306
|
def data_type(self):
|
|
@@ -2119,16 +2119,19 @@ class Datasource(Namespaced, BaseModel):
|
|
|
2119
2119
|
|
|
2120
2120
|
|
|
2121
2121
|
class UnnestJoin(BaseModel):
|
|
2122
|
-
|
|
2122
|
+
concepts: list[Concept]
|
|
2123
|
+
parent: Function
|
|
2123
2124
|
alias: str = "unnest"
|
|
2124
2125
|
rendering_required: bool = True
|
|
2125
2126
|
|
|
2126
2127
|
def __hash__(self):
|
|
2127
|
-
return (
|
|
2128
|
+
return (
|
|
2129
|
+
self.alias + "".join([str(s.address) for s in self.concepts])
|
|
2130
|
+
).__hash__()
|
|
2128
2131
|
|
|
2129
2132
|
|
|
2130
2133
|
class InstantiatedUnnestJoin(BaseModel):
|
|
2131
|
-
|
|
2134
|
+
concept_to_unnest: Concept
|
|
2132
2135
|
alias: str = "unnest"
|
|
2133
2136
|
|
|
2134
2137
|
|
|
@@ -2268,6 +2271,7 @@ class QueryDatasource(BaseModel):
|
|
|
2268
2271
|
raise SyntaxError(
|
|
2269
2272
|
f"Cannot join a datasource to itself, joining {join.left_datasource}"
|
|
2270
2273
|
)
|
|
2274
|
+
|
|
2271
2275
|
return v
|
|
2272
2276
|
|
|
2273
2277
|
@field_validator("input_concepts")
|
|
@@ -2287,8 +2291,13 @@ class QueryDatasource(BaseModel):
|
|
|
2287
2291
|
for key in ("input_concepts", "output_concepts"):
|
|
2288
2292
|
if not values.get(key):
|
|
2289
2293
|
continue
|
|
2294
|
+
concept: Concept
|
|
2290
2295
|
for concept in values[key]:
|
|
2291
|
-
if
|
|
2296
|
+
if (
|
|
2297
|
+
concept.address not in v
|
|
2298
|
+
and not any(x in v for x in concept.pseudonyms)
|
|
2299
|
+
and CONFIG.validate_missing
|
|
2300
|
+
):
|
|
2292
2301
|
raise SyntaxError(
|
|
2293
2302
|
f"Missing source map for {concept.address} on {key}, have {v}"
|
|
2294
2303
|
)
|
|
@@ -2533,7 +2542,7 @@ class CTE(BaseModel):
|
|
|
2533
2542
|
)
|
|
2534
2543
|
]
|
|
2535
2544
|
for join in self.joins:
|
|
2536
|
-
if isinstance(join, UnnestJoin) and
|
|
2545
|
+
if isinstance(join, UnnestJoin) and concept in join.concepts:
|
|
2537
2546
|
join.rendering_required = False
|
|
2538
2547
|
|
|
2539
2548
|
self.parent_ctes = [
|
|
@@ -2996,8 +3005,8 @@ class EnvironmentDatasourceDict(dict):
|
|
|
2996
3005
|
except KeyError:
|
|
2997
3006
|
if DEFAULT_NAMESPACE + "." + key in self:
|
|
2998
3007
|
return self.__getitem__(DEFAULT_NAMESPACE + "." + key)
|
|
2999
|
-
if "." in key and key.split(".")[0] == DEFAULT_NAMESPACE:
|
|
3000
|
-
return self.__getitem__(key.split(".")[1])
|
|
3008
|
+
if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
|
|
3009
|
+
return self.__getitem__(key.split(".", 1)[1])
|
|
3001
3010
|
raise
|
|
3002
3011
|
|
|
3003
3012
|
def values(self) -> ValuesView[Datasource]: # type: ignore
|
|
@@ -3027,8 +3036,8 @@ class EnvironmentConceptDict(dict):
|
|
|
3027
3036
|
return super(EnvironmentConceptDict, self).__getitem__(key)
|
|
3028
3037
|
|
|
3029
3038
|
except KeyError:
|
|
3030
|
-
if "." in key and key.split(".")[0] == DEFAULT_NAMESPACE:
|
|
3031
|
-
return self.__getitem__(key.split(".")[1], line_no)
|
|
3039
|
+
if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
|
|
3040
|
+
return self.__getitem__(key.split(".", 1)[1], line_no)
|
|
3032
3041
|
if DEFAULT_NAMESPACE + "." + key in self:
|
|
3033
3042
|
return self.__getitem__(DEFAULT_NAMESPACE + "." + key, line_no)
|
|
3034
3043
|
if not self.fail_on_missing:
|
|
@@ -3293,10 +3302,9 @@ class Environment(BaseModel):
|
|
|
3293
3302
|
self.concepts[concept.name] = concept
|
|
3294
3303
|
else:
|
|
3295
3304
|
self.concepts[concept.address] = concept
|
|
3296
|
-
|
|
3297
|
-
from trilogy.core.environment_helpers import generate_related_concepts
|
|
3305
|
+
from trilogy.core.environment_helpers import generate_related_concepts
|
|
3298
3306
|
|
|
3299
|
-
|
|
3307
|
+
generate_related_concepts(concept, self, meta=meta, add_derived=add_derived)
|
|
3300
3308
|
self.gen_concept_list_caches()
|
|
3301
3309
|
return concept
|
|
3302
3310
|
|
|
@@ -3423,14 +3431,6 @@ class Comparison(
|
|
|
3423
3431
|
raise SyntaxError(
|
|
3424
3432
|
f"Cannot compare {self.left} and {self.right} of different types"
|
|
3425
3433
|
)
|
|
3426
|
-
if self.operator == ComparisonOperator.BETWEEN:
|
|
3427
|
-
if (
|
|
3428
|
-
not isinstance(self.right, ComparisonOperator)
|
|
3429
|
-
and self.right.operator == BooleanOperator.AND
|
|
3430
|
-
):
|
|
3431
|
-
raise SyntaxError(
|
|
3432
|
-
f"Between operator must have two operands with and, not {self.right}"
|
|
3433
|
-
)
|
|
3434
3434
|
|
|
3435
3435
|
def __add__(self, other):
|
|
3436
3436
|
if other is None:
|
trilogy/core/optimization.py
CHANGED
|
@@ -10,6 +10,7 @@ from trilogy.core.optimizations import (
|
|
|
10
10
|
OptimizationRule,
|
|
11
11
|
InlineConstant,
|
|
12
12
|
PredicatePushdown,
|
|
13
|
+
PredicatePushdownRemove,
|
|
13
14
|
InlineDatasource,
|
|
14
15
|
)
|
|
15
16
|
|
|
@@ -34,6 +35,31 @@ MAX_OPTIMIZATION_LOOPS = 100
|
|
|
34
35
|
# return parent
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def reorder_ctes(
|
|
39
|
+
input: list[CTE],
|
|
40
|
+
):
|
|
41
|
+
import networkx as nx
|
|
42
|
+
|
|
43
|
+
# Create a directed graph
|
|
44
|
+
G = nx.DiGraph()
|
|
45
|
+
mapping: dict[str, CTE] = {}
|
|
46
|
+
for cte in input:
|
|
47
|
+
mapping[cte.name] = cte
|
|
48
|
+
for parent in cte.parent_ctes:
|
|
49
|
+
G.add_edge(parent.name, cte.name)
|
|
50
|
+
# Perform topological sort (only works for DAGs)
|
|
51
|
+
try:
|
|
52
|
+
topological_order = list(nx.topological_sort(G))
|
|
53
|
+
if not topological_order:
|
|
54
|
+
return input
|
|
55
|
+
return [mapping[x] for x in topological_order]
|
|
56
|
+
except nx.NetworkXUnfeasible as e:
|
|
57
|
+
print(
|
|
58
|
+
"The graph is not a DAG (contains cycles) and cannot be topologically sorted."
|
|
59
|
+
)
|
|
60
|
+
raise e
|
|
61
|
+
|
|
62
|
+
|
|
37
63
|
def filter_irrelevant_ctes(
|
|
38
64
|
input: list[CTE],
|
|
39
65
|
root_cte: CTE,
|
|
@@ -169,20 +195,22 @@ def optimize_ctes(
|
|
|
169
195
|
REGISTERED_RULES.append(InlineDatasource())
|
|
170
196
|
if CONFIG.optimizations.predicate_pushdown:
|
|
171
197
|
REGISTERED_RULES.append(PredicatePushdown())
|
|
172
|
-
|
|
198
|
+
if CONFIG.optimizations.predicate_pushdown:
|
|
199
|
+
REGISTERED_RULES.append(PredicatePushdownRemove())
|
|
173
200
|
for rule in REGISTERED_RULES:
|
|
174
201
|
loops = 0
|
|
175
202
|
complete = False
|
|
176
203
|
while not complete and (loops <= MAX_OPTIMIZATION_LOOPS):
|
|
177
204
|
actions_taken = False
|
|
178
205
|
# assume we go through all CTEs once
|
|
179
|
-
look_at = [root_cte, *input]
|
|
206
|
+
look_at = [root_cte, *reversed(input)]
|
|
180
207
|
inverse_map = gen_inverse_map(look_at)
|
|
181
208
|
for cte in look_at:
|
|
182
209
|
opt = rule.optimize(cte, inverse_map)
|
|
183
210
|
actions_taken = actions_taken or opt
|
|
184
211
|
complete = not actions_taken
|
|
185
212
|
loops += 1
|
|
213
|
+
input = reorder_ctes(filter_irrelevant_ctes(input, root_cte))
|
|
186
214
|
logger.info(f"finished checking for {type(rule).__name__} in {loops} loops")
|
|
187
215
|
|
|
188
|
-
return filter_irrelevant_ctes(input, root_cte)
|
|
216
|
+
return reorder_ctes(filter_irrelevant_ctes(input, root_cte))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .inline_constant import InlineConstant
|
|
2
2
|
from .inline_datasource import InlineDatasource
|
|
3
|
-
from .predicate_pushdown import PredicatePushdown
|
|
3
|
+
from .predicate_pushdown import PredicatePushdown, PredicatePushdownRemove
|
|
4
4
|
from .base_optimization import OptimizationRule
|
|
5
5
|
|
|
6
6
|
__all__ = [
|
|
@@ -8,4 +8,5 @@ __all__ = [
|
|
|
8
8
|
"InlineConstant",
|
|
9
9
|
"InlineDatasource",
|
|
10
10
|
"PredicatePushdown",
|
|
11
|
+
"PredicatePushdownRemove",
|
|
11
12
|
]
|
|
@@ -114,48 +114,6 @@ class PredicatePushdown(OptimizationRule):
|
|
|
114
114
|
if not cte.condition:
|
|
115
115
|
self.debug(f"No CTE condition for {cte.name}")
|
|
116
116
|
return False
|
|
117
|
-
|
|
118
|
-
parent_filter_status = {
|
|
119
|
-
parent.name: is_child_of(cte.condition, parent.condition)
|
|
120
|
-
for parent in cte.parent_ctes
|
|
121
|
-
}
|
|
122
|
-
# flatten existnce argument tuples to a list
|
|
123
|
-
|
|
124
|
-
flattened_existence = [
|
|
125
|
-
x.address for y in cte.condition.existence_arguments for x in y
|
|
126
|
-
]
|
|
127
|
-
|
|
128
|
-
existence_only = [
|
|
129
|
-
parent.name
|
|
130
|
-
for parent in cte.parent_ctes
|
|
131
|
-
if all([x.address in flattened_existence for x in parent.output_columns])
|
|
132
|
-
and len(flattened_existence) > 0
|
|
133
|
-
]
|
|
134
|
-
if all(
|
|
135
|
-
[
|
|
136
|
-
value
|
|
137
|
-
for key, value in parent_filter_status.items()
|
|
138
|
-
if key not in existence_only
|
|
139
|
-
]
|
|
140
|
-
) and not any([isinstance(x, Datasource) for x in cte.source.datasources]):
|
|
141
|
-
self.log(
|
|
142
|
-
f"All parents of {cte.name} have same filter or are existence only inputs, removing filter from {cte.name}"
|
|
143
|
-
)
|
|
144
|
-
cte.condition = None
|
|
145
|
-
# remove any "parent" CTEs that provided only existence inputs
|
|
146
|
-
if existence_only:
|
|
147
|
-
original = [y.name for y in cte.parent_ctes]
|
|
148
|
-
cte.parent_ctes = [
|
|
149
|
-
x for x in cte.parent_ctes if x.name not in existence_only
|
|
150
|
-
]
|
|
151
|
-
self.log(
|
|
152
|
-
f"new parents for {cte.name} are {[x.name for x in cte.parent_ctes]}, vs {original}"
|
|
153
|
-
)
|
|
154
|
-
return True
|
|
155
|
-
else:
|
|
156
|
-
self.log(
|
|
157
|
-
f"Could not remove filter from {cte.name}, as not all parents have the same filter: {parent_filter_status}"
|
|
158
|
-
)
|
|
159
117
|
if self.complete.get(cte.name):
|
|
160
118
|
self.debug("Have done this CTE before")
|
|
161
119
|
return False
|
|
@@ -197,3 +155,63 @@ class PredicatePushdown(OptimizationRule):
|
|
|
197
155
|
|
|
198
156
|
self.complete[cte.name] = True
|
|
199
157
|
return optimized
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class PredicatePushdownRemove(OptimizationRule):
|
|
161
|
+
|
|
162
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
163
|
+
super().__init__(*args, **kwargs)
|
|
164
|
+
self.complete: dict[str, bool] = {}
|
|
165
|
+
|
|
166
|
+
def optimize(self, cte: CTE, inverse_map: dict[str, list[CTE]]) -> bool:
|
|
167
|
+
optimized = False
|
|
168
|
+
|
|
169
|
+
if not cte.parent_ctes:
|
|
170
|
+
self.debug(f"No parent CTEs for {cte.name}")
|
|
171
|
+
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
if not cte.condition:
|
|
175
|
+
self.debug(f"No CTE condition for {cte.name}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
parent_filter_status = {
|
|
179
|
+
parent.name: is_child_of(cte.condition, parent.condition)
|
|
180
|
+
for parent in cte.parent_ctes
|
|
181
|
+
}
|
|
182
|
+
# flatten existnce argument tuples to a list
|
|
183
|
+
|
|
184
|
+
flattened_existence = [
|
|
185
|
+
x.address for y in cte.condition.existence_arguments for x in y
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
existence_only = [
|
|
189
|
+
parent.name
|
|
190
|
+
for parent in cte.parent_ctes
|
|
191
|
+
if all([x.address in flattened_existence for x in parent.output_columns])
|
|
192
|
+
and len(flattened_existence) > 0
|
|
193
|
+
]
|
|
194
|
+
if all(
|
|
195
|
+
[
|
|
196
|
+
value
|
|
197
|
+
for key, value in parent_filter_status.items()
|
|
198
|
+
if key not in existence_only
|
|
199
|
+
]
|
|
200
|
+
) and not any([isinstance(x, Datasource) for x in cte.source.datasources]):
|
|
201
|
+
self.log(
|
|
202
|
+
f"All parents of {cte.name} have same filter or are existence only inputs, removing filter from {cte.name}"
|
|
203
|
+
)
|
|
204
|
+
cte.condition = None
|
|
205
|
+
# remove any "parent" CTEs that provided only existence inputs
|
|
206
|
+
if existence_only:
|
|
207
|
+
original = [y.name for y in cte.parent_ctes]
|
|
208
|
+
cte.parent_ctes = [
|
|
209
|
+
x for x in cte.parent_ctes if x.name not in existence_only
|
|
210
|
+
]
|
|
211
|
+
self.log(
|
|
212
|
+
f"new parents for {cte.name} are {[x.name for x in cte.parent_ctes]}, vs {original}"
|
|
213
|
+
)
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
self.complete[cte.name] = True
|
|
217
|
+
return optimized
|
|
@@ -180,7 +180,10 @@ def generate_candidates_restrictive(
|
|
|
180
180
|
local_candidates = [
|
|
181
181
|
x
|
|
182
182
|
for x in list(candidates)
|
|
183
|
-
if x.address not in exhausted
|
|
183
|
+
if x.address not in exhausted
|
|
184
|
+
and x.granularity != Granularity.SINGLE_ROW
|
|
185
|
+
and x.address not in priority_concept.pseudonyms
|
|
186
|
+
and priority_concept.address not in x.pseudonyms
|
|
184
187
|
]
|
|
185
188
|
combos: list[list[Concept]] = []
|
|
186
189
|
grain_check = Grain(components=[*local_candidates]).components_copy
|
|
@@ -608,7 +611,7 @@ def _search_concepts(
|
|
|
608
611
|
if len(stack) == 1:
|
|
609
612
|
output = stack[0]
|
|
610
613
|
logger.info(
|
|
611
|
-
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)} with output {[x.address for x in output.output_concepts]}"
|
|
614
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)} with output {[x.address for x in output.output_concepts]} and {output.resolve().source_map}"
|
|
612
615
|
)
|
|
613
616
|
return output
|
|
614
617
|
|
|
@@ -658,8 +661,7 @@ def _search_concepts(
|
|
|
658
661
|
if x.address not in [y.address for y in mandatory_list]
|
|
659
662
|
and x not in ex_resolve.grain.components
|
|
660
663
|
]
|
|
661
|
-
expanded.
|
|
662
|
-
expanded.rebuild_cache()
|
|
664
|
+
expanded.set_output_concepts(mandatory_list)
|
|
663
665
|
|
|
664
666
|
logger.info(
|
|
665
667
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in mandatory_list]} via concept addition; removing extra {[c.address for c in extra]}"
|
|
@@ -10,6 +10,7 @@ from trilogy.core.processing.node_generators.common import (
|
|
|
10
10
|
)
|
|
11
11
|
from trilogy.utility import unique
|
|
12
12
|
from trilogy.constants import logger
|
|
13
|
+
from itertools import combinations
|
|
13
14
|
|
|
14
15
|
LOGGER_PREFIX = "[GEN_BASIC_NODE]"
|
|
15
16
|
|
|
@@ -31,12 +32,17 @@ def gen_basic_node(
|
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
local_optional_redundant = [x for x in local_optional if x in parent_concepts]
|
|
34
|
-
attempts
|
|
35
|
-
|
|
35
|
+
attempts: List[tuple[list[Concept], list[Concept]]] = [
|
|
36
|
+
(parent_concepts, [concept] + local_optional_redundant)
|
|
37
|
+
]
|
|
38
|
+
equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
|
|
39
|
+
non_equivalent_optional = [
|
|
40
|
+
x for x in local_optional if x not in equivalent_optional
|
|
41
|
+
]
|
|
36
42
|
|
|
37
43
|
if local_optional:
|
|
38
|
-
for combo in range(1, len(
|
|
39
|
-
combos = combinations(
|
|
44
|
+
for combo in range(1, len(non_equivalent_optional) + 1):
|
|
45
|
+
combos = combinations(non_equivalent_optional, combo)
|
|
40
46
|
for optional_set in combos:
|
|
41
47
|
attempts.append(
|
|
42
48
|
(
|
|
@@ -64,13 +70,10 @@ def gen_basic_node(
|
|
|
64
70
|
continue
|
|
65
71
|
if all(x in source.partial_concepts for source in sources):
|
|
66
72
|
partials.append(x)
|
|
67
|
-
outputs = parent_node.output_concepts + [concept]
|
|
68
|
-
logger.info(
|
|
69
|
-
f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept} with attempted extra {[x.address for x in attempt]}, output {[x.address for x in outputs]}"
|
|
70
|
-
)
|
|
71
|
-
# parents.resolve()
|
|
72
73
|
|
|
73
74
|
parent_node.add_output_concept(concept)
|
|
75
|
+
for x in equivalent_optional:
|
|
76
|
+
parent_node.add_output_concept(x)
|
|
74
77
|
|
|
75
78
|
parent_node.remove_output_concepts(
|
|
76
79
|
[
|
|
@@ -79,6 +82,9 @@ def gen_basic_node(
|
|
|
79
82
|
if x.address not in [y.address for y in basic_output]
|
|
80
83
|
]
|
|
81
84
|
)
|
|
85
|
+
logger.info(
|
|
86
|
+
f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept} with attempted extra {[x.address for x in attempt]}, output {[x.address for x in parent_node.output_concepts]}"
|
|
87
|
+
)
|
|
82
88
|
return parent_node
|
|
83
89
|
logger.info(
|
|
84
90
|
f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
|
|
@@ -86,7 +86,7 @@ def determine_induced_minimal_nodes(
|
|
|
86
86
|
|
|
87
87
|
for node in G.nodes:
|
|
88
88
|
if concepts.get(node):
|
|
89
|
-
lookup = concepts[node]
|
|
89
|
+
lookup: Concept = concepts[node]
|
|
90
90
|
if lookup.derivation not in (PurposeLineage.BASIC, PurposeLineage.ROOT):
|
|
91
91
|
nodes_to_remove.append(node)
|
|
92
92
|
elif lookup.derivation == PurposeLineage.BASIC and G.out_degree(node) == 0:
|
|
@@ -155,6 +155,26 @@ def detect_ambiguity_and_raise(all_concepts, reduced_concept_sets) -> None:
|
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def has_synonym(concept: Concept, others: list[list[Concept]]) -> bool:
|
|
159
|
+
return any(
|
|
160
|
+
c.address in concept.pseudonyms or concept.address in c.pseudonyms
|
|
161
|
+
for sublist in others
|
|
162
|
+
for c in sublist
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def filter_relevant_subgraphs(subgraphs: list[list[Concept]]) -> list[list[Concept]]:
|
|
167
|
+
return [
|
|
168
|
+
subgraph
|
|
169
|
+
for subgraph in subgraphs
|
|
170
|
+
if len(subgraph) > 1
|
|
171
|
+
or (
|
|
172
|
+
len(subgraph) == 1
|
|
173
|
+
and not has_synonym(subgraph[0], [x for x in subgraphs if x != subgraph])
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
158
178
|
def resolve_weak_components(
|
|
159
179
|
all_concepts: List[Concept],
|
|
160
180
|
environment: Environment,
|
|
@@ -249,6 +269,7 @@ def resolve_weak_components(
|
|
|
249
269
|
continue
|
|
250
270
|
subgraphs.append(sub_component)
|
|
251
271
|
return subgraphs
|
|
272
|
+
# return filter_relevant_subgraphs(subgraphs)
|
|
252
273
|
|
|
253
274
|
|
|
254
275
|
def subgraphs_to_merge_node(
|