pytrilogy 0.0.3.31__py3-none-any.whl → 0.0.3.33__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.31.dist-info → pytrilogy-0.0.3.33.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.31.dist-info → pytrilogy-0.0.3.33.dist-info}/RECORD +23 -23
- trilogy/__init__.py +1 -1
- trilogy/core/enums.py +1 -0
- trilogy/core/environment_helpers.py +25 -1
- trilogy/core/models/author.py +25 -7
- trilogy/core/models/build.py +1 -1
- trilogy/core/models/environment.py +1 -1
- trilogy/core/models/execute.py +1 -1
- trilogy/core/optimization.py +8 -0
- trilogy/core/processing/concept_strategies_v3.py +46 -2
- trilogy/core/processing/node_generators/group_node.py +3 -1
- trilogy/core/processing/node_generators/rowset_node.py +5 -1
- trilogy/core/processing/nodes/group_node.py +58 -1
- trilogy/core/processing/nodes/merge_node.py +13 -1
- trilogy/dialect/duckdb.py +1 -0
- trilogy/parsing/common.py +78 -29
- trilogy/parsing/parse_engine.py +9 -2
- trilogy/parsing/trilogy.lark +2 -1
- {pytrilogy-0.0.3.31.dist-info → pytrilogy-0.0.3.33.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.31.dist-info → pytrilogy-0.0.3.33.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.31.dist-info → pytrilogy-0.0.3.33.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.31.dist-info → pytrilogy-0.0.3.33.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
pytrilogy-0.0.3.
|
|
2
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
pytrilogy-0.0.3.33.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=d8QFTlSiA9H5vsTbRyWcF6hZW9Dih9-6ZorFRaWkb_Q,303
|
|
3
3
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
trilogy/constants.py,sha256=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
|
|
5
5
|
trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
|
|
@@ -11,42 +11,42 @@ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
|
|
|
11
11
|
trilogy/authoring/__init__.py,sha256=ohkYA3_LGYZh3fwzEYKTN6ofACDI5GYl3VCbGxVvlzY,2233
|
|
12
12
|
trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
|
|
14
|
-
trilogy/core/enums.py,sha256=
|
|
14
|
+
trilogy/core/enums.py,sha256=fWexUZtssfvP5TiD7eQ66Q_tPUCNCCTGNSzLbVXrnqQ,7358
|
|
15
15
|
trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
|
|
16
|
-
trilogy/core/environment_helpers.py,sha256=
|
|
16
|
+
trilogy/core/environment_helpers.py,sha256=5ayyhf4CGBlg_LssPu3DbS_H9H1Kq6Qog5TgR8pwyMk,8518
|
|
17
17
|
trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
|
|
18
18
|
trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,673
|
|
19
19
|
trilogy/core/functions.py,sha256=z5uyC5sAS_vnFBJcky7TjA0QAy-xI4Z9uD_9vK_XP1s,27134
|
|
20
20
|
trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
|
|
21
21
|
trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
|
|
22
|
-
trilogy/core/optimization.py,sha256=
|
|
22
|
+
trilogy/core/optimization.py,sha256=aihzx4-2-mSjx5td1TDTYGvc7e9Zvy-_xEyhPqLS-Ig,8314
|
|
23
23
|
trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
|
|
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=qEN8dJCnOMtB2Uu58GcbiYz6t5LiluCAdDUCUfl6BMk,76572
|
|
26
|
+
trilogy/core/models/build.py,sha256=N3Zr47iN4y9gFWS72uYjecmiQfcm8Fl9wT3QS8xKBAA,58185
|
|
27
27
|
trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
|
|
28
28
|
trilogy/core/models/core.py,sha256=wx6hJcFECMG-Ij972ADNkr-3nFXkYESr82ObPiC46_U,10875
|
|
29
29
|
trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
|
|
30
|
-
trilogy/core/models/environment.py,sha256=
|
|
31
|
-
trilogy/core/models/execute.py,sha256=
|
|
30
|
+
trilogy/core/models/environment.py,sha256=yd19vDdC-IiKPV5YbNZcDUOEr1BC_ZxE_5fQDIipa3s,26910
|
|
31
|
+
trilogy/core/models/execute.py,sha256=cmDPkVng3LHw87Q4WiOxJvDQJCoPhEiVMeMqxSxRj7Q,34261
|
|
32
32
|
trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
|
|
33
33
|
trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
|
|
34
34
|
trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52doLU9sIg3pmU2_S08,1332
|
|
35
35
|
trilogy/core/optimizations/inline_datasource.py,sha256=AHuTGh2x0GQ8usOe0NiFncfTFQ_KogdgDl4uucmhIbI,4241
|
|
36
36
|
trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
|
|
37
37
|
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
trilogy/core/processing/concept_strategies_v3.py,sha256=
|
|
38
|
+
trilogy/core/processing/concept_strategies_v3.py,sha256=MhSCS1M6etHaonfrPNwe6Gk-QuDTVGHkPjt1_9_gqko,42536
|
|
39
39
|
trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
|
|
40
40
|
trilogy/core/processing/utility.py,sha256=mnN_pewdpDgRou4QJ1JLcqYHyZdp8DrcsGsqW3QmA3o,20591
|
|
41
41
|
trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW6gjUJCsiXumdbJNozHUf-Y,800
|
|
42
42
|
trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
|
|
43
43
|
trilogy/core/processing/node_generators/common.py,sha256=ZsDzThjm_mAtdQpKAg8QIJiPVZ4KuUkKyilj4eOhSDs,9439
|
|
44
44
|
trilogy/core/processing/node_generators/filter_node.py,sha256=rlY7TbgjJlGhahYgdCIJpJbaSREAGVJEsyUIGaA38O0,8271
|
|
45
|
-
trilogy/core/processing/node_generators/group_node.py,sha256=
|
|
45
|
+
trilogy/core/processing/node_generators/group_node.py,sha256=kO-ersxIL04rZwX5-vFIFQQnp357PFo_7ZKXoGq3wyc,5989
|
|
46
46
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=E5bEjovSx422d_MlAUCDFdY4P2WJVp61BmWwltkhzA8,3095
|
|
47
47
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
|
|
48
48
|
trilogy/core/processing/node_generators/node_merge_node.py,sha256=sv55oynfqgpHEpo1OEtVDri-5fywzPhDlR85qaWikvY,16195
|
|
49
|
-
trilogy/core/processing/node_generators/rowset_node.py,sha256=
|
|
49
|
+
trilogy/core/processing/node_generators/rowset_node.py,sha256=YmBs6ZQ7azLXRFEmeoecpGjK4pMHsUCovuBxfb3UKZI,6848
|
|
50
50
|
trilogy/core/processing/node_generators/select_merge_node.py,sha256=pIsHfXWA30RkKSMBDKPtDmCriJtHoNKRMJC0THSDxpI,19951
|
|
51
51
|
trilogy/core/processing/node_generators/select_node.py,sha256=Y-zO0AFkTrpi2LyebjpyHU7WWANr7nKZSS9rY7DH4Wo,1888
|
|
52
52
|
trilogy/core/processing/node_generators/synonym_node.py,sha256=9LHK2XHDjbyTLjmDQieskG8fqbiSpRnFOkfrutDnOTE,2258
|
|
@@ -58,8 +58,8 @@ trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,s
|
|
|
58
58
|
trilogy/core/processing/nodes/__init__.py,sha256=DqPG3Y8vl5-UTeox6hn1EE6iwPIJpsM-XeZALHSgLZQ,5058
|
|
59
59
|
trilogy/core/processing/nodes/base_node.py,sha256=FHrY8GsTKPuMJklOjILbhGqCt5s1nmlj62Z-molARDA,16835
|
|
60
60
|
trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
|
|
61
|
-
trilogy/core/processing/nodes/group_node.py,sha256=
|
|
62
|
-
trilogy/core/processing/nodes/merge_node.py,sha256=
|
|
61
|
+
trilogy/core/processing/nodes/group_node.py,sha256=MUvcOg9U5J6TnWBel8eht9PdI9BfAKjUxmfjP_ZXx9o,10484
|
|
62
|
+
trilogy/core/processing/nodes/merge_node.py,sha256=djFBJ8Sq1_crP0g7REGCbMFl4so7zPzabkPgAA1TUl4,15720
|
|
63
63
|
trilogy/core/processing/nodes/select_node_v2.py,sha256=Xyfq8lU7rP7JTAd8VV0ATDNal64n4xIBgWQsOuMe_Ak,8824
|
|
64
64
|
trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ_LpvPokpZKOhx4,1425
|
|
65
65
|
trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
|
|
@@ -75,7 +75,7 @@ trilogy/dialect/bigquery.py,sha256=PkjFcNGZHYOe655PmJhb8a0afdFULuovqP0qQVO8m0I,2
|
|
|
75
75
|
trilogy/dialect/common.py,sha256=XjHkP8Dqezjkd2JU5xoAlMRS_6HNyXQCF4CykLK3C8o,5011
|
|
76
76
|
trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
|
|
77
77
|
trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
|
|
78
|
-
trilogy/dialect/duckdb.py,sha256=
|
|
78
|
+
trilogy/dialect/duckdb.py,sha256=XTBK4RhE1_wF2_IA_7c2W5ih0uxZx0wZ1mfJ3YFIuso,3768
|
|
79
79
|
trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
|
|
80
80
|
trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
|
|
81
81
|
trilogy/dialect/presto.py,sha256=Mw7_F8h19mWfuZHkHQJizQWbpu1lIHe6t2PA0r88gsY,3392
|
|
@@ -87,13 +87,13 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
|
|
|
87
87
|
trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
|
|
88
88
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
89
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
trilogy/parsing/common.py,sha256=
|
|
90
|
+
trilogy/parsing/common.py,sha256=bhZ_f520D2diRcXC3YeEToPSXBbUBBNq42zII_smdYc,23518
|
|
91
91
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
92
92
|
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
93
93
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
94
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
94
|
+
trilogy/parsing/parse_engine.py,sha256=2BeNdsYoyn5UktTdafEKTZ8H7-ELEahDJeqcwMvPtO8,62567
|
|
95
95
|
trilogy/parsing/render.py,sha256=ElpmCWUhGs8h489S7cdlbI8bilJlnBgHZ8KMR8y1FrM,18840
|
|
96
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
96
|
+
trilogy/parsing/trilogy.lark,sha256=SeuOZrpIUSqQwCj5mTSmTSkeSLTPwBgtm2AdpcGAy9A,13023
|
|
97
97
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
98
|
trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
|
|
99
99
|
trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -102,8 +102,8 @@ trilogy/std/date.preql,sha256=0MHeGLp2mG4QBKtmozcBZ7qVjOAdWOtrliIKn6hz1Pc,95
|
|
|
102
102
|
trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
|
|
103
103
|
trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
|
|
104
104
|
trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
|
|
105
|
-
pytrilogy-0.0.3.
|
|
106
|
-
pytrilogy-0.0.3.
|
|
107
|
-
pytrilogy-0.0.3.
|
|
108
|
-
pytrilogy-0.0.3.
|
|
109
|
-
pytrilogy-0.0.3.
|
|
105
|
+
pytrilogy-0.0.3.33.dist-info/METADATA,sha256=nCv6QIF6oA0nSwlsLa_znFnm6RetCf4zyfBdBszleNY,9100
|
|
106
|
+
pytrilogy-0.0.3.33.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
107
|
+
pytrilogy-0.0.3.33.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
108
|
+
pytrilogy-0.0.3.33.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
109
|
+
pytrilogy-0.0.3.33.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/core/enums.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from trilogy.constants import DEFAULT_NAMESPACE
|
|
2
|
-
from trilogy.core.enums import ConceptSource, FunctionType, Purpose
|
|
2
|
+
from trilogy.core.enums import ConceptSource, DatePart, FunctionType, Purpose
|
|
3
3
|
from trilogy.core.functions import AttrAccess, FunctionFactory
|
|
4
4
|
from trilogy.core.models.author import Concept, Function, Metadata
|
|
5
5
|
from trilogy.core.models.core import DataType, StructType, arg_to_datatype
|
|
@@ -64,6 +64,30 @@ def generate_date_concepts(concept: Concept, environment: Environment):
|
|
|
64
64
|
if new_concept.name in environment.concepts:
|
|
65
65
|
continue
|
|
66
66
|
environment.add_concept(new_concept, add_derived=False)
|
|
67
|
+
for grain in [DatePart.MONTH, DatePart.YEAR]:
|
|
68
|
+
function = factory.create_function(
|
|
69
|
+
operator=FunctionType.DATE_TRUNCATE,
|
|
70
|
+
args=[concept, grain],
|
|
71
|
+
)
|
|
72
|
+
new_concept = Concept(
|
|
73
|
+
name=f"{concept.name}.{grain.value}_start",
|
|
74
|
+
datatype=DataType.DATE,
|
|
75
|
+
purpose=Purpose.PROPERTY,
|
|
76
|
+
lineage=function,
|
|
77
|
+
grain=concept.grain,
|
|
78
|
+
namespace=concept.namespace,
|
|
79
|
+
keys=set(
|
|
80
|
+
[concept.address],
|
|
81
|
+
),
|
|
82
|
+
metadata=Metadata(
|
|
83
|
+
description=f"Auto-derived from {base_description}. The date truncated to the {grain.value}.",
|
|
84
|
+
line_number=base_line_number,
|
|
85
|
+
concept_source=ConceptSource.AUTO_DERIVED,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
if new_concept.name in environment.concepts:
|
|
89
|
+
continue
|
|
90
|
+
environment.add_concept(new_concept, add_derived=False)
|
|
67
91
|
|
|
68
92
|
|
|
69
93
|
def generate_datetime_concepts(concept: Concept, environment: Environment):
|
trilogy/core/models/author.py
CHANGED
|
@@ -1326,7 +1326,7 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
|
|
|
1326
1326
|
|
|
1327
1327
|
class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
1328
1328
|
type: WindowType
|
|
1329
|
-
content:
|
|
1329
|
+
content: FuncArgs
|
|
1330
1330
|
order_by: List["OrderItem"]
|
|
1331
1331
|
over: List["ConceptRef"] = Field(default_factory=list)
|
|
1332
1332
|
index: Optional[int] = None
|
|
@@ -1335,7 +1335,7 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1335
1335
|
return self.__repr__()
|
|
1336
1336
|
|
|
1337
1337
|
def __repr__(self):
|
|
1338
|
-
return f"{self.type}
|
|
1338
|
+
return f"{self.type.value} {self.content} by {self.index} over {self.over} order {self.order_by}"
|
|
1339
1339
|
|
|
1340
1340
|
@field_validator("content", mode="before")
|
|
1341
1341
|
def enforce_concept_ref(cls, v):
|
|
@@ -1358,7 +1358,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1358
1358
|
) -> "WindowItem":
|
|
1359
1359
|
output = WindowItem.model_construct(
|
|
1360
1360
|
type=self.type,
|
|
1361
|
-
content=
|
|
1361
|
+
content=(
|
|
1362
|
+
self.content.with_merge(source, target, modifiers)
|
|
1363
|
+
if isinstance(self.content, Mergeable)
|
|
1364
|
+
else self.content
|
|
1365
|
+
),
|
|
1362
1366
|
over=[x.with_merge(source, target, modifiers) for x in self.over],
|
|
1363
1367
|
order_by=[x.with_merge(source, target, modifiers) for x in self.order_by],
|
|
1364
1368
|
index=self.index,
|
|
@@ -1379,7 +1383,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1379
1383
|
def with_namespace(self, namespace: str) -> "WindowItem":
|
|
1380
1384
|
return WindowItem.model_construct(
|
|
1381
1385
|
type=self.type,
|
|
1382
|
-
content=
|
|
1386
|
+
content=(
|
|
1387
|
+
self.content.with_namespace(namespace)
|
|
1388
|
+
if isinstance(self.content, Namespaced)
|
|
1389
|
+
else self.content
|
|
1390
|
+
),
|
|
1383
1391
|
over=[x.with_namespace(namespace) for x in self.over],
|
|
1384
1392
|
order_by=[x.with_namespace(namespace) for x in self.order_by],
|
|
1385
1393
|
index=self.index,
|
|
@@ -1387,7 +1395,8 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1387
1395
|
|
|
1388
1396
|
@property
|
|
1389
1397
|
def concept_arguments(self) -> List[ConceptRef]:
|
|
1390
|
-
output = [
|
|
1398
|
+
output = []
|
|
1399
|
+
output += get_concept_arguments(self.content)
|
|
1391
1400
|
for order in self.order_by:
|
|
1392
1401
|
output += get_concept_arguments(order)
|
|
1393
1402
|
for item in self.over:
|
|
@@ -1658,8 +1667,13 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1658
1667
|
# attempt to exit early to avoid checking all types
|
|
1659
1668
|
break
|
|
1660
1669
|
elif isinstance(arg, ptype):
|
|
1670
|
+
if isinstance(arg, str) and DataType.DATE_PART in valid_inputs[idx]:
|
|
1671
|
+
if arg not in [x.value for x in DatePart]:
|
|
1672
|
+
pass
|
|
1673
|
+
else:
|
|
1674
|
+
break
|
|
1661
1675
|
raise TypeError(
|
|
1662
|
-
f
|
|
1676
|
+
f'Invalid {dtype} constant passed into {operator_name} "{arg}", expecting one of {valid_inputs[idx]}'
|
|
1663
1677
|
)
|
|
1664
1678
|
return v
|
|
1665
1679
|
|
|
@@ -1782,6 +1796,10 @@ class FunctionCallWrapper(
|
|
|
1782
1796
|
],
|
|
1783
1797
|
)
|
|
1784
1798
|
|
|
1799
|
+
def with_reference_replacement(self, source, target):
|
|
1800
|
+
raise NotImplementedError("Cannot reference replace")
|
|
1801
|
+
return self
|
|
1802
|
+
|
|
1785
1803
|
def with_merge(
|
|
1786
1804
|
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
1787
1805
|
) -> "FunctionCallWrapper":
|
|
@@ -2362,6 +2380,7 @@ FuncArgs = (
|
|
|
2362
2380
|
| FilterItem
|
|
2363
2381
|
| int
|
|
2364
2382
|
| float
|
|
2383
|
+
| DatePart
|
|
2365
2384
|
| str
|
|
2366
2385
|
| date
|
|
2367
2386
|
| datetime
|
|
@@ -2371,7 +2390,6 @@ FuncArgs = (
|
|
|
2371
2390
|
| ListType
|
|
2372
2391
|
| MapType
|
|
2373
2392
|
| NumericType
|
|
2374
|
-
| DatePart
|
|
2375
2393
|
| list
|
|
2376
2394
|
| ListWrapper[Any]
|
|
2377
2395
|
)
|
trilogy/core/models/build.py
CHANGED
|
@@ -1646,7 +1646,7 @@ class Factory:
|
|
|
1646
1646
|
|
|
1647
1647
|
@build.register
|
|
1648
1648
|
def _(self, base: WindowItem) -> BuildWindowItem:
|
|
1649
|
-
|
|
1649
|
+
# to do proper discovery, we need to inject virtual intermediate ocncepts
|
|
1650
1650
|
return BuildWindowItem.model_construct(
|
|
1651
1651
|
type=base.type,
|
|
1652
1652
|
content=self.build(base.content),
|
|
@@ -250,7 +250,7 @@ class Environment(BaseModel):
|
|
|
250
250
|
imports=dict(self.imports),
|
|
251
251
|
namespace=self.namespace,
|
|
252
252
|
working_path=self.working_path,
|
|
253
|
-
environment_config=self.config,
|
|
253
|
+
environment_config=self.config.model_copy(deep=True),
|
|
254
254
|
version=self.version,
|
|
255
255
|
cte_name_map=dict(self.cte_name_map),
|
|
256
256
|
materialized_concepts=set(self.materialized_concepts),
|
trilogy/core/models/execute.py
CHANGED
|
@@ -637,7 +637,7 @@ class QueryDatasource(BaseModel):
|
|
|
637
637
|
and CONFIG.validate_missing
|
|
638
638
|
):
|
|
639
639
|
raise SyntaxError(
|
|
640
|
-
f"On query datasource missing source map for {concept.address} on {key}, have {v}"
|
|
640
|
+
f"On query datasource missing source map for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
|
|
641
641
|
)
|
|
642
642
|
return v
|
|
643
643
|
|
trilogy/core/optimization.py
CHANGED
|
@@ -94,6 +94,11 @@ def filter_irrelevant_ctes(
|
|
|
94
94
|
inverse_map = gen_inverse_map(input)
|
|
95
95
|
recurse(root_cte, inverse_map)
|
|
96
96
|
final = [cte for cte in input if cte.name in relevant_ctes]
|
|
97
|
+
filtered = [cte for cte in input if cte.name not in relevant_ctes]
|
|
98
|
+
if filtered:
|
|
99
|
+
logger.info(
|
|
100
|
+
f"[Optimization][Irrelevent CTE filtering] Removing redundant CTEs {[x.name for x in filtered]}"
|
|
101
|
+
)
|
|
97
102
|
if len(final) == len(input):
|
|
98
103
|
return input
|
|
99
104
|
return filter_irrelevant_ctes(final, root_cte)
|
|
@@ -130,6 +135,9 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
|
|
|
130
135
|
if not output_addresses.issubset(parent_output_addresses):
|
|
131
136
|
return None
|
|
132
137
|
if not direct_parent.grain == cte.grain:
|
|
138
|
+
logger.info("grain mismatch, cannot early exit")
|
|
139
|
+
logger.info(direct_parent.grain)
|
|
140
|
+
logger.info(cte.grain)
|
|
133
141
|
return None
|
|
134
142
|
|
|
135
143
|
assert isinstance(cte, CTE)
|
|
@@ -625,6 +625,7 @@ def validate_stack(
|
|
|
625
625
|
for concept in resolved.output_concepts:
|
|
626
626
|
if concept.address in resolved.hidden_concepts:
|
|
627
627
|
continue
|
|
628
|
+
|
|
628
629
|
validate_concept(
|
|
629
630
|
concept,
|
|
630
631
|
node,
|
|
@@ -773,6 +774,8 @@ def _search_concepts(
|
|
|
773
774
|
) -> StrategyNode | None:
|
|
774
775
|
# these are the concepts we need in the output projection
|
|
775
776
|
mandatory_list = unique(mandatory_list, "address")
|
|
777
|
+
# cache our values before an filter injection
|
|
778
|
+
original_mandatory = [*mandatory_list]
|
|
776
779
|
for x in mandatory_list:
|
|
777
780
|
if isinstance(x, UndefinedConcept):
|
|
778
781
|
raise SyntaxError(f"Undefined concept {x.address}")
|
|
@@ -921,7 +924,7 @@ def _search_concepts(
|
|
|
921
924
|
mandatory_completion = [c.address for c in completion_mandatory]
|
|
922
925
|
logger.info(
|
|
923
926
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} finished concept loop for {priority_concept} {priority_concept.derivation} condition {conditions} flag for accepting partial addresses is"
|
|
924
|
-
f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
|
|
927
|
+
f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} synonyms partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
|
|
925
928
|
)
|
|
926
929
|
if complete == ValidationResult.INCOMPLETE_CONDITION:
|
|
927
930
|
cond_dict = {str(node): node.preexisting_conditions for node in stack}
|
|
@@ -948,6 +951,11 @@ def _search_concepts(
|
|
|
948
951
|
if complete == ValidationResult.COMPLETE:
|
|
949
952
|
condition_required = True
|
|
950
953
|
non_virtual = [c for c in completion_mandatory if c.address not in virtual]
|
|
954
|
+
non_virtual_output = [c for c in original_mandatory if c.address not in virtual]
|
|
955
|
+
non_virtual_different = len(completion_mandatory) != len(original_mandatory)
|
|
956
|
+
non_virtual_difference_values = set(
|
|
957
|
+
[x.address for x in completion_mandatory]
|
|
958
|
+
).difference(set([x.address for x in original_mandatory]))
|
|
951
959
|
if not conditions:
|
|
952
960
|
condition_required = False
|
|
953
961
|
non_virtual = [c for c in mandatory_list if c.address not in virtual]
|
|
@@ -966,7 +974,19 @@ def _search_concepts(
|
|
|
966
974
|
)
|
|
967
975
|
if len(stack) == 1:
|
|
968
976
|
output: StrategyNode = stack[0]
|
|
969
|
-
|
|
977
|
+
if non_virtual_different:
|
|
978
|
+
logger.info(
|
|
979
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values"
|
|
980
|
+
)
|
|
981
|
+
output.set_output_concepts(
|
|
982
|
+
[
|
|
983
|
+
x
|
|
984
|
+
for x in output.output_concepts
|
|
985
|
+
if x.address in non_virtual_output
|
|
986
|
+
],
|
|
987
|
+
rebuild=False,
|
|
988
|
+
)
|
|
989
|
+
|
|
970
990
|
logger.info(
|
|
971
991
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)}"
|
|
972
992
|
)
|
|
@@ -995,6 +1015,30 @@ def _search_concepts(
|
|
|
995
1015
|
logger.info(
|
|
996
1016
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
|
|
997
1017
|
)
|
|
1018
|
+
if condition_required and conditions and non_virtual_different:
|
|
1019
|
+
logger.info(
|
|
1020
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Conditions {conditions} were injected, checking if we need a group to restore grain"
|
|
1021
|
+
)
|
|
1022
|
+
result = GroupNode.check_if_required(
|
|
1023
|
+
downstream_concepts=original_mandatory,
|
|
1024
|
+
parents=[output.resolve()],
|
|
1025
|
+
environment=environment,
|
|
1026
|
+
depth=depth,
|
|
1027
|
+
)
|
|
1028
|
+
logger.info(f"gcheck result is {result}")
|
|
1029
|
+
if result.required:
|
|
1030
|
+
logger.info(
|
|
1031
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Adding group node"
|
|
1032
|
+
)
|
|
1033
|
+
return GroupNode(
|
|
1034
|
+
output_concepts=original_mandatory,
|
|
1035
|
+
input_concepts=original_mandatory,
|
|
1036
|
+
environment=environment,
|
|
1037
|
+
parents=[output],
|
|
1038
|
+
partial_concepts=output.partial_concepts,
|
|
1039
|
+
preexisting_conditions=conditions.conditional,
|
|
1040
|
+
depth=depth,
|
|
1041
|
+
)
|
|
998
1042
|
return output
|
|
999
1043
|
|
|
1000
1044
|
# if we can't find it after expanding to a merge, then
|
|
@@ -149,7 +149,9 @@ def gen_group_node(
|
|
|
149
149
|
g=g,
|
|
150
150
|
depth=depth,
|
|
151
151
|
source_concepts=source_concepts,
|
|
152
|
-
log_lambda=create_log_lambda(
|
|
152
|
+
log_lambda=create_log_lambda(
|
|
153
|
+
LOGGER_PREFIX + f" for {concept.address}", depth, logger
|
|
154
|
+
),
|
|
153
155
|
history=history,
|
|
154
156
|
conditions=conditions,
|
|
155
157
|
)
|
|
@@ -117,7 +117,11 @@ def gen_rowset_node(
|
|
|
117
117
|
f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]}"
|
|
118
118
|
)
|
|
119
119
|
if not local_optional or all(
|
|
120
|
-
|
|
120
|
+
(
|
|
121
|
+
x.address in node.output_concepts
|
|
122
|
+
or (z in x.pseudonyms for z in node.output_concepts)
|
|
123
|
+
)
|
|
124
|
+
and x.address not in node.partial_concepts
|
|
121
125
|
for x in local_optional
|
|
122
126
|
):
|
|
123
127
|
logger.info(
|
|
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
4
|
from trilogy.constants import logger
|
|
5
|
-
from trilogy.core.enums import SourceType
|
|
5
|
+
from trilogy.core.enums import Purpose, SourceType
|
|
6
6
|
from trilogy.core.models.build import (
|
|
7
7
|
BuildComparison,
|
|
8
8
|
BuildConcept,
|
|
@@ -78,7 +78,9 @@ class GroupNode(StrategyNode):
|
|
|
78
78
|
downstream_concepts: List[BuildConcept],
|
|
79
79
|
parents: list[QueryDatasource | BuildDatasource],
|
|
80
80
|
environment: BuildEnvironment,
|
|
81
|
+
depth: int = 0,
|
|
81
82
|
) -> GroupRequiredResponse:
|
|
83
|
+
padding = "\t" * depth
|
|
82
84
|
target_grain = BuildGrain.from_concepts(
|
|
83
85
|
downstream_concepts,
|
|
84
86
|
environment=environment,
|
|
@@ -95,12 +97,67 @@ class GroupNode(StrategyNode):
|
|
|
95
97
|
lookups: list[BuildConcept | str] = [
|
|
96
98
|
concept_map[x] if x in concept_map else x for x in comp_grain.components
|
|
97
99
|
]
|
|
100
|
+
|
|
98
101
|
comp_grain = BuildGrain.from_concepts(lookups, environment=environment)
|
|
102
|
+
|
|
99
103
|
# dynamically select if we need to group
|
|
100
104
|
# because sometimes, we are already at required grain
|
|
101
105
|
if comp_grain.issubset(target_grain):
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, is subset, no grain required"
|
|
109
|
+
)
|
|
102
110
|
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
111
|
+
# find out what extra is in the comp grain vs target grain
|
|
112
|
+
difference = [
|
|
113
|
+
environment.concepts[c] for c in (comp_grain - target_grain).components
|
|
114
|
+
]
|
|
115
|
+
logger.info(
|
|
116
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, difference {difference}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# if the difference is all unique properties whose keys are in the source grain
|
|
120
|
+
# we can also suppress the group
|
|
121
|
+
if all(
|
|
122
|
+
[
|
|
123
|
+
x.purpose == Purpose.UNIQUE_PROPERTY
|
|
124
|
+
and x.keys
|
|
125
|
+
and all(z in comp_grain.components for z in x.keys)
|
|
126
|
+
for x in difference
|
|
127
|
+
]
|
|
128
|
+
):
|
|
129
|
+
logger.info(
|
|
130
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
|
|
131
|
+
)
|
|
132
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
133
|
+
if all([x.purpose == Purpose.KEY for x in difference]):
|
|
134
|
+
logger.info(
|
|
135
|
+
f"{padding}{LOGGER_PREFIX} checking if downstream is unique properties of key"
|
|
136
|
+
)
|
|
137
|
+
replaced_grain_raw: list[set[str]] = [
|
|
138
|
+
(
|
|
139
|
+
x.keys or set()
|
|
140
|
+
if x.purpose == Purpose.UNIQUE_PROPERTY
|
|
141
|
+
else set([x.address])
|
|
142
|
+
)
|
|
143
|
+
for x in downstream_concepts
|
|
144
|
+
if x.address in target_grain.components
|
|
145
|
+
]
|
|
146
|
+
# flatten the list of lists
|
|
147
|
+
replaced_grain = [
|
|
148
|
+
item for sublist in replaced_grain_raw for item in sublist
|
|
149
|
+
]
|
|
150
|
+
# if the replaced grain is a subset of the comp grain, we can skip the group
|
|
151
|
+
unique_grain_comp = BuildGrain.from_concepts(
|
|
152
|
+
replaced_grain, environment=environment
|
|
153
|
+
)
|
|
154
|
+
if comp_grain.issubset(unique_grain_comp):
|
|
155
|
+
logger.info(
|
|
156
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
|
|
157
|
+
)
|
|
158
|
+
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
103
159
|
|
|
160
|
+
logger.info(f"{padding}{LOGGER_PREFIX} Group requirement check: group required")
|
|
104
161
|
return GroupRequiredResponse(target_grain, comp_grain, True)
|
|
105
162
|
|
|
106
163
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -296,9 +296,21 @@ class MergeNode(StrategyNode):
|
|
|
296
296
|
return dataset
|
|
297
297
|
|
|
298
298
|
pregrain = BuildGrain()
|
|
299
|
+
|
|
299
300
|
for source in final_datasets:
|
|
301
|
+
if all(
|
|
302
|
+
[x.address in self.existence_concepts for x in source.output_concepts]
|
|
303
|
+
):
|
|
304
|
+
logger.info(
|
|
305
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} skipping existence only source with {source.output_concepts} from grain accumulation"
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
300
308
|
pregrain += source.grain
|
|
301
309
|
|
|
310
|
+
pregrain = BuildGrain.from_concepts(
|
|
311
|
+
pregrain.components, environment=self.environment
|
|
312
|
+
)
|
|
313
|
+
|
|
302
314
|
grain = self.grain if self.grain else pregrain
|
|
303
315
|
logger.info(
|
|
304
316
|
f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
|
|
@@ -310,6 +322,7 @@ class MergeNode(StrategyNode):
|
|
|
310
322
|
)
|
|
311
323
|
else:
|
|
312
324
|
joins = []
|
|
325
|
+
|
|
313
326
|
logger.info(
|
|
314
327
|
f"{self.logging_prefix}{LOGGER_PREFIX} Final join count for CTE parent count {len(join_candidates)} is {len(joins)}"
|
|
315
328
|
)
|
|
@@ -343,7 +356,6 @@ class MergeNode(StrategyNode):
|
|
|
343
356
|
nullable_concepts = find_nullable_concepts(
|
|
344
357
|
source_map=source_map, joins=joins, datasources=final_datasets
|
|
345
358
|
)
|
|
346
|
-
|
|
347
359
|
qds = QueryDatasource(
|
|
348
360
|
input_concepts=unique(self.input_concepts, "address"),
|
|
349
361
|
output_concepts=unique(self.output_concepts, "address"),
|
trilogy/dialect/duckdb.py
CHANGED
|
@@ -27,6 +27,7 @@ FUNCTION_MAP = {
|
|
|
27
27
|
FunctionType.INDEX_ACCESS: lambda args: (f"{args[0]}[{args[1]}]"),
|
|
28
28
|
# datetime is aliased
|
|
29
29
|
FunctionType.CURRENT_DATETIME: lambda x: "cast(get_current_timestamp() as datetime)",
|
|
30
|
+
FunctionType.DATE: lambda x: f"cast({x[0]} as date)",
|
|
30
31
|
FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc('{x[1]}', {x[0]})",
|
|
31
32
|
FunctionType.DATE_ADD: lambda x: f"date_add({x[0]}, {x[2]} * INTERVAL 1 {x[1]})",
|
|
32
33
|
FunctionType.DATE_PART: lambda x: f"date_part('{x[1]}', {x[0]})",
|
trilogy/parsing/common.py
CHANGED
|
@@ -40,6 +40,7 @@ from trilogy.core.models.author import (
|
|
|
40
40
|
UndefinedConcept,
|
|
41
41
|
WhereClause,
|
|
42
42
|
WindowItem,
|
|
43
|
+
address_with_namespace,
|
|
43
44
|
)
|
|
44
45
|
from trilogy.core.models.core import DataType, arg_to_datatype
|
|
45
46
|
from trilogy.core.models.environment import Environment
|
|
@@ -255,7 +256,7 @@ def concepts_to_grain_concepts(
|
|
|
255
256
|
return v2
|
|
256
257
|
|
|
257
258
|
|
|
258
|
-
def
|
|
259
|
+
def _get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
|
|
259
260
|
from trilogy.core.models.author import get_concept_arguments
|
|
260
261
|
|
|
261
262
|
is_metric = False
|
|
@@ -270,9 +271,16 @@ def get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
|
|
|
270
271
|
return [], True
|
|
271
272
|
elif isinstance(arg, AggregateWrapper) and arg.by:
|
|
272
273
|
return arg.by, True
|
|
274
|
+
elif isinstance(arg, FunctionCallWrapper):
|
|
275
|
+
return get_relevant_parent_concepts(arg.content)
|
|
273
276
|
return get_concept_arguments(arg), False
|
|
274
277
|
|
|
275
278
|
|
|
279
|
+
def get_relevant_parent_concepts(arg):
|
|
280
|
+
results = _get_relevant_parent_concepts(arg)
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
|
|
276
284
|
def function_to_concept(
|
|
277
285
|
parent: Function,
|
|
278
286
|
name: str,
|
|
@@ -415,10 +423,31 @@ def window_item_to_concept(
|
|
|
415
423
|
metadata: Metadata | None = None,
|
|
416
424
|
) -> Concept:
|
|
417
425
|
fmetadata = metadata or Metadata()
|
|
426
|
+
# if isinstance(
|
|
427
|
+
# parent.content,
|
|
428
|
+
# (
|
|
429
|
+
# AggregateWrapper
|
|
430
|
+
# | FunctionCallWrapper
|
|
431
|
+
# | WindowItem
|
|
432
|
+
# | FilterItem
|
|
433
|
+
# | Function
|
|
434
|
+
# | ListWrapper
|
|
435
|
+
# | MapWrapper
|
|
436
|
+
# ),
|
|
437
|
+
# ):
|
|
438
|
+
# new_parent = arbitrary_to_concept(
|
|
439
|
+
# parent.content, environment=environment, namespace=namespace
|
|
440
|
+
# )
|
|
441
|
+
# environment.add_concept(new_parent)
|
|
442
|
+
# parent = parent.model_copy(update={"content": new_parent.reference})
|
|
443
|
+
|
|
444
|
+
if not isinstance(parent.content, ConceptRef):
|
|
445
|
+
raise NotImplementedError(
|
|
446
|
+
f"Window function wiht non ref content {parent.content} not yet supported"
|
|
447
|
+
)
|
|
418
448
|
bcontent = environment.concepts[parent.content.address]
|
|
419
449
|
if isinstance(bcontent, UndefinedConcept):
|
|
420
450
|
return UndefinedConcept(address=f"{namespace}.{name}", metadata=fmetadata)
|
|
421
|
-
|
|
422
451
|
if bcontent.purpose == Purpose.METRIC:
|
|
423
452
|
local_purpose, keys = get_purpose_and_keys(None, (bcontent,), environment)
|
|
424
453
|
else:
|
|
@@ -534,38 +563,58 @@ def align_item_to_concept(
|
|
|
534
563
|
return new
|
|
535
564
|
|
|
536
565
|
|
|
566
|
+
def rowset_concept(
|
|
567
|
+
orig_address: ConceptRef,
|
|
568
|
+
environment: Environment,
|
|
569
|
+
rowset: RowsetDerivationStatement,
|
|
570
|
+
pre_output: list[Concept],
|
|
571
|
+
orig: dict[str, Concept],
|
|
572
|
+
orig_map: dict[str, Concept],
|
|
573
|
+
):
|
|
574
|
+
orig_concept = environment.concepts[orig_address.address]
|
|
575
|
+
name = orig_concept.name
|
|
576
|
+
if isinstance(orig_concept.lineage, FilterItem):
|
|
577
|
+
if orig_concept.lineage.where == rowset.select.where_clause:
|
|
578
|
+
name = environment.concepts[orig_concept.lineage.content.address].name
|
|
579
|
+
base_namespace = (
|
|
580
|
+
f"{rowset.name}.{orig_concept.namespace}"
|
|
581
|
+
if orig_concept.namespace != rowset.namespace
|
|
582
|
+
else rowset.name
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
new_concept = Concept(
|
|
586
|
+
name=name,
|
|
587
|
+
datatype=orig_concept.datatype,
|
|
588
|
+
purpose=orig_concept.purpose,
|
|
589
|
+
lineage=None,
|
|
590
|
+
grain=orig_concept.grain,
|
|
591
|
+
metadata=Metadata(concept_source=ConceptSource.CTE),
|
|
592
|
+
namespace=base_namespace,
|
|
593
|
+
keys=orig_concept.keys,
|
|
594
|
+
derivation=Derivation.ROWSET,
|
|
595
|
+
granularity=orig_concept.granularity,
|
|
596
|
+
pseudonyms={
|
|
597
|
+
address_with_namespace(x, rowset.name) for x in orig_concept.pseudonyms
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
for x in orig_concept.pseudonyms:
|
|
601
|
+
new_address = address_with_namespace(x, rowset.name)
|
|
602
|
+
origa = environment.alias_origin_lookup[x]
|
|
603
|
+
environment.concepts[new_address] = new_concept
|
|
604
|
+
environment.alias_origin_lookup[new_address] = origa.model_copy(
|
|
605
|
+
update={"namespace": f"{rowset.name}.{origa.namespace}"}
|
|
606
|
+
)
|
|
607
|
+
orig[orig_concept.address] = new_concept
|
|
608
|
+
orig_map[new_concept.address] = orig_concept
|
|
609
|
+
pre_output.append(new_concept)
|
|
610
|
+
|
|
611
|
+
|
|
537
612
|
def rowset_to_concepts(rowset: RowsetDerivationStatement, environment: Environment):
|
|
538
613
|
pre_output: list[Concept] = []
|
|
539
614
|
orig: dict[str, Concept] = {}
|
|
540
615
|
orig_map: dict[str, Concept] = {}
|
|
541
616
|
for orig_address in rowset.select.output_components:
|
|
542
|
-
|
|
543
|
-
name = orig_concept.name
|
|
544
|
-
if isinstance(orig_concept.lineage, FilterItem):
|
|
545
|
-
if orig_concept.lineage.where == rowset.select.where_clause:
|
|
546
|
-
name = environment.concepts[orig_concept.lineage.content.address].name
|
|
547
|
-
base_namespace = (
|
|
548
|
-
f"{rowset.name}.{orig_concept.namespace}"
|
|
549
|
-
if orig_concept.namespace != rowset.namespace
|
|
550
|
-
else rowset.name
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
new_concept = Concept(
|
|
554
|
-
name=name,
|
|
555
|
-
datatype=orig_concept.datatype,
|
|
556
|
-
purpose=orig_concept.purpose,
|
|
557
|
-
lineage=None,
|
|
558
|
-
grain=orig_concept.grain,
|
|
559
|
-
# TODO: add proper metadata
|
|
560
|
-
metadata=Metadata(concept_source=ConceptSource.CTE),
|
|
561
|
-
namespace=base_namespace,
|
|
562
|
-
keys=orig_concept.keys,
|
|
563
|
-
derivation=Derivation.ROWSET,
|
|
564
|
-
granularity=orig_concept.granularity,
|
|
565
|
-
)
|
|
566
|
-
orig[orig_concept.address] = new_concept
|
|
567
|
-
orig_map[new_concept.address] = orig_concept
|
|
568
|
-
pre_output.append(new_concept)
|
|
617
|
+
rowset_concept(orig_address, environment, rowset, pre_output, orig, orig_map)
|
|
569
618
|
select_lineage = rowset.select.as_lineage(environment)
|
|
570
619
|
for x in pre_output:
|
|
571
620
|
x.lineage = RowsetItem(
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -252,6 +252,8 @@ def rehydrate_lineage(
|
|
|
252
252
|
)
|
|
253
253
|
return lineage
|
|
254
254
|
elif isinstance(lineage, WindowItem):
|
|
255
|
+
# this is temporarily guaranteed until we do some upstream work
|
|
256
|
+
assert isinstance(lineage.content, ConceptRef)
|
|
255
257
|
lineage.content.datatype = environment.concepts[
|
|
256
258
|
lineage.content.address
|
|
257
259
|
].datatype
|
|
@@ -542,6 +544,10 @@ class ParseToObjects(Transformer):
|
|
|
542
544
|
|
|
543
545
|
@v_args(meta=True)
|
|
544
546
|
def concept_property_declaration(self, meta: Meta, args) -> Concept:
|
|
547
|
+
unique = False
|
|
548
|
+
if not args[0] == Purpose.PROPERTY:
|
|
549
|
+
unique = True
|
|
550
|
+
args = args[1:]
|
|
545
551
|
metadata = Metadata()
|
|
546
552
|
modifiers = []
|
|
547
553
|
for arg in args:
|
|
@@ -569,7 +575,7 @@ class ParseToObjects(Transformer):
|
|
|
569
575
|
concept = Concept(
|
|
570
576
|
name=name,
|
|
571
577
|
datatype=args[2],
|
|
572
|
-
purpose=
|
|
578
|
+
purpose=Purpose.PROPERTY if not unique else Purpose.UNIQUE_PROPERTY,
|
|
573
579
|
metadata=metadata,
|
|
574
580
|
grain=Grain(components={x.address for x in parents}),
|
|
575
581
|
namespace=namespace,
|
|
@@ -633,7 +639,8 @@ class ParseToObjects(Transformer):
|
|
|
633
639
|
source_value = source_value.content
|
|
634
640
|
|
|
635
641
|
if isinstance(
|
|
636
|
-
source_value,
|
|
642
|
+
source_value,
|
|
643
|
+
(FilterItem, WindowItem, AggregateWrapper, Function, FunctionCallWrapper),
|
|
637
644
|
):
|
|
638
645
|
concept = arbitrary_to_concept(
|
|
639
646
|
source_value,
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
concept_declaration: PURPOSE IDENTIFIER data_type concept_nullable_modifier? metadata?
|
|
22
22
|
//customer_id.property first_name STRING;
|
|
23
23
|
//<customer_id,country>.property local_alias STRING
|
|
24
|
-
|
|
24
|
+
UNIQUE: "UNIQUE"i
|
|
25
|
+
concept_property_declaration: UNIQUE? PROPERTY (prop_ident | IDENTIFIER) data_type concept_nullable_modifier? metadata?
|
|
25
26
|
//metric post_length <- len(post_text);
|
|
26
27
|
concept_derivation: (PURPOSE | AUTO | PROPERTY ) (prop_ident | IDENTIFIER) "<-" expr
|
|
27
28
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|