pytrilogy 0.0.2.26__py3-none-any.whl → 0.0.2.28__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.26.dist-info → pytrilogy-0.0.2.28.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.26.dist-info → pytrilogy-0.0.2.28.dist-info}/RECORD +21 -21
- {pytrilogy-0.0.2.26.dist-info → pytrilogy-0.0.2.28.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/core/graph_models.py +2 -2
- trilogy/core/models.py +146 -92
- trilogy/core/optimizations/inline_datasource.py +4 -4
- trilogy/core/processing/node_generators/select_merge_node.py +7 -1
- trilogy/core/processing/nodes/base_node.py +3 -0
- trilogy/core/processing/nodes/merge_node.py +10 -10
- trilogy/core/processing/nodes/select_node_v2.py +6 -2
- trilogy/core/processing/utility.py +6 -9
- trilogy/core/query_processor.py +21 -17
- trilogy/executor.py +17 -1
- trilogy/hooks/query_debugger.py +5 -1
- trilogy/parsing/parse_engine.py +17 -14
- trilogy/parsing/render.py +25 -7
- trilogy/parsing/trilogy.lark +4 -2
- {pytrilogy-0.0.2.26.dist-info → pytrilogy-0.0.2.28.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.26.dist-info → pytrilogy-0.0.2.28.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.26.dist-info → pytrilogy-0.0.2.28.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
trilogy/__init__.py,sha256=Oj4rApJpgEd7VNBVDA5Sy1tHnI0ISyXQcfpiH-fv6UY,291
|
|
2
2
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
trilogy/constants.py,sha256=KiyYnctoZen4Hzv8WG2jeN-IE-dfQbWHdVCUeTZYjBg,1270
|
|
4
4
|
trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
|
|
5
|
-
trilogy/executor.py,sha256=
|
|
5
|
+
trilogy/executor.py,sha256=VcZ2U3RUU2al_VJ75AKVwmCJQLltYouxlgTjq4oxPB0,12577
|
|
6
6
|
trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
|
|
7
7
|
trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
|
|
@@ -14,20 +14,20 @@ trilogy/core/environment_helpers.py,sha256=DIsoo-GcXmXVPB1JbNh8Oku25Nyef9mexPIdy
|
|
|
14
14
|
trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
|
|
15
15
|
trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
|
|
16
16
|
trilogy/core/functions.py,sha256=IhVpt3n6wEanKHnGu3oA2w6-hKIlxWpEyz7fHN66mpo,10720
|
|
17
|
-
trilogy/core/graph_models.py,sha256=
|
|
17
|
+
trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
|
|
18
18
|
trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
|
|
19
|
-
trilogy/core/models.py,sha256=
|
|
19
|
+
trilogy/core/models.py,sha256=W_0ZfIIEuyHfYsSXGMJOJPNJf7vSljSrRm42nLyiL8w,159702
|
|
20
20
|
trilogy/core/optimization.py,sha256=od_60A9F8J8Nj24MHgrxl4vwRwmBFH13TMdoMQvgVKs,7717
|
|
21
|
-
trilogy/core/query_processor.py,sha256
|
|
21
|
+
trilogy/core/query_processor.py,sha256=mbcZlgjChrRjDHkdmMbKe-T70UpbBkJhS09MyU5a6UY,17785
|
|
22
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
|
-
trilogy/core/optimizations/inline_datasource.py,sha256=
|
|
25
|
+
trilogy/core/optimizations/inline_datasource.py,sha256=NqUOVl0pOXF1R_roELVW8I0qN7or2wPtAsRmDD9QJso,3658
|
|
26
26
|
trilogy/core/optimizations/predicate_pushdown.py,sha256=1l9WnFOSv79e341typG3tTdk0XGl1J_ToQih3LYoGIY,8435
|
|
27
27
|
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
28
|
trilogy/core/processing/concept_strategies_v3.py,sha256=7MT_x6QFHrbSDmjz21pYdQB5ux419ES4QS-8lO16eyw,36091
|
|
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=xlaKqnoWg-mEwTF-erBF9QXnXZtESrTuYrK2RQb7Wi4,17411
|
|
31
31
|
trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
|
|
32
32
|
trilogy/core/processing/node_generators/basic_node.py,sha256=WQNgJ1MwrMS_BQ-b3XwGGB6eToDykelAVj_fesJuqe0,2069
|
|
33
33
|
trilogy/core/processing/node_generators/common.py,sha256=eslHTTPFTkmwHwKIuUsbFn54jxj-Avtt-QScqtNwzdg,8945
|
|
@@ -37,16 +37,16 @@ trilogy/core/processing/node_generators/group_to_node.py,sha256=R9i_wHipxjXJyfYE
|
|
|
37
37
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=_KO9lqzHQoy4VAviO0ttQlmK0tjaqrJj4SJPhmoIYm8,6229
|
|
38
38
|
trilogy/core/processing/node_generators/node_merge_node.py,sha256=dIEv5P2MTViAES2MzqJgccYzM3HldjHrQYFwH00cqyc,14003
|
|
39
39
|
trilogy/core/processing/node_generators/rowset_node.py,sha256=KtdN6t2xM8CJxobc4aQX4W8uX98U6IabeuBF_FtBLR4,4583
|
|
40
|
-
trilogy/core/processing/node_generators/select_merge_node.py,sha256=
|
|
40
|
+
trilogy/core/processing/node_generators/select_merge_node.py,sha256=vE7GoPu2_okO3jS96oA5O3jFsrkiSqIvIP5WkyfFil0,11596
|
|
41
41
|
trilogy/core/processing/node_generators/select_node.py,sha256=nwXHQF6C-aQUIelx9dyxN2pK3muL-4-6RIqnqQqNwtw,1808
|
|
42
42
|
trilogy/core/processing/node_generators/unnest_node.py,sha256=cZ26CN338CBnd6asML1OBUtNcDzmNlFpY0Vnade4yrc,2256
|
|
43
43
|
trilogy/core/processing/node_generators/window_node.py,sha256=jy3FF8uN0VA7yyrBeR40B9CAqR_5qBP4PiS6Gr-f-7w,2590
|
|
44
44
|
trilogy/core/processing/nodes/__init__.py,sha256=qS5EJDRwwIrCEfS7ibCA2ESE0RPzsAIii1UWd_wNsHA,4760
|
|
45
|
-
trilogy/core/processing/nodes/base_node.py,sha256=
|
|
45
|
+
trilogy/core/processing/nodes/base_node.py,sha256=8nEG3OPE_LzFXI48-Y6FS8MyO79LY0Sm0EqYz31WJ1Q,15719
|
|
46
46
|
trilogy/core/processing/nodes/filter_node.py,sha256=GfZ9eghpFDI-s7iQP2UqTljCmn25LT_T5TAxDlh7PkQ,2343
|
|
47
47
|
trilogy/core/processing/nodes/group_node.py,sha256=PrBHaGq_f8RmokUw9lXLGJ5YbjdP77P7Ag0pgR6e2cU,7293
|
|
48
|
-
trilogy/core/processing/nodes/merge_node.py,sha256=
|
|
49
|
-
trilogy/core/processing/nodes/select_node_v2.py,sha256=
|
|
48
|
+
trilogy/core/processing/nodes/merge_node.py,sha256=W3eCjmJbs8Wfw7Y5AgIY2pP-ntPCrrMe11UG-QGJvA8,14835
|
|
49
|
+
trilogy/core/processing/nodes/select_node_v2.py,sha256=k5WvqmOkLwnP9SFSF5z33a1SFo4nZ-y9ODLi-P05YkI,8281
|
|
50
50
|
trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
|
|
51
51
|
trilogy/core/processing/nodes/window_node.py,sha256=ro0QfMFi4ZmIn5Q4D0M_vJWfnHH_C0MN7XkVkx8Gygg,1214
|
|
52
52
|
trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -63,21 +63,21 @@ trilogy/dialect/sql_server.py,sha256=owUZbMFrooYIMj1DSLstPWxPO7K7WAUEWNvDKM-DMt0
|
|
|
63
63
|
trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
64
|
trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
|
|
65
65
|
trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2467
|
|
66
|
-
trilogy/hooks/query_debugger.py,sha256=
|
|
66
|
+
trilogy/hooks/query_debugger.py,sha256=787umJjdGA057DCC714dqFstzJRUbwmz3MNr66IdpQI,4404
|
|
67
67
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
68
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
69
69
|
trilogy/parsing/common.py,sha256=t7yiL_3f6rz_rouF9et84v5orAgs-EprV4V9ghQ6ql4,10024
|
|
70
70
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
71
71
|
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
72
72
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
73
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
74
|
-
trilogy/parsing/render.py,sha256=
|
|
75
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
73
|
+
trilogy/parsing/parse_engine.py,sha256=JwG98fotPpvh5VC-CcHknCTFid9-Zj1Wfo8CyPOnJzs,64431
|
|
74
|
+
trilogy/parsing/render.py,sha256=B9J2GrYQcE76kddMQSeAmvAPX-9pv39mpeSHZ10SNj8,14655
|
|
75
|
+
trilogy/parsing/trilogy.lark,sha256=_z5px2N-e8oLUf7SpPMXXNqbAykDkZOvP4_lPgf5-Uk,12245
|
|
76
76
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
77
77
|
trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
|
|
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.
|
|
83
|
-
pytrilogy-0.0.2.
|
|
78
|
+
pytrilogy-0.0.2.28.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
79
|
+
pytrilogy-0.0.2.28.dist-info/METADATA,sha256=Ftsu-RyQ2c7b05KV4JZm7J9f1DEMup2xjMfLUA-PfWQ,8403
|
|
80
|
+
pytrilogy-0.0.2.28.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
81
|
+
pytrilogy-0.0.2.28.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
|
|
82
|
+
pytrilogy-0.0.2.28.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
83
|
+
pytrilogy-0.0.2.28.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/core/graph_models.py
CHANGED
|
@@ -6,7 +6,7 @@ from trilogy.core.models import Concept, Datasource
|
|
|
6
6
|
def concept_to_node(input: Concept) -> str:
|
|
7
7
|
# if input.purpose == Purpose.METRIC:
|
|
8
8
|
# return f"c~{input.namespace}.{input.name}@{input.grain}"
|
|
9
|
-
return f"c~{input.
|
|
9
|
+
return f"c~{input.address}@{input.grain}"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def datasource_to_node(input: Datasource) -> str:
|
|
@@ -14,7 +14,7 @@ def datasource_to_node(input: Datasource) -> str:
|
|
|
14
14
|
# return "ds~join~" + ",".join(
|
|
15
15
|
# [datasource_to_node(sub) for sub in input.datasources]
|
|
16
16
|
# )
|
|
17
|
-
return f"ds~{input.
|
|
17
|
+
return f"ds~{input.identifier}"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class ReferenceGraph(nx.DiGraph):
|
trilogy/core/models.py
CHANGED
|
@@ -606,6 +606,8 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
606
606
|
return self.grain.components_copy if self.grain else []
|
|
607
607
|
|
|
608
608
|
def with_namespace(self, namespace: str) -> "Concept":
|
|
609
|
+
if namespace == self.namespace:
|
|
610
|
+
return self
|
|
609
611
|
return self.__class__(
|
|
610
612
|
name=self.name,
|
|
611
613
|
datatype=self.datatype,
|
|
@@ -1719,7 +1721,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1719
1721
|
def to_datasource(
|
|
1720
1722
|
self,
|
|
1721
1723
|
namespace: str,
|
|
1722
|
-
|
|
1724
|
+
name: str,
|
|
1723
1725
|
address: Address,
|
|
1724
1726
|
grain: Grain | None = None,
|
|
1725
1727
|
) -> Datasource:
|
|
@@ -1753,7 +1755,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1753
1755
|
condition = self.having_clause.conditional
|
|
1754
1756
|
|
|
1755
1757
|
new_datasource = Datasource(
|
|
1756
|
-
|
|
1758
|
+
name=name,
|
|
1757
1759
|
address=address,
|
|
1758
1760
|
grain=grain or self.grain,
|
|
1759
1761
|
columns=columns,
|
|
@@ -2059,7 +2061,7 @@ class MergeStatementV2(HasUUID, Namespaced, BaseModel):
|
|
|
2059
2061
|
|
|
2060
2062
|
|
|
2061
2063
|
class Datasource(HasUUID, Namespaced, BaseModel):
|
|
2062
|
-
|
|
2064
|
+
name: str
|
|
2063
2065
|
columns: List[ColumnAssignment]
|
|
2064
2066
|
address: Union[Address, str]
|
|
2065
2067
|
grain: Grain = Field(
|
|
@@ -2094,10 +2096,14 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2094
2096
|
self.add_column(target, original[0].alias, modifiers)
|
|
2095
2097
|
|
|
2096
2098
|
@property
|
|
2097
|
-
def
|
|
2099
|
+
def identifier(self) -> str:
|
|
2098
2100
|
if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
|
|
2099
|
-
return self.
|
|
2100
|
-
return f"{self.namespace}.{self.
|
|
2101
|
+
return self.name
|
|
2102
|
+
return f"{self.namespace}.{self.name}"
|
|
2103
|
+
|
|
2104
|
+
@property
|
|
2105
|
+
def safe_identifier(self) -> str:
|
|
2106
|
+
return self.identifier.replace(".", "_")
|
|
2101
2107
|
|
|
2102
2108
|
@property
|
|
2103
2109
|
def condition(self):
|
|
@@ -2166,13 +2172,13 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2166
2172
|
return self
|
|
2167
2173
|
|
|
2168
2174
|
def __repr__(self):
|
|
2169
|
-
return f"Datasource<{self.
|
|
2175
|
+
return f"Datasource<{self.identifier}@<{self.grain}>"
|
|
2170
2176
|
|
|
2171
2177
|
def __str__(self):
|
|
2172
2178
|
return self.__repr__()
|
|
2173
2179
|
|
|
2174
2180
|
def __hash__(self):
|
|
2175
|
-
return self.
|
|
2181
|
+
return self.identifier.__hash__()
|
|
2176
2182
|
|
|
2177
2183
|
def with_namespace(self, namespace: str):
|
|
2178
2184
|
new_namespace = (
|
|
@@ -2181,7 +2187,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2181
2187
|
else namespace
|
|
2182
2188
|
)
|
|
2183
2189
|
return Datasource(
|
|
2184
|
-
|
|
2190
|
+
name=self.name,
|
|
2185
2191
|
namespace=new_namespace,
|
|
2186
2192
|
grain=self.grain.with_namespace(namespace),
|
|
2187
2193
|
address=self.address,
|
|
@@ -2231,19 +2237,6 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2231
2237
|
f" {existing}."
|
|
2232
2238
|
)
|
|
2233
2239
|
|
|
2234
|
-
@property
|
|
2235
|
-
def name(self) -> str:
|
|
2236
|
-
return self.identifier
|
|
2237
|
-
# TODO: namespace all references
|
|
2238
|
-
# return f'{self.namespace}_{self.identifier}'
|
|
2239
|
-
|
|
2240
|
-
@property
|
|
2241
|
-
def full_name(self) -> str:
|
|
2242
|
-
if not self.namespace:
|
|
2243
|
-
return self.identifier
|
|
2244
|
-
namespace = self.namespace.replace(".", "_") if self.namespace else ""
|
|
2245
|
-
return f"{namespace}_{self.identifier}"
|
|
2246
|
-
|
|
2247
2240
|
@property
|
|
2248
2241
|
def safe_location(self) -> str:
|
|
2249
2242
|
if isinstance(self.address, Address):
|
|
@@ -2298,7 +2291,7 @@ class BaseJoin(BaseModel):
|
|
|
2298
2291
|
super().__init__(**data)
|
|
2299
2292
|
if (
|
|
2300
2293
|
self.left_datasource
|
|
2301
|
-
and self.left_datasource.
|
|
2294
|
+
and self.left_datasource.identifier == self.right_datasource.identifier
|
|
2302
2295
|
):
|
|
2303
2296
|
raise SyntaxError(
|
|
2304
2297
|
f"Cannot join a dataself to itself, joining {self.left_datasource} and"
|
|
@@ -2410,6 +2403,10 @@ class QueryDatasource(BaseModel):
|
|
|
2410
2403
|
def __repr__(self):
|
|
2411
2404
|
return f"{self.identifier}@<{self.grain}>"
|
|
2412
2405
|
|
|
2406
|
+
@property
|
|
2407
|
+
def safe_identifier(self):
|
|
2408
|
+
return self.identifier.replace(".", "_")
|
|
2409
|
+
|
|
2413
2410
|
@property
|
|
2414
2411
|
def non_partial_concept_addresses(self) -> List[str]:
|
|
2415
2412
|
return [
|
|
@@ -2474,10 +2471,6 @@ class QueryDatasource(BaseModel):
|
|
|
2474
2471
|
def name(self):
|
|
2475
2472
|
return self.identifier
|
|
2476
2473
|
|
|
2477
|
-
@property
|
|
2478
|
-
def full_name(self):
|
|
2479
|
-
return self.identifier
|
|
2480
|
-
|
|
2481
2474
|
@property
|
|
2482
2475
|
def group_required(self) -> bool:
|
|
2483
2476
|
if self.force_group is True:
|
|
@@ -2524,10 +2517,12 @@ class QueryDatasource(BaseModel):
|
|
|
2524
2517
|
merged_datasources = {}
|
|
2525
2518
|
|
|
2526
2519
|
for ds in [*self.datasources, *other.datasources]:
|
|
2527
|
-
if ds.
|
|
2528
|
-
merged_datasources[ds.
|
|
2520
|
+
if ds.safe_identifier in merged_datasources:
|
|
2521
|
+
merged_datasources[ds.safe_identifier] = (
|
|
2522
|
+
merged_datasources[ds.safe_identifier] + ds
|
|
2523
|
+
)
|
|
2529
2524
|
else:
|
|
2530
|
-
merged_datasources[ds.
|
|
2525
|
+
merged_datasources[ds.safe_identifier] = ds
|
|
2531
2526
|
|
|
2532
2527
|
final_source_map = defaultdict(set)
|
|
2533
2528
|
for key in self.source_map:
|
|
@@ -2538,7 +2533,9 @@ class QueryDatasource(BaseModel):
|
|
|
2538
2533
|
if key not in final_source_map:
|
|
2539
2534
|
final_source_map[key] = other.source_map[key]
|
|
2540
2535
|
for k, v in final_source_map.items():
|
|
2541
|
-
final_source_map[k] = set(
|
|
2536
|
+
final_source_map[k] = set(
|
|
2537
|
+
merged_datasources[x.safe_identifier] for x in list(v)
|
|
2538
|
+
)
|
|
2542
2539
|
self_hidden = self.hidden_concepts or []
|
|
2543
2540
|
other_hidden = other.hidden_concepts or []
|
|
2544
2541
|
hidden = [x for x in self_hidden if x.address in other_hidden]
|
|
@@ -2578,7 +2575,7 @@ class QueryDatasource(BaseModel):
|
|
|
2578
2575
|
)
|
|
2579
2576
|
# partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
|
|
2580
2577
|
return (
|
|
2581
|
-
"_join_".join([d.
|
|
2578
|
+
"_join_".join([d.identifier for d in self.datasources])
|
|
2582
2579
|
+ (f"_at_{grain}" if grain else "_at_abstract")
|
|
2583
2580
|
+ (f"_filtered_by_{filters}" if filters else "")
|
|
2584
2581
|
# + (f"_partial_{partial}" if partial else "")
|
|
@@ -2594,8 +2591,9 @@ class QueryDatasource(BaseModel):
|
|
|
2594
2591
|
for x in self.datasources:
|
|
2595
2592
|
# query datasources should be referenced by their alias, always
|
|
2596
2593
|
force_alias = isinstance(x, QueryDatasource)
|
|
2594
|
+
#
|
|
2597
2595
|
use_raw_name = isinstance(x, Datasource) and not force_alias
|
|
2598
|
-
if source and x.
|
|
2596
|
+
if source and x.safe_identifier != source:
|
|
2599
2597
|
continue
|
|
2600
2598
|
try:
|
|
2601
2599
|
return x.get_alias(
|
|
@@ -2649,6 +2647,14 @@ class CTE(BaseModel):
|
|
|
2649
2647
|
base_name_override: Optional[str] = None
|
|
2650
2648
|
base_alias_override: Optional[str] = None
|
|
2651
2649
|
|
|
2650
|
+
@property
|
|
2651
|
+
def identifier(self):
|
|
2652
|
+
return self.name
|
|
2653
|
+
|
|
2654
|
+
@property
|
|
2655
|
+
def safe_identifier(self):
|
|
2656
|
+
return self.name
|
|
2657
|
+
|
|
2652
2658
|
@computed_field # type: ignore
|
|
2653
2659
|
@property
|
|
2654
2660
|
def output_lcl(self) -> LooseConceptList:
|
|
@@ -2746,7 +2752,7 @@ class CTE(BaseModel):
|
|
|
2746
2752
|
return False
|
|
2747
2753
|
if any(
|
|
2748
2754
|
[
|
|
2749
|
-
x.
|
|
2755
|
+
x.safe_identifier == ds_being_inlined.safe_identifier
|
|
2750
2756
|
for x in self.source.datasources
|
|
2751
2757
|
]
|
|
2752
2758
|
):
|
|
@@ -2757,39 +2763,49 @@ class CTE(BaseModel):
|
|
|
2757
2763
|
*[
|
|
2758
2764
|
x
|
|
2759
2765
|
for x in self.source.datasources
|
|
2760
|
-
if x.
|
|
2766
|
+
if x.safe_identifier != qds_being_inlined.safe_identifier
|
|
2761
2767
|
],
|
|
2762
2768
|
]
|
|
2763
2769
|
# need to identify this before updating joins
|
|
2764
2770
|
if self.base_name == parent.name:
|
|
2765
2771
|
self.base_name_override = ds_being_inlined.safe_location
|
|
2766
|
-
self.base_alias_override = ds_being_inlined.
|
|
2772
|
+
self.base_alias_override = ds_being_inlined.safe_identifier
|
|
2767
2773
|
|
|
2768
2774
|
for join in self.joins:
|
|
2769
2775
|
if isinstance(join, InstantiatedUnnestJoin):
|
|
2770
2776
|
continue
|
|
2771
|
-
if
|
|
2777
|
+
if (
|
|
2778
|
+
join.left_cte
|
|
2779
|
+
and join.left_cte.safe_identifier == parent.safe_identifier
|
|
2780
|
+
):
|
|
2772
2781
|
join.inline_cte(parent)
|
|
2773
2782
|
if join.joinkey_pairs:
|
|
2774
2783
|
for pair in join.joinkey_pairs:
|
|
2775
|
-
if pair.cte and pair.cte.
|
|
2784
|
+
if pair.cte and pair.cte.safe_identifier == parent.safe_identifier:
|
|
2776
2785
|
join.inline_cte(parent)
|
|
2777
|
-
if join.right_cte.
|
|
2786
|
+
if join.right_cte.safe_identifier == parent.safe_identifier:
|
|
2778
2787
|
join.inline_cte(parent)
|
|
2779
2788
|
for k, v in self.source_map.items():
|
|
2780
2789
|
if isinstance(v, list):
|
|
2781
2790
|
self.source_map[k] = [
|
|
2782
|
-
|
|
2791
|
+
(
|
|
2792
|
+
ds_being_inlined.safe_identifier
|
|
2793
|
+
if x == parent.safe_identifier
|
|
2794
|
+
else x
|
|
2795
|
+
)
|
|
2796
|
+
for x in v
|
|
2783
2797
|
]
|
|
2784
|
-
elif v == parent.
|
|
2785
|
-
self.source_map[k] = [ds_being_inlined.
|
|
2798
|
+
elif v == parent.safe_identifier:
|
|
2799
|
+
self.source_map[k] = [ds_being_inlined.safe_identifier]
|
|
2786
2800
|
|
|
2787
2801
|
# zip in any required values for lookups
|
|
2788
2802
|
for k in ds_being_inlined.output_lcl.addresses:
|
|
2789
2803
|
if k in self.source_map and self.source_map[k]:
|
|
2790
2804
|
continue
|
|
2791
|
-
self.source_map[k] = [ds_being_inlined.
|
|
2792
|
-
self.parent_ctes = [
|
|
2805
|
+
self.source_map[k] = [ds_being_inlined.safe_identifier]
|
|
2806
|
+
self.parent_ctes = [
|
|
2807
|
+
x for x in self.parent_ctes if x.safe_identifier != parent.safe_identifier
|
|
2808
|
+
]
|
|
2793
2809
|
if force_group:
|
|
2794
2810
|
self.group_to_grain = True
|
|
2795
2811
|
return True
|
|
@@ -3006,28 +3022,22 @@ class Join(BaseModel):
|
|
|
3006
3022
|
def inline_cte(self, cte: CTE):
|
|
3007
3023
|
self.inlined_ctes.add(cte.name)
|
|
3008
3024
|
|
|
3009
|
-
# @property
|
|
3010
|
-
# def left_name(self) -> str:
|
|
3011
|
-
# if self.left_cte.name in self.inlined_ctes:
|
|
3012
|
-
# return self.left_cte.source.datasources[0].identifier
|
|
3013
|
-
# return self.left_cte.name
|
|
3014
|
-
|
|
3015
3025
|
def get_name(self, cte: CTE):
|
|
3016
|
-
if cte.
|
|
3017
|
-
return cte.source.datasources[0].
|
|
3018
|
-
return cte.
|
|
3026
|
+
if cte.identifier in self.inlined_ctes:
|
|
3027
|
+
return cte.source.datasources[0].safe_identifier
|
|
3028
|
+
return cte.safe_identifier
|
|
3019
3029
|
|
|
3020
3030
|
@property
|
|
3021
3031
|
def right_name(self) -> str:
|
|
3022
|
-
if self.right_cte.
|
|
3023
|
-
return self.right_cte.source.datasources[0].
|
|
3024
|
-
return self.right_cte.
|
|
3032
|
+
if self.right_cte.identifier in self.inlined_ctes:
|
|
3033
|
+
return self.right_cte.source.datasources[0].safe_identifier
|
|
3034
|
+
return self.right_cte.safe_identifier
|
|
3025
3035
|
|
|
3026
3036
|
@property
|
|
3027
3037
|
def right_ref(self) -> str:
|
|
3028
|
-
if self.right_cte.
|
|
3029
|
-
return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].
|
|
3030
|
-
return self.right_cte.
|
|
3038
|
+
if self.right_cte.identifier in self.inlined_ctes:
|
|
3039
|
+
return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].safe_identifier}"
|
|
3040
|
+
return self.right_cte.safe_identifier
|
|
3031
3041
|
|
|
3032
3042
|
@property
|
|
3033
3043
|
def unique_id(self) -> str:
|
|
@@ -3245,7 +3255,6 @@ class EnvironmentConceptDict(dict):
|
|
|
3245
3255
|
)
|
|
3246
3256
|
self.undefined[key] = undefined
|
|
3247
3257
|
return undefined
|
|
3248
|
-
|
|
3249
3258
|
matches = self._find_similar_concepts(key)
|
|
3250
3259
|
message = f"Undefined concept: {key}."
|
|
3251
3260
|
if matches:
|
|
@@ -3255,8 +3264,15 @@ class EnvironmentConceptDict(dict):
|
|
|
3255
3264
|
raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
|
|
3256
3265
|
raise UndefinedConceptException(message, matches)
|
|
3257
3266
|
|
|
3258
|
-
def _find_similar_concepts(self, concept_name):
|
|
3259
|
-
|
|
3267
|
+
def _find_similar_concepts(self, concept_name: str):
|
|
3268
|
+
def strip_local(input: str):
|
|
3269
|
+
if input.startswith(f"{DEFAULT_NAMESPACE}."):
|
|
3270
|
+
return input[len(DEFAULT_NAMESPACE) + 1 :]
|
|
3271
|
+
return input
|
|
3272
|
+
|
|
3273
|
+
matches = difflib.get_close_matches(
|
|
3274
|
+
strip_local(concept_name), [strip_local(x) for x in self.keys()]
|
|
3275
|
+
)
|
|
3260
3276
|
return matches
|
|
3261
3277
|
|
|
3262
3278
|
def items(self) -> ItemsView[str, Concept]: # type: ignore
|
|
@@ -3306,7 +3322,9 @@ class Environment(BaseModel):
|
|
|
3306
3322
|
] = Field(default_factory=EnvironmentDatasourceDict)
|
|
3307
3323
|
functions: Dict[str, Function] = Field(default_factory=dict)
|
|
3308
3324
|
data_types: Dict[str, DataType] = Field(default_factory=dict)
|
|
3309
|
-
imports: Dict[str, ImportStatement] = Field(
|
|
3325
|
+
imports: Dict[str, list[ImportStatement]] = Field(
|
|
3326
|
+
default_factory=lambda: defaultdict(list)
|
|
3327
|
+
)
|
|
3310
3328
|
namespace: str = DEFAULT_NAMESPACE
|
|
3311
3329
|
working_path: str | Path = Field(default_factory=lambda: os.getcwd())
|
|
3312
3330
|
environment_config: EnvironmentOptions = Field(default_factory=EnvironmentOptions)
|
|
@@ -3315,7 +3333,6 @@ class Environment(BaseModel):
|
|
|
3315
3333
|
|
|
3316
3334
|
materialized_concepts: List[Concept] = Field(default_factory=list)
|
|
3317
3335
|
alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
|
|
3318
|
-
canonical_map: Dict[str, str] = Field(default_factory=dict)
|
|
3319
3336
|
_parse_count: int = 0
|
|
3320
3337
|
|
|
3321
3338
|
@classmethod
|
|
@@ -3420,14 +3437,52 @@ class Environment(BaseModel):
|
|
|
3420
3437
|
f"Assignment to concept '{lookup}' is a duplicate declaration;"
|
|
3421
3438
|
)
|
|
3422
3439
|
|
|
3423
|
-
def add_import(
|
|
3424
|
-
self
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3440
|
+
def add_import(
|
|
3441
|
+
self, alias: str, source: Environment, imp_stm: ImportStatement | None = None
|
|
3442
|
+
):
|
|
3443
|
+
exists = False
|
|
3444
|
+
existing = self.imports[alias]
|
|
3445
|
+
if imp_stm:
|
|
3446
|
+
if any([x.path == imp_stm.path for x in existing]):
|
|
3447
|
+
exists = True
|
|
3448
|
+
|
|
3449
|
+
else:
|
|
3450
|
+
if any([x.path == source.working_path for x in existing]):
|
|
3451
|
+
exists = True
|
|
3452
|
+
imp_stm = ImportStatement(alias=alias, path=Path(source.working_path))
|
|
3453
|
+
|
|
3454
|
+
same_namespace = alias == self.namespace
|
|
3455
|
+
|
|
3456
|
+
if not exists:
|
|
3457
|
+
self.imports[alias].append(imp_stm)
|
|
3458
|
+
|
|
3459
|
+
for k, concept in source.concepts.items():
|
|
3460
|
+
if same_namespace:
|
|
3461
|
+
new = self.add_concept(concept, _ignore_cache=True)
|
|
3462
|
+
else:
|
|
3463
|
+
new = self.add_concept(
|
|
3464
|
+
concept.with_namespace(alias), _ignore_cache=True
|
|
3465
|
+
)
|
|
3466
|
+
|
|
3467
|
+
k = address_with_namespace(k, alias)
|
|
3468
|
+
# set this explicitly, to handle aliasing
|
|
3469
|
+
self.concepts[k] = new
|
|
3470
|
+
|
|
3471
|
+
for _, datasource in source.datasources.items():
|
|
3472
|
+
if same_namespace:
|
|
3473
|
+
self.add_datasource(datasource, _ignore_cache=True)
|
|
3474
|
+
else:
|
|
3475
|
+
self.add_datasource(
|
|
3476
|
+
datasource.with_namespace(alias), _ignore_cache=True
|
|
3477
|
+
)
|
|
3478
|
+
for key, val in source.alias_origin_lookup.items():
|
|
3479
|
+
if same_namespace:
|
|
3480
|
+
self.alias_origin_lookup[key] = val
|
|
3481
|
+
else:
|
|
3482
|
+
self.alias_origin_lookup[address_with_namespace(key, alias)] = (
|
|
3483
|
+
val.with_namespace(alias)
|
|
3484
|
+
)
|
|
3485
|
+
|
|
3431
3486
|
self.gen_concept_list_caches()
|
|
3432
3487
|
return self
|
|
3433
3488
|
|
|
@@ -3438,18 +3493,15 @@ class Environment(BaseModel):
|
|
|
3438
3493
|
apath[-1] = apath[-1] + ".preql"
|
|
3439
3494
|
|
|
3440
3495
|
target: Path = Path(self.working_path, *apath)
|
|
3496
|
+
if alias in self.imports:
|
|
3497
|
+
imports = self.imports[alias]
|
|
3498
|
+
for x in imports:
|
|
3499
|
+
if x.path == target:
|
|
3500
|
+
return imports
|
|
3441
3501
|
if env:
|
|
3442
|
-
self.imports[alias]
|
|
3443
|
-
alias=alias, path=target, environment=env
|
|
3502
|
+
self.imports[alias].append(
|
|
3503
|
+
ImportStatement(alias=alias, path=target, environment=env)
|
|
3444
3504
|
)
|
|
3445
|
-
|
|
3446
|
-
elif alias in self.imports:
|
|
3447
|
-
current = self.imports[alias]
|
|
3448
|
-
env = self.imports[alias].environment
|
|
3449
|
-
if current.path != target:
|
|
3450
|
-
raise ImportError(
|
|
3451
|
-
f"Attempted to import {target} with alias {alias} but {alias} is already imported from {current.path}"
|
|
3452
|
-
)
|
|
3453
3505
|
else:
|
|
3454
3506
|
try:
|
|
3455
3507
|
with open(target, "r", encoding="utf-8") as f:
|
|
@@ -3468,14 +3520,13 @@ class Environment(BaseModel):
|
|
|
3468
3520
|
f"Unable to import file {target.parent}, parsing error: {e}"
|
|
3469
3521
|
)
|
|
3470
3522
|
env = nparser.environment
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
self.add_concept(concept.with_namespace(alias))
|
|
3523
|
+
for _, concept in env.concepts.items():
|
|
3524
|
+
self.add_concept(concept.with_namespace(alias))
|
|
3474
3525
|
|
|
3475
|
-
|
|
3476
|
-
|
|
3526
|
+
for _, datasource in env.datasources.items():
|
|
3527
|
+
self.add_datasource(datasource.with_namespace(alias))
|
|
3477
3528
|
imps = ImportStatement(alias=alias, path=target, environment=env)
|
|
3478
|
-
self.imports[alias]
|
|
3529
|
+
self.imports[alias].append(imps)
|
|
3479
3530
|
return imps
|
|
3480
3531
|
|
|
3481
3532
|
def parse(
|
|
@@ -3522,8 +3573,6 @@ class Environment(BaseModel):
|
|
|
3522
3573
|
existing = self.validate_concept(concept, meta=meta)
|
|
3523
3574
|
if existing:
|
|
3524
3575
|
concept = existing
|
|
3525
|
-
if concept.namespace == DEFAULT_NAMESPACE:
|
|
3526
|
-
self.concepts[concept.name] = concept
|
|
3527
3576
|
self.concepts[concept.address] = concept
|
|
3528
3577
|
from trilogy.core.environment_helpers import generate_related_concepts
|
|
3529
3578
|
|
|
@@ -3538,8 +3587,14 @@ class Environment(BaseModel):
|
|
|
3538
3587
|
meta: Meta | None = None,
|
|
3539
3588
|
_ignore_cache: bool = False,
|
|
3540
3589
|
):
|
|
3541
|
-
self.datasources[datasource.
|
|
3590
|
+
self.datasources[datasource.identifier] = datasource
|
|
3591
|
+
|
|
3592
|
+
eligible_to_promote_roots = datasource.non_partial_for is None
|
|
3593
|
+
# mark this as canonical source
|
|
3542
3594
|
for current_concept in datasource.output_concepts:
|
|
3595
|
+
if not eligible_to_promote_roots:
|
|
3596
|
+
continue
|
|
3597
|
+
|
|
3543
3598
|
current_derivation = current_concept.derivation
|
|
3544
3599
|
# TODO: refine this section;
|
|
3545
3600
|
# too hacky for maintainability
|
|
@@ -3605,7 +3660,6 @@ class Environment(BaseModel):
|
|
|
3605
3660
|
v.pseudonyms.add(source.address)
|
|
3606
3661
|
if v.address == source.address:
|
|
3607
3662
|
replacements[k] = target
|
|
3608
|
-
self.canonical_map[k] = target.address
|
|
3609
3663
|
v.pseudonyms.add(target.address)
|
|
3610
3664
|
# we need to update keys and grains of all concepts
|
|
3611
3665
|
else:
|
|
@@ -63,14 +63,14 @@ class InlineDatasource(OptimizationRule):
|
|
|
63
63
|
for replaceable in to_inline:
|
|
64
64
|
if replaceable.name not in self.candidates[cte.name]:
|
|
65
65
|
self.candidates[cte.name].add(replaceable.name)
|
|
66
|
-
self.count[replaceable.source.
|
|
66
|
+
self.count[replaceable.source.identifier] += 1
|
|
67
67
|
return True
|
|
68
68
|
if (
|
|
69
|
-
self.count[replaceable.source.
|
|
69
|
+
self.count[replaceable.source.identifier]
|
|
70
70
|
> CONFIG.optimizations.constant_inline_cutoff
|
|
71
71
|
):
|
|
72
72
|
self.log(
|
|
73
|
-
f"Skipping inlining raw datasource {replaceable.source.
|
|
73
|
+
f"Skipping inlining raw datasource {replaceable.source.identifier} ({replaceable.name}) due to multiple references"
|
|
74
74
|
)
|
|
75
75
|
continue
|
|
76
76
|
if not replaceable.source.datasources[0].grain.issubset(replaceable.grain):
|
|
@@ -81,7 +81,7 @@ class InlineDatasource(OptimizationRule):
|
|
|
81
81
|
result = cte.inline_parent_datasource(replaceable, force_group=force_group)
|
|
82
82
|
if result:
|
|
83
83
|
self.log(
|
|
84
|
-
f"Inlined parent {replaceable.name} with {replaceable.source.
|
|
84
|
+
f"Inlined parent {replaceable.name} with {replaceable.source.identifier}"
|
|
85
85
|
)
|
|
86
86
|
optimized = True
|
|
87
87
|
else:
|
|
@@ -193,6 +193,7 @@ def create_select_node(
|
|
|
193
193
|
g,
|
|
194
194
|
environment: Environment,
|
|
195
195
|
depth: int,
|
|
196
|
+
conditions: WhereClause | None = None,
|
|
196
197
|
) -> StrategyNode:
|
|
197
198
|
ds_name = ds_name.split("~")[1]
|
|
198
199
|
all_concepts = [
|
|
@@ -231,6 +232,7 @@ def create_select_node(
|
|
|
231
232
|
c.concept for c in datasource.columns if c.is_nullable and c.concept in all_lcl
|
|
232
233
|
]
|
|
233
234
|
nullable_lcl = LooseConceptList(concepts=nullable_concepts)
|
|
235
|
+
partial_is_full = conditions and (conditions == datasource.non_partial_for)
|
|
234
236
|
|
|
235
237
|
bcandidate: StrategyNode = SelectNode(
|
|
236
238
|
input_concepts=[c.concept for c in datasource.columns],
|
|
@@ -239,12 +241,15 @@ def create_select_node(
|
|
|
239
241
|
g=g,
|
|
240
242
|
parents=[],
|
|
241
243
|
depth=depth,
|
|
242
|
-
partial_concepts=
|
|
244
|
+
partial_concepts=(
|
|
245
|
+
[] if partial_is_full else [c for c in all_concepts if c in partial_lcl]
|
|
246
|
+
),
|
|
243
247
|
nullable_concepts=[c for c in all_concepts if c in nullable_lcl],
|
|
244
248
|
accept_partial=accept_partial,
|
|
245
249
|
datasource=datasource,
|
|
246
250
|
grain=Grain(components=all_concepts),
|
|
247
251
|
conditions=datasource.where.conditional if datasource.where else None,
|
|
252
|
+
render_condition=not partial_is_full,
|
|
248
253
|
)
|
|
249
254
|
|
|
250
255
|
# we need to nest the group node one further
|
|
@@ -312,6 +317,7 @@ def gen_select_merge_node(
|
|
|
312
317
|
accept_partial=accept_partial,
|
|
313
318
|
environment=environment,
|
|
314
319
|
depth=depth,
|
|
320
|
+
conditions=conditions,
|
|
315
321
|
)
|
|
316
322
|
for k, subgraph in sub_nodes.items()
|
|
317
323
|
]
|
|
@@ -165,6 +165,7 @@ class StrategyNode:
|
|
|
165
165
|
hidden_concepts: List[Concept] | None = None,
|
|
166
166
|
existence_concepts: List[Concept] | None = None,
|
|
167
167
|
virtual_output_concepts: List[Concept] | None = None,
|
|
168
|
+
render_condition: bool = True,
|
|
168
169
|
):
|
|
169
170
|
self.input_concepts: List[Concept] = (
|
|
170
171
|
unique(input_concepts, "address") if input_concepts else []
|
|
@@ -208,6 +209,7 @@ class StrategyNode:
|
|
|
208
209
|
)
|
|
209
210
|
self.validate_parents()
|
|
210
211
|
self.log = True
|
|
212
|
+
self.render_condition = render_condition
|
|
211
213
|
|
|
212
214
|
def add_parents(self, parents: list["StrategyNode"]):
|
|
213
215
|
self.parents += parents
|
|
@@ -380,6 +382,7 @@ class StrategyNode:
|
|
|
380
382
|
hidden_concepts=list(self.hidden_concepts),
|
|
381
383
|
existence_concepts=list(self.existence_concepts),
|
|
382
384
|
virtual_output_concepts=list(self.virtual_output_concepts),
|
|
385
|
+
render_condition=self.render_condition,
|
|
383
386
|
)
|
|
384
387
|
|
|
385
388
|
|
|
@@ -89,8 +89,8 @@ def deduplicate_nodes_and_joins(
|
|
|
89
89
|
joins = [
|
|
90
90
|
j
|
|
91
91
|
for j in joins
|
|
92
|
-
if j.left_node.resolve().
|
|
93
|
-
and j.right_node.resolve().
|
|
92
|
+
if j.left_node.resolve().identifier not in removed
|
|
93
|
+
and j.right_node.resolve().identifier not in removed
|
|
94
94
|
]
|
|
95
95
|
return joins, merged
|
|
96
96
|
|
|
@@ -155,8 +155,8 @@ class MergeNode(StrategyNode):
|
|
|
155
155
|
for join in node_joins:
|
|
156
156
|
left = join.left_node.resolve()
|
|
157
157
|
right = join.right_node.resolve()
|
|
158
|
-
if left.
|
|
159
|
-
raise SyntaxError(f"Cannot join node {left.
|
|
158
|
+
if left.identifier == right.identifier:
|
|
159
|
+
raise SyntaxError(f"Cannot join node {left.identifier} to itself")
|
|
160
160
|
joins.append(
|
|
161
161
|
BaseJoin(
|
|
162
162
|
left_datasource=left,
|
|
@@ -168,7 +168,7 @@ class MergeNode(StrategyNode):
|
|
|
168
168
|
)
|
|
169
169
|
return joins
|
|
170
170
|
|
|
171
|
-
def create_full_joins(self, dataset_list: List[QueryDatasource]):
|
|
171
|
+
def create_full_joins(self, dataset_list: List[QueryDatasource | Datasource]):
|
|
172
172
|
joins = []
|
|
173
173
|
seen = set()
|
|
174
174
|
for left_value in dataset_list:
|
|
@@ -198,7 +198,7 @@ class MergeNode(StrategyNode):
|
|
|
198
198
|
environment: Environment,
|
|
199
199
|
) -> List[BaseJoin | UnnestJoin]:
|
|
200
200
|
# only finally, join between them for unique values
|
|
201
|
-
dataset_list: List[QueryDatasource] = sorted(
|
|
201
|
+
dataset_list: List[QueryDatasource | Datasource] = sorted(
|
|
202
202
|
final_datasets, key=lambda x: -len(x.grain.components_copy)
|
|
203
203
|
)
|
|
204
204
|
|
|
@@ -238,13 +238,13 @@ class MergeNode(StrategyNode):
|
|
|
238
238
|
merged: dict[str, QueryDatasource | Datasource] = {}
|
|
239
239
|
final_joins: List[NodeJoin] | None = self.node_joins
|
|
240
240
|
for source in parent_sources:
|
|
241
|
-
if source.
|
|
241
|
+
if source.identifier in merged:
|
|
242
242
|
logger.info(
|
|
243
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.
|
|
243
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.identifier} into existing"
|
|
244
244
|
)
|
|
245
|
-
merged[source.
|
|
245
|
+
merged[source.identifier] = merged[source.identifier] + source
|
|
246
246
|
else:
|
|
247
|
-
merged[source.
|
|
247
|
+
merged[source.identifier] = source
|
|
248
248
|
|
|
249
249
|
# it's possible that we have more sources than we need
|
|
250
250
|
final_joins, merged = deduplicate_nodes_and_joins(
|
|
@@ -49,6 +49,7 @@ class SelectNode(StrategyNode):
|
|
|
49
49
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
50
50
|
preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
51
51
|
hidden_concepts: List[Concept] | None = None,
|
|
52
|
+
render_condition: bool = True,
|
|
52
53
|
):
|
|
53
54
|
super().__init__(
|
|
54
55
|
input_concepts=input_concepts,
|
|
@@ -65,6 +66,7 @@ class SelectNode(StrategyNode):
|
|
|
65
66
|
conditions=conditions,
|
|
66
67
|
preexisting_conditions=preexisting_conditions,
|
|
67
68
|
hidden_concepts=hidden_concepts,
|
|
69
|
+
render_condition=render_condition,
|
|
68
70
|
)
|
|
69
71
|
self.accept_partial = accept_partial
|
|
70
72
|
self.datasource = datasource
|
|
@@ -120,7 +122,8 @@ class SelectNode(StrategyNode):
|
|
|
120
122
|
],
|
|
121
123
|
nullable_concepts=[c.concept for c in datasource.columns if c.is_nullable],
|
|
122
124
|
source_type=SourceType.DIRECT_SELECT,
|
|
123
|
-
|
|
125
|
+
# we can skip rendering conditions
|
|
126
|
+
condition=self.conditions if self.render_condition else None,
|
|
124
127
|
# select nodes should never group
|
|
125
128
|
force_group=self.force_group,
|
|
126
129
|
hidden_concepts=self.hidden_concepts,
|
|
@@ -128,7 +131,7 @@ class SelectNode(StrategyNode):
|
|
|
128
131
|
|
|
129
132
|
def resolve_from_constant_datasources(self) -> QueryDatasource:
|
|
130
133
|
datasource = Datasource(
|
|
131
|
-
|
|
134
|
+
name=CONSTANT_DATASET, address=CONSTANT_DATASET, columns=[]
|
|
132
135
|
)
|
|
133
136
|
return QueryDatasource(
|
|
134
137
|
input_concepts=[],
|
|
@@ -205,6 +208,7 @@ class SelectNode(StrategyNode):
|
|
|
205
208
|
conditions=self.conditions,
|
|
206
209
|
preexisting_conditions=self.preexisting_conditions,
|
|
207
210
|
hidden_concepts=self.hidden_concepts,
|
|
211
|
+
render_condition=self.render_condition,
|
|
208
212
|
)
|
|
209
213
|
|
|
210
214
|
|
|
@@ -66,9 +66,6 @@ def resolve_join_order_v2(
|
|
|
66
66
|
) -> list[JoinOrderOutput]:
|
|
67
67
|
datasources = [x for x in g.nodes if x.startswith("ds~")]
|
|
68
68
|
concepts = [x for x in g.nodes if x.startswith("c~")]
|
|
69
|
-
# from trilogy.hooks.graph_hook import GraphHook
|
|
70
|
-
|
|
71
|
-
# GraphHook().query_graph_built(g)
|
|
72
69
|
|
|
73
70
|
output: list[JoinOrderOutput] = []
|
|
74
71
|
pivot_map = {
|
|
@@ -78,7 +75,7 @@ def resolve_join_order_v2(
|
|
|
78
75
|
pivots = list(
|
|
79
76
|
sorted(
|
|
80
77
|
[x for x in pivot_map if len(pivot_map[x]) > 1],
|
|
81
|
-
key=lambda x: len(pivot_map[x]),
|
|
78
|
+
key=lambda x: (len(pivot_map[x]), len(x), x),
|
|
82
79
|
)
|
|
83
80
|
)
|
|
84
81
|
solo = [x for x in pivot_map if len(pivot_map[x]) == 1]
|
|
@@ -95,7 +92,7 @@ def resolve_join_order_v2(
|
|
|
95
92
|
root = pivots.pop()
|
|
96
93
|
|
|
97
94
|
# sort so less partials is last and eligible lefts are
|
|
98
|
-
def score_key(x: str) -> int:
|
|
95
|
+
def score_key(x: str) -> tuple[int, int, str]:
|
|
99
96
|
base = 1
|
|
100
97
|
# if it's left, higher weight
|
|
101
98
|
if x in eligible_left:
|
|
@@ -103,7 +100,7 @@ def resolve_join_order_v2(
|
|
|
103
100
|
# if it has the concept as a partial, lower weight
|
|
104
101
|
if root in partials.get(x, []):
|
|
105
102
|
base -= 1
|
|
106
|
-
return base
|
|
103
|
+
return (base, len(x), x)
|
|
107
104
|
|
|
108
105
|
# get remainig un-joined datasets
|
|
109
106
|
to_join = sorted(
|
|
@@ -296,7 +293,7 @@ def add_node_join_concept(
|
|
|
296
293
|
|
|
297
294
|
|
|
298
295
|
def resolve_instantiated_concept(
|
|
299
|
-
concept: Concept, datasource: QueryDatasource
|
|
296
|
+
concept: Concept, datasource: QueryDatasource | Datasource
|
|
300
297
|
) -> Concept:
|
|
301
298
|
if concept.address in datasource.output_concepts:
|
|
302
299
|
return concept
|
|
@@ -309,14 +306,14 @@ def resolve_instantiated_concept(
|
|
|
309
306
|
|
|
310
307
|
|
|
311
308
|
def get_node_joins(
|
|
312
|
-
datasources: List[QueryDatasource],
|
|
309
|
+
datasources: List[QueryDatasource | Datasource],
|
|
313
310
|
environment: Environment,
|
|
314
311
|
# concepts:List[Concept],
|
|
315
312
|
):
|
|
316
313
|
|
|
317
314
|
graph = nx.Graph()
|
|
318
315
|
partials: dict[str, list[str]] = {}
|
|
319
|
-
ds_node_map: dict[str, QueryDatasource] = {}
|
|
316
|
+
ds_node_map: dict[str, QueryDatasource | Datasource] = {}
|
|
320
317
|
concept_map: dict[str, Concept] = {}
|
|
321
318
|
for datasource in datasources:
|
|
322
319
|
ds_node = f"ds~{datasource.identifier}"
|
trilogy/core/query_processor.py
CHANGED
|
@@ -5,7 +5,7 @@ from trilogy.core.graph_models import ReferenceGraph
|
|
|
5
5
|
from trilogy.core.constants import CONSTANT_DATASET
|
|
6
6
|
from trilogy.core.processing.concept_strategies_v3 import source_query_concepts
|
|
7
7
|
from trilogy.core.enums import BooleanOperator
|
|
8
|
-
from trilogy.constants import CONFIG
|
|
8
|
+
from trilogy.constants import CONFIG
|
|
9
9
|
from trilogy.core.processing.nodes import SelectNode, StrategyNode, History
|
|
10
10
|
from trilogy.core.models import (
|
|
11
11
|
Concept,
|
|
@@ -55,12 +55,12 @@ def base_join_to_join(
|
|
|
55
55
|
|
|
56
56
|
def get_datasource_cte(datasource: Datasource | QueryDatasource) -> CTE:
|
|
57
57
|
for cte in ctes:
|
|
58
|
-
if cte.source.
|
|
58
|
+
if cte.source.identifier == datasource.identifier:
|
|
59
59
|
return cte
|
|
60
60
|
for cte in ctes:
|
|
61
|
-
if cte.source.datasources[0].
|
|
61
|
+
if cte.source.datasources[0].identifier == datasource.identifier:
|
|
62
62
|
return cte
|
|
63
|
-
raise ValueError(f"Could not find CTE for datasource {datasource.
|
|
63
|
+
raise ValueError(f"Could not find CTE for datasource {datasource.identifier}")
|
|
64
64
|
|
|
65
65
|
if base_join.left_datasource is not None:
|
|
66
66
|
left_cte = get_datasource_cte(base_join.left_datasource)
|
|
@@ -109,7 +109,7 @@ def generate_source_map(
|
|
|
109
109
|
# now populate anything derived in this level
|
|
110
110
|
for qdk, qdv in query_datasource.source_map.items():
|
|
111
111
|
unnest = [x for x in qdv if isinstance(x, UnnestJoin)]
|
|
112
|
-
for
|
|
112
|
+
for _ in unnest:
|
|
113
113
|
source_map[qdk] = []
|
|
114
114
|
if (
|
|
115
115
|
qdk not in source_map
|
|
@@ -119,16 +119,18 @@ def generate_source_map(
|
|
|
119
119
|
source_map[qdk] = []
|
|
120
120
|
basic = [x for x in qdv if isinstance(x, Datasource)]
|
|
121
121
|
for base in basic:
|
|
122
|
-
source_map[qdk].append(base.
|
|
122
|
+
source_map[qdk].append(base.safe_identifier)
|
|
123
123
|
|
|
124
124
|
ctes = [x for x in qdv if isinstance(x, QueryDatasource)]
|
|
125
125
|
if ctes:
|
|
126
|
-
names = set([x.
|
|
127
|
-
matches = [
|
|
126
|
+
names = set([x.safe_identifier for x in ctes])
|
|
127
|
+
matches = [
|
|
128
|
+
cte for cte in all_new_ctes if cte.source.safe_identifier in names
|
|
129
|
+
]
|
|
128
130
|
|
|
129
131
|
if not matches and names:
|
|
130
132
|
raise SyntaxError(
|
|
131
|
-
f"Missing parent CTEs for source map; expecting {names}, have {[cte.source.
|
|
133
|
+
f"Missing parent CTEs for source map; expecting {names}, have {[cte.source.safe_identifier for cte in all_new_ctes]}"
|
|
132
134
|
)
|
|
133
135
|
for cte in matches:
|
|
134
136
|
output_address = [
|
|
@@ -137,11 +139,11 @@ def generate_source_map(
|
|
|
137
139
|
if x.address not in [z.address for z in cte.partial_concepts]
|
|
138
140
|
]
|
|
139
141
|
if qdk in output_address:
|
|
140
|
-
source_map[qdk].append(cte.
|
|
142
|
+
source_map[qdk].append(cte.safe_identifier)
|
|
141
143
|
# now do a pass that accepts partials
|
|
142
144
|
for cte in matches:
|
|
143
145
|
if qdk not in source_map:
|
|
144
|
-
source_map[qdk] = [cte.
|
|
146
|
+
source_map[qdk] = [cte.safe_identifier]
|
|
145
147
|
if qdk not in source_map:
|
|
146
148
|
if not qdv:
|
|
147
149
|
source_map[qdk] = []
|
|
@@ -154,8 +156,10 @@ def generate_source_map(
|
|
|
154
156
|
# as they cannot be referenced in row resolution
|
|
155
157
|
existence_source_map: Dict[str, list[str]] = defaultdict(list)
|
|
156
158
|
for ek, ev in query_datasource.existence_source_map.items():
|
|
157
|
-
|
|
158
|
-
ematches = [
|
|
159
|
+
ids = set([x.safe_identifier for x in ev])
|
|
160
|
+
ematches = [
|
|
161
|
+
cte.name for cte in all_new_ctes if cte.source.safe_identifier in ids
|
|
162
|
+
]
|
|
159
163
|
existence_source_map[ek] = ematches
|
|
160
164
|
return {
|
|
161
165
|
k: [] if not v else list(set(v)) for k, v in source_map.items()
|
|
@@ -209,7 +213,7 @@ def resolve_cte_base_name_and_alias_v2(
|
|
|
209
213
|
and not source.datasources[0].name == CONSTANT_DATASET
|
|
210
214
|
):
|
|
211
215
|
ds = source.datasources[0]
|
|
212
|
-
return ds.safe_location, ds.
|
|
216
|
+
return ds.safe_location, ds.safe_identifier
|
|
213
217
|
|
|
214
218
|
joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
|
|
215
219
|
if joins and len(joins) > 0:
|
|
@@ -268,17 +272,17 @@ def datasource_to_ctes(
|
|
|
268
272
|
# this is required to ensure that constant datasets
|
|
269
273
|
# render properly on initial access; since they have
|
|
270
274
|
# no actual source
|
|
271
|
-
if source.
|
|
275
|
+
if source.name == CONSTANT_DATASET:
|
|
272
276
|
source_map = {k: [] for k in query_datasource.source_map}
|
|
273
277
|
existence_map = source_map
|
|
274
278
|
else:
|
|
275
279
|
source_map = {
|
|
276
|
-
k: [] if not v else [source.
|
|
280
|
+
k: [] if not v else [source.safe_identifier]
|
|
277
281
|
for k, v in query_datasource.source_map.items()
|
|
278
282
|
}
|
|
279
283
|
existence_map = source_map
|
|
280
284
|
|
|
281
|
-
human_id = generate_cte_name(query_datasource.
|
|
285
|
+
human_id = generate_cte_name(query_datasource.identifier, name_map)
|
|
282
286
|
|
|
283
287
|
final_joins = [base_join_to_join(join, parents) for join in query_datasource.joins]
|
|
284
288
|
|
trilogy/executor.py
CHANGED
|
@@ -276,6 +276,20 @@ class Executor(object):
|
|
|
276
276
|
output.append(compiled_sql)
|
|
277
277
|
return output
|
|
278
278
|
|
|
279
|
+
def parse_file(self, file: str | Path, persist: bool = False) -> Generator[
|
|
280
|
+
ProcessedQuery
|
|
281
|
+
| ProcessedQueryPersist
|
|
282
|
+
| ProcessedShowStatement
|
|
283
|
+
| ProcessedRawSQLStatement
|
|
284
|
+
| ProcessedCopyStatement,
|
|
285
|
+
None,
|
|
286
|
+
None,
|
|
287
|
+
]:
|
|
288
|
+
file = Path(file)
|
|
289
|
+
with open(file, "r") as f:
|
|
290
|
+
command = f.read()
|
|
291
|
+
return self.parse_text_generator(command, persist=persist)
|
|
292
|
+
|
|
279
293
|
def parse_text(
|
|
280
294
|
self, command: str, persist: bool = False
|
|
281
295
|
) -> List[
|
|
@@ -319,9 +333,11 @@ class Executor(object):
|
|
|
319
333
|
x = self.generator.generate_queries(
|
|
320
334
|
self.environment, [t], hooks=self.hooks
|
|
321
335
|
)[0]
|
|
336
|
+
|
|
337
|
+
yield x
|
|
338
|
+
|
|
322
339
|
if persist and isinstance(x, ProcessedQueryPersist):
|
|
323
340
|
self.environment.add_datasource(x.datasource)
|
|
324
|
-
yield x
|
|
325
341
|
|
|
326
342
|
def execute_raw_sql(
|
|
327
343
|
self, command: str, variables: dict | None = None
|
trilogy/hooks/query_debugger.py
CHANGED
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -123,6 +123,13 @@ from trilogy.parsing.common import (
|
|
|
123
123
|
arbitrary_to_concept,
|
|
124
124
|
process_function_args,
|
|
125
125
|
)
|
|
126
|
+
from dataclasses import dataclass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class WholeGrainWrapper:
|
|
131
|
+
where: WhereClause
|
|
132
|
+
|
|
126
133
|
|
|
127
134
|
CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
|
|
128
135
|
|
|
@@ -566,9 +573,11 @@ class ParseToObjects(Transformer):
|
|
|
566
573
|
return args
|
|
567
574
|
|
|
568
575
|
def grain_clause(self, args) -> Grain:
|
|
569
|
-
# namespace=self.environment.namespace,
|
|
570
576
|
return Grain(components=[self.environment.concepts[a] for a in args[0]])
|
|
571
577
|
|
|
578
|
+
def whole_grain_clause(self, args) -> WholeGrainWrapper:
|
|
579
|
+
return WholeGrainWrapper(where=args[0])
|
|
580
|
+
|
|
572
581
|
def MULTILINE_STRING(self, args) -> str:
|
|
573
582
|
return args[3:-3]
|
|
574
583
|
|
|
@@ -582,11 +591,14 @@ class ParseToObjects(Transformer):
|
|
|
582
591
|
grain: Optional[Grain] = None
|
|
583
592
|
address: Optional[Address] = None
|
|
584
593
|
where: Optional[WhereClause] = None
|
|
594
|
+
non_partial_for: Optional[WhereClause] = None
|
|
585
595
|
for val in args[1:]:
|
|
586
596
|
if isinstance(val, Address):
|
|
587
597
|
address = val
|
|
588
598
|
elif isinstance(val, Grain):
|
|
589
599
|
grain = val
|
|
600
|
+
elif isinstance(val, WholeGrainWrapper):
|
|
601
|
+
non_partial_for = val.where
|
|
590
602
|
elif isinstance(val, Query):
|
|
591
603
|
address = Address(location=f"({val.text})", is_query=True)
|
|
592
604
|
elif isinstance(val, WhereClause):
|
|
@@ -596,7 +608,7 @@ class ParseToObjects(Transformer):
|
|
|
596
608
|
"Malformed datasource, missing address or query declaration"
|
|
597
609
|
)
|
|
598
610
|
datasource = Datasource(
|
|
599
|
-
|
|
611
|
+
name=name,
|
|
600
612
|
columns=columns,
|
|
601
613
|
# grain will be set by default from args
|
|
602
614
|
# TODO: move to factory
|
|
@@ -604,6 +616,7 @@ class ParseToObjects(Transformer):
|
|
|
604
616
|
address=address,
|
|
605
617
|
namespace=self.environment.namespace,
|
|
606
618
|
where=where,
|
|
619
|
+
non_partial_for=non_partial_for,
|
|
607
620
|
)
|
|
608
621
|
for column in columns:
|
|
609
622
|
column.concept = column.concept.with_grain(datasource.grain)
|
|
@@ -801,20 +814,10 @@ class ParseToObjects(Transformer):
|
|
|
801
814
|
except Exception as e:
|
|
802
815
|
raise ImportError(f"Unable to import file {target}, parsing error: {e}")
|
|
803
816
|
|
|
804
|
-
for _, concept in nparser.environment.concepts.items():
|
|
805
|
-
self.environment.add_concept(
|
|
806
|
-
concept.with_namespace(alias), _ignore_cache=True
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
for _, datasource in nparser.environment.datasources.items():
|
|
810
|
-
self.environment.add_datasource(
|
|
811
|
-
datasource.with_namespace(alias), _ignore_cache=True
|
|
812
|
-
)
|
|
813
817
|
imps = ImportStatement(
|
|
814
818
|
alias=alias, path=Path(args[0]), environment=nparser.environment
|
|
815
819
|
)
|
|
816
|
-
self.environment.
|
|
817
|
-
self.environment.gen_concept_list_caches()
|
|
820
|
+
self.environment.add_import(alias, nparser.environment, imps)
|
|
818
821
|
return imps
|
|
819
822
|
|
|
820
823
|
@v_args(meta=True)
|
|
@@ -841,7 +844,7 @@ class ParseToObjects(Transformer):
|
|
|
841
844
|
if self.environment.namespace
|
|
842
845
|
else DEFAULT_NAMESPACE
|
|
843
846
|
),
|
|
844
|
-
|
|
847
|
+
name=identifier,
|
|
845
848
|
address=Address(location=address),
|
|
846
849
|
grain=grain,
|
|
847
850
|
)
|
trilogy/parsing/render.py
CHANGED
|
@@ -80,6 +80,9 @@ class Renderer:
|
|
|
80
80
|
metrics = []
|
|
81
81
|
# first, keys
|
|
82
82
|
for concept in arg.concepts.values():
|
|
83
|
+
if "__preql_internal" in concept.address:
|
|
84
|
+
continue
|
|
85
|
+
|
|
83
86
|
# don't render anything that came from an import
|
|
84
87
|
if concept.namespace in arg.imports:
|
|
85
88
|
continue
|
|
@@ -122,10 +125,10 @@ class Renderer:
|
|
|
122
125
|
for datasource in arg.datasources.values()
|
|
123
126
|
if datasource.namespace == DEFAULT_NAMESPACE
|
|
124
127
|
]
|
|
125
|
-
rendered_imports = [
|
|
126
|
-
|
|
127
|
-
for import_statement in
|
|
128
|
-
|
|
128
|
+
rendered_imports = []
|
|
129
|
+
for _, imports in arg.imports.items():
|
|
130
|
+
for import_statement in imports:
|
|
131
|
+
rendered_imports.append(self.to_string(import_statement))
|
|
129
132
|
components = []
|
|
130
133
|
if rendered_imports:
|
|
131
134
|
components.append(rendered_imports)
|
|
@@ -133,19 +136,26 @@ class Renderer:
|
|
|
133
136
|
components.append(rendered_concepts)
|
|
134
137
|
if rendered_datasources:
|
|
135
138
|
components.append(rendered_datasources)
|
|
139
|
+
|
|
136
140
|
final = "\n\n".join("\n".join(x) for x in components)
|
|
137
141
|
return final
|
|
138
142
|
|
|
139
143
|
@to_string.register
|
|
140
144
|
def _(self, arg: Datasource):
|
|
141
145
|
assignments = ",\n ".join([self.to_string(x) for x in arg.columns])
|
|
146
|
+
if arg.non_partial_for:
|
|
147
|
+
non_partial = f"\ncomplete where {self.to_string(arg.non_partial_for)}"
|
|
148
|
+
else:
|
|
149
|
+
non_partial = ""
|
|
142
150
|
base = f"""datasource {arg.name} (
|
|
143
151
|
{assignments}
|
|
144
152
|
)
|
|
145
|
-
{self.to_string(arg.grain)}
|
|
153
|
+
{self.to_string(arg.grain)}{non_partial}
|
|
146
154
|
{self.to_string(arg.address)}"""
|
|
155
|
+
|
|
147
156
|
if arg.where:
|
|
148
157
|
base += f"\nwhere {self.to_string(arg.where)}"
|
|
158
|
+
|
|
149
159
|
base += ";"
|
|
150
160
|
return base
|
|
151
161
|
|
|
@@ -214,9 +224,15 @@ class Renderer:
|
|
|
214
224
|
|
|
215
225
|
@to_string.register
|
|
216
226
|
def _(self, arg: "ColumnAssignment"):
|
|
227
|
+
if arg.modifiers:
|
|
228
|
+
modifiers = "".join(
|
|
229
|
+
[self.to_string(modifier) for modifier in arg.modifiers]
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
modifiers = ""
|
|
217
233
|
if isinstance(arg.alias, str):
|
|
218
|
-
return f"{arg.alias}: {self.to_string(arg.concept)}"
|
|
219
|
-
return f"{self.to_string(arg.alias)}: {self.to_string(arg.concept)}"
|
|
234
|
+
return f"{arg.alias}: {modifiers}{self.to_string(arg.concept)}"
|
|
235
|
+
return f"{self.to_string(arg.alias)}: {modifiers}{self.to_string(arg.concept)}"
|
|
220
236
|
|
|
221
237
|
@to_string.register
|
|
222
238
|
def _(self, arg: "RawColumnExpr"):
|
|
@@ -352,6 +368,8 @@ class Renderer:
|
|
|
352
368
|
|
|
353
369
|
@to_string.register
|
|
354
370
|
def _(self, arg: "ImportStatement"):
|
|
371
|
+
if arg.alias == DEFAULT_NAMESPACE:
|
|
372
|
+
return f"import {arg.path};"
|
|
355
373
|
return f"import {arg.path} as {arg.alias};"
|
|
356
374
|
|
|
357
375
|
@to_string.register
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -35,8 +35,10 @@
|
|
|
35
35
|
prop_ident: "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "." IDENTIFIER
|
|
36
36
|
|
|
37
37
|
// datasource concepts
|
|
38
|
-
datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? (address | query) where?
|
|
39
|
-
|
|
38
|
+
datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? whole_grain_clause? (address | query) where?
|
|
39
|
+
|
|
40
|
+
whole_grain_clause: "complete" where
|
|
41
|
+
|
|
40
42
|
grain_clause: "grain" "(" column_list ")"
|
|
41
43
|
|
|
42
44
|
address: "address" (QUOTED_ADDRESS | ADDRESS)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|