pytrilogy 0.0.3.113__py3-none-any.whl → 0.0.3.116__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.113.dist-info → pytrilogy-0.0.3.116.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.113.dist-info → pytrilogy-0.0.3.116.dist-info}/RECORD +30 -30
- trilogy/__init__.py +1 -1
- trilogy/constants.py +29 -0
- trilogy/core/enums.py +6 -1
- trilogy/core/functions.py +33 -0
- trilogy/core/models/author.py +126 -2
- trilogy/core/models/build.py +70 -7
- trilogy/core/models/environment.py +2 -1
- trilogy/core/optimization.py +3 -2
- trilogy/core/optimizations/hide_unused_concept.py +1 -5
- trilogy/core/processing/concept_strategies_v3.py +26 -5
- trilogy/core/processing/discovery_node_factory.py +2 -2
- trilogy/core/processing/discovery_utility.py +11 -4
- trilogy/core/processing/node_generators/basic_node.py +26 -15
- trilogy/core/processing/node_generators/common.py +4 -1
- trilogy/core/processing/node_generators/filter_node.py +7 -0
- trilogy/core/processing/node_generators/multiselect_node.py +3 -3
- trilogy/core/processing/node_generators/unnest_node.py +77 -6
- trilogy/core/statements/author.py +4 -1
- trilogy/dialect/base.py +42 -2
- trilogy/executor.py +1 -1
- trilogy/parsing/common.py +117 -20
- trilogy/parsing/parse_engine.py +115 -5
- trilogy/parsing/render.py +2 -1
- trilogy/parsing/trilogy.lark +20 -7
- {pytrilogy-0.0.3.113.dist-info → pytrilogy-0.0.3.116.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.113.dist-info → pytrilogy-0.0.3.116.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.113.dist-info → pytrilogy-0.0.3.116.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.113.dist-info → pytrilogy-0.0.3.116.dist-info}/top_level.txt +0 -0
trilogy/core/models/build.py
CHANGED
|
@@ -48,6 +48,8 @@ from trilogy.core.models.author import (
|
|
|
48
48
|
Concept,
|
|
49
49
|
ConceptRef,
|
|
50
50
|
Conditional,
|
|
51
|
+
DeriveClause,
|
|
52
|
+
DeriveItem,
|
|
51
53
|
FilterItem,
|
|
52
54
|
FuncArgs,
|
|
53
55
|
Function,
|
|
@@ -134,8 +136,9 @@ def concept_is_relevant(
|
|
|
134
136
|
if concept.purpose in (Purpose.METRIC,):
|
|
135
137
|
if all([c in others for c in concept.grain.components]):
|
|
136
138
|
return False
|
|
139
|
+
if concept.derivation in (Derivation.UNNEST,):
|
|
140
|
+
return True
|
|
137
141
|
if concept.derivation in (Derivation.BASIC,):
|
|
138
|
-
|
|
139
142
|
return any(concept_is_relevant(c, others) for c in concept.concept_arguments)
|
|
140
143
|
if concept.granularity == Granularity.SINGLE_ROW:
|
|
141
144
|
return False
|
|
@@ -246,7 +249,7 @@ class BuildParamaterizedConceptReference(DataTyped):
|
|
|
246
249
|
concept: BuildConcept
|
|
247
250
|
|
|
248
251
|
def __str__(self):
|
|
249
|
-
return f":{self.concept.address}"
|
|
252
|
+
return f":{self.concept.address.replace('.', '_')}"
|
|
250
253
|
|
|
251
254
|
@property
|
|
252
255
|
def safe_address(self) -> str:
|
|
@@ -1267,6 +1270,22 @@ class BuildAlignClause:
|
|
|
1267
1270
|
items: List[BuildAlignItem]
|
|
1268
1271
|
|
|
1269
1272
|
|
|
1273
|
+
@dataclass
|
|
1274
|
+
class BuildDeriveClause:
|
|
1275
|
+
items: List[BuildDeriveItem]
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
@dataclass
|
|
1279
|
+
class BuildDeriveItem:
|
|
1280
|
+
expr: BuildExpr
|
|
1281
|
+
name: str
|
|
1282
|
+
namespace: str = field(default=DEFAULT_NAMESPACE)
|
|
1283
|
+
|
|
1284
|
+
@property
|
|
1285
|
+
def address(self) -> str:
|
|
1286
|
+
return f"{self.namespace}.{self.name}"
|
|
1287
|
+
|
|
1288
|
+
|
|
1270
1289
|
@dataclass
|
|
1271
1290
|
class BuildSelectLineage:
|
|
1272
1291
|
selection: List[BuildConcept]
|
|
@@ -1298,12 +1317,16 @@ class BuildMultiSelectLineage(BuildConceptArgs):
|
|
|
1298
1317
|
limit: Optional[int] = None
|
|
1299
1318
|
where_clause: Union["BuildWhereClause", None] = field(default=None)
|
|
1300
1319
|
having_clause: Union["BuildHavingClause", None] = field(default=None)
|
|
1320
|
+
derive: BuildDeriveClause | None = None
|
|
1301
1321
|
|
|
1302
1322
|
@property
|
|
1303
1323
|
def derived_concepts(self) -> set[str]:
|
|
1304
1324
|
output = set()
|
|
1305
1325
|
for item in self.align.items:
|
|
1306
1326
|
output.add(item.aligned_concept)
|
|
1327
|
+
if self.derive:
|
|
1328
|
+
for ditem in self.derive.items:
|
|
1329
|
+
output.add(ditem.address)
|
|
1307
1330
|
return output
|
|
1308
1331
|
|
|
1309
1332
|
@property
|
|
@@ -1311,10 +1334,12 @@ class BuildMultiSelectLineage(BuildConceptArgs):
|
|
|
1311
1334
|
return self.build_output_components
|
|
1312
1335
|
|
|
1313
1336
|
@property
|
|
1314
|
-
def
|
|
1315
|
-
output = set()
|
|
1316
|
-
|
|
1317
|
-
output
|
|
1337
|
+
def calculated_derivations(self) -> set[str]:
|
|
1338
|
+
output: set[str] = set()
|
|
1339
|
+
if not self.derive:
|
|
1340
|
+
return output
|
|
1341
|
+
for item in self.derive.items:
|
|
1342
|
+
output.add(item.address)
|
|
1318
1343
|
return output
|
|
1319
1344
|
|
|
1320
1345
|
@property
|
|
@@ -1334,6 +1359,7 @@ class BuildMultiSelectLineage(BuildConceptArgs):
|
|
|
1334
1359
|
for c in x.concepts:
|
|
1335
1360
|
if c.address in cte.output_lcl:
|
|
1336
1361
|
return c
|
|
1362
|
+
|
|
1337
1363
|
raise SyntaxError(
|
|
1338
1364
|
f"Could not find upstream map for multiselect {str(concept)} on cte ({cte})"
|
|
1339
1365
|
)
|
|
@@ -1668,7 +1694,6 @@ class Factory:
|
|
|
1668
1694
|
valid_inputs=base.valid_inputs,
|
|
1669
1695
|
arg_count=base.arg_count,
|
|
1670
1696
|
)
|
|
1671
|
-
|
|
1672
1697
|
new = BuildFunction(
|
|
1673
1698
|
operator=base.operator,
|
|
1674
1699
|
arguments=[self.handle_constant(self.build(c)) for c in raw_args],
|
|
@@ -1724,6 +1749,14 @@ class Factory:
|
|
|
1724
1749
|
return self._build_concept(base)
|
|
1725
1750
|
|
|
1726
1751
|
def _build_concept(self, base: Concept) -> BuildConcept:
|
|
1752
|
+
try:
|
|
1753
|
+
return self.__build_concept(base)
|
|
1754
|
+
except RecursionError as e:
|
|
1755
|
+
raise RecursionError(
|
|
1756
|
+
f"Recursion error building concept {base.address}. This is likely due to a circular reference."
|
|
1757
|
+
) from e
|
|
1758
|
+
|
|
1759
|
+
def __build_concept(self, base: Concept) -> BuildConcept:
|
|
1727
1760
|
# TODO: if we are using parameters, wrap it in a new model and use that in rendering
|
|
1728
1761
|
if base.address in self.local_concepts:
|
|
1729
1762
|
return self.local_concepts[base.address]
|
|
@@ -1953,6 +1986,28 @@ class Factory:
|
|
|
1953
1986
|
def _build_align_clause(self, base: AlignClause) -> BuildAlignClause:
|
|
1954
1987
|
return BuildAlignClause(items=[self._build_align_item(x) for x in base.items])
|
|
1955
1988
|
|
|
1989
|
+
@build.register
|
|
1990
|
+
def _(self, base: DeriveItem) -> BuildDeriveItem:
|
|
1991
|
+
return self._build_derive_item(base)
|
|
1992
|
+
|
|
1993
|
+
def _build_derive_item(self, base: DeriveItem) -> BuildDeriveItem:
|
|
1994
|
+
expr: Concept | FuncArgs = base.expr
|
|
1995
|
+
validation = requires_concept_nesting(expr)
|
|
1996
|
+
if validation:
|
|
1997
|
+
expr, _ = self.instantiate_concept(validation)
|
|
1998
|
+
return BuildDeriveItem(
|
|
1999
|
+
expr=self.build(expr),
|
|
2000
|
+
name=base.name,
|
|
2001
|
+
namespace=base.namespace,
|
|
2002
|
+
)
|
|
2003
|
+
|
|
2004
|
+
@build.register
|
|
2005
|
+
def _(self, base: DeriveClause) -> BuildDeriveClause:
|
|
2006
|
+
return self._build_derive_clause(base)
|
|
2007
|
+
|
|
2008
|
+
def _build_derive_clause(self, base: DeriveClause) -> BuildDeriveClause:
|
|
2009
|
+
return BuildDeriveClause(items=[self.build(x) for x in base.items])
|
|
2010
|
+
|
|
1956
2011
|
@build.register
|
|
1957
2012
|
def _(self, base: RowsetItem) -> BuildRowsetItem:
|
|
1958
2013
|
return self._build_rowset_item(base)
|
|
@@ -2002,6 +2057,13 @@ class Factory:
|
|
|
2002
2057
|
def _build_tuple_wrapper(self, base: TupleWrapper) -> TupleWrapper:
|
|
2003
2058
|
return TupleWrapper(val=[self.build(x) for x in base.val], type=base.type)
|
|
2004
2059
|
|
|
2060
|
+
@build.register
|
|
2061
|
+
def _(self, base: ListWrapper) -> ListWrapper:
|
|
2062
|
+
return self._build_list_wrapper(base)
|
|
2063
|
+
|
|
2064
|
+
def _build_list_wrapper(self, base: ListWrapper) -> ListWrapper:
|
|
2065
|
+
return ListWrapper([self.build(x) for x in base], type=base.type)
|
|
2066
|
+
|
|
2005
2067
|
@build.register
|
|
2006
2068
|
def _(self, base: FilterItem) -> BuildFilterItem:
|
|
2007
2069
|
return self._build_filter_item(base)
|
|
@@ -2147,6 +2209,7 @@ class Factory:
|
|
|
2147
2209
|
selects=base.selects,
|
|
2148
2210
|
grain=final_grain,
|
|
2149
2211
|
align=factory.build(base.align),
|
|
2212
|
+
derive=factory.build(base.derive) if base.derive else None,
|
|
2150
2213
|
# self.align.with_select_context(
|
|
2151
2214
|
# local_build_cache, self.grain, environment
|
|
2152
2215
|
# ),
|
|
@@ -413,7 +413,8 @@ class Environment(BaseModel):
|
|
|
413
413
|
self.imports[alias].append(imp_stm)
|
|
414
414
|
# we can't exit early
|
|
415
415
|
# as there may be new concepts
|
|
416
|
-
|
|
416
|
+
iteration: list[tuple[str, Concept]] = list(source.concepts.items())
|
|
417
|
+
for k, concept in iteration:
|
|
417
418
|
# skip internal namespace
|
|
418
419
|
if INTERNAL_NAMESPACE in concept.address:
|
|
419
420
|
continue
|
trilogy/core/optimization.py
CHANGED
|
@@ -228,7 +228,8 @@ def optimize_ctes(
|
|
|
228
228
|
REGISTERED_RULES.append(PredicatePushdown())
|
|
229
229
|
if CONFIG.optimizations.predicate_pushdown:
|
|
230
230
|
REGISTERED_RULES.append(PredicatePushdownRemove())
|
|
231
|
-
|
|
231
|
+
if CONFIG.optimizations.hide_unused_concepts:
|
|
232
|
+
REGISTERED_RULES.append(HideUnusedConcepts())
|
|
232
233
|
for rule in REGISTERED_RULES:
|
|
233
234
|
loops = 0
|
|
234
235
|
complete = False
|
|
@@ -242,7 +243,7 @@ def optimize_ctes(
|
|
|
242
243
|
actions_taken = actions_taken or opt
|
|
243
244
|
complete = not actions_taken
|
|
244
245
|
loops += 1
|
|
245
|
-
|
|
246
|
+
input = reorder_ctes(filter_irrelevant_ctes(input, root_cte))
|
|
246
247
|
logger.info(
|
|
247
248
|
f"[Optimization] Finished checking for {type(rule).__name__} after {loops} loop(s)"
|
|
248
249
|
)
|
|
@@ -39,11 +39,7 @@ class HideUnusedConcepts(OptimizationRule):
|
|
|
39
39
|
self.log(
|
|
40
40
|
f"Hiding unused concepts {[x.address for x in add_to_hidden]} from {cte.name} (used: {used}, all: {[x.address for x in cte.output_columns]})"
|
|
41
41
|
)
|
|
42
|
-
candidates = [
|
|
43
|
-
x.address
|
|
44
|
-
for x in cte.output_columns
|
|
45
|
-
if x.address not in used and x.address not in cte.hidden_concepts
|
|
46
|
-
]
|
|
42
|
+
candidates = [x.address for x in cte.output_columns if x.address not in used]
|
|
47
43
|
if len(candidates) == len(set([x.address for x in cte.output_columns])):
|
|
48
44
|
# pop one out
|
|
49
45
|
candidates.pop()
|
|
@@ -306,7 +306,12 @@ def evaluate_loop_conditions(
|
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
def check_for_early_exit(
|
|
309
|
-
complete
|
|
309
|
+
complete: ValidationResult,
|
|
310
|
+
found: set[str],
|
|
311
|
+
partial: set[str],
|
|
312
|
+
missing: set[str],
|
|
313
|
+
context: LoopContext,
|
|
314
|
+
priority_concept: BuildConcept,
|
|
310
315
|
) -> bool:
|
|
311
316
|
if complete == ValidationResult.INCOMPLETE_CONDITION:
|
|
312
317
|
cond_dict = {str(node): node.preexisting_conditions for node in context.stack}
|
|
@@ -331,8 +336,18 @@ def check_for_early_exit(
|
|
|
331
336
|
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as we have attempted all nodes"
|
|
332
337
|
)
|
|
333
338
|
return True
|
|
339
|
+
elif all(
|
|
340
|
+
[
|
|
341
|
+
x.address in found and x.address not in partial
|
|
342
|
+
for x in context.mandatory_list
|
|
343
|
+
]
|
|
344
|
+
):
|
|
345
|
+
logger.info(
|
|
346
|
+
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as we have found all mandatory nodes without partials"
|
|
347
|
+
)
|
|
348
|
+
return True
|
|
334
349
|
logger.info(
|
|
335
|
-
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {context.attempted}
|
|
350
|
+
f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {context.attempted} of total {len(context.mandatory_list)}."
|
|
336
351
|
)
|
|
337
352
|
else:
|
|
338
353
|
logger.info(
|
|
@@ -436,6 +451,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
|
|
|
436
451
|
context.original_mandatory,
|
|
437
452
|
context.environment,
|
|
438
453
|
non_virtual_difference_values,
|
|
454
|
+
depth=context.depth,
|
|
439
455
|
)
|
|
440
456
|
|
|
441
457
|
return group_if_required_v2(
|
|
@@ -443,6 +459,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
|
|
|
443
459
|
context.original_mandatory,
|
|
444
460
|
context.environment,
|
|
445
461
|
non_virtual_difference_values,
|
|
462
|
+
depth=context.depth,
|
|
446
463
|
)
|
|
447
464
|
|
|
448
465
|
|
|
@@ -466,6 +483,7 @@ def _search_concepts(
|
|
|
466
483
|
conditions=conditions,
|
|
467
484
|
)
|
|
468
485
|
|
|
486
|
+
# if we get a can
|
|
469
487
|
if candidate:
|
|
470
488
|
return candidate
|
|
471
489
|
context = initialize_loop_context(
|
|
@@ -477,13 +495,16 @@ def _search_concepts(
|
|
|
477
495
|
accept_partial=accept_partial,
|
|
478
496
|
conditions=conditions,
|
|
479
497
|
)
|
|
480
|
-
|
|
498
|
+
partial: set[str] = set()
|
|
499
|
+
virtual: set[str] = set()
|
|
500
|
+
complete = ValidationResult.INCOMPLETE
|
|
481
501
|
while context.incomplete:
|
|
482
502
|
|
|
483
503
|
priority_concept = get_priority_concept(
|
|
484
504
|
context.mandatory_list,
|
|
485
505
|
context.attempted,
|
|
486
506
|
found_concepts=context.found,
|
|
507
|
+
partial_concepts=partial,
|
|
487
508
|
depth=depth,
|
|
488
509
|
)
|
|
489
510
|
|
|
@@ -538,7 +559,7 @@ def _search_concepts(
|
|
|
538
559
|
# assign
|
|
539
560
|
context.found = found_c
|
|
540
561
|
early_exit = check_for_early_exit(
|
|
541
|
-
complete, partial, missing_c, context, priority_concept
|
|
562
|
+
complete, found_c, partial, missing_c, context, priority_concept
|
|
542
563
|
)
|
|
543
564
|
if early_exit:
|
|
544
565
|
break
|
|
@@ -608,4 +629,4 @@ def source_query_concepts(
|
|
|
608
629
|
logger.info(
|
|
609
630
|
f"{depth_to_prefix(0)}{LOGGER_PREFIX} final concepts are {[x.address for x in final]}"
|
|
610
631
|
)
|
|
611
|
-
return group_if_required_v2(root, output_concepts, environment)
|
|
632
|
+
return group_if_required_v2(root, output_concepts, environment, depth=0)
|
|
@@ -187,7 +187,7 @@ def _generate_aggregate_node(ctx: NodeGenerationContext) -> StrategyNode | None:
|
|
|
187
187
|
|
|
188
188
|
logger.info(
|
|
189
189
|
f"{depth_to_prefix(ctx.depth)}{LOGGER_PREFIX} "
|
|
190
|
-
f"for {ctx.concept.address}, generating aggregate node with {agg_optional}"
|
|
190
|
+
f"for {ctx.concept.address}, generating aggregate node with optional {agg_optional}"
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
return gen_group_node(
|
|
@@ -441,7 +441,7 @@ def generate_node(
|
|
|
441
441
|
depth: int,
|
|
442
442
|
source_concepts: SearchConceptsType,
|
|
443
443
|
history: History,
|
|
444
|
-
accept_partial: bool
|
|
444
|
+
accept_partial: bool,
|
|
445
445
|
conditions: BuildWhereClause | None = None,
|
|
446
446
|
) -> StrategyNode | None:
|
|
447
447
|
|
|
@@ -184,10 +184,14 @@ def group_if_required_v2(
|
|
|
184
184
|
final: List[BuildConcept],
|
|
185
185
|
environment: BuildEnvironment,
|
|
186
186
|
where_injected: set[str] | None = None,
|
|
187
|
+
depth: int = 0,
|
|
187
188
|
):
|
|
188
189
|
where_injected = where_injected or set()
|
|
189
190
|
required = check_if_group_required(
|
|
190
|
-
downstream_concepts=final,
|
|
191
|
+
downstream_concepts=final,
|
|
192
|
+
parents=[root.resolve()],
|
|
193
|
+
environment=environment,
|
|
194
|
+
depth=depth,
|
|
191
195
|
)
|
|
192
196
|
targets = [
|
|
193
197
|
x
|
|
@@ -258,6 +262,7 @@ def get_priority_concept(
|
|
|
258
262
|
all_concepts: List[BuildConcept],
|
|
259
263
|
attempted_addresses: set[str],
|
|
260
264
|
found_concepts: set[str],
|
|
265
|
+
partial_concepts: set[str],
|
|
261
266
|
depth: int,
|
|
262
267
|
) -> BuildConcept:
|
|
263
268
|
# optimized search for missing concepts
|
|
@@ -265,13 +270,15 @@ def get_priority_concept(
|
|
|
265
270
|
[
|
|
266
271
|
c
|
|
267
272
|
for c in all_concepts
|
|
268
|
-
if c.address not in attempted_addresses
|
|
273
|
+
if c.address not in attempted_addresses
|
|
274
|
+
and (c.address not in found_concepts or c.address in partial_concepts)
|
|
269
275
|
],
|
|
270
276
|
key=lambda x: x.address,
|
|
271
277
|
)
|
|
272
278
|
# sometimes we need to scan intermediate concepts to get merge keys or filter keys,
|
|
273
279
|
# so do an exhaustive search
|
|
274
|
-
# pass_two = [c for c in all_concepts
|
|
280
|
+
# pass_two = [c for c in all_concepts if c.address not in attempted_addresses]
|
|
281
|
+
|
|
275
282
|
for remaining_concept in (pass_one,):
|
|
276
283
|
priority = (
|
|
277
284
|
# then multiselects to remove them from scope
|
|
@@ -333,5 +340,5 @@ def get_priority_concept(
|
|
|
333
340
|
if final:
|
|
334
341
|
return final[0]
|
|
335
342
|
raise ValueError(
|
|
336
|
-
f"Cannot resolve query. No remaining priority concepts, have attempted {attempted_addresses}"
|
|
343
|
+
f"Cannot resolve query. No remaining priority concepts, have attempted {attempted_addresses} out of {all_concepts} with found {found_concepts}"
|
|
337
344
|
)
|
|
@@ -7,7 +7,7 @@ from trilogy.core.models.build_environment import BuildEnvironment
|
|
|
7
7
|
from trilogy.core.processing.node_generators.common import (
|
|
8
8
|
resolve_function_parent_concepts,
|
|
9
9
|
)
|
|
10
|
-
from trilogy.core.processing.nodes import History, StrategyNode
|
|
10
|
+
from trilogy.core.processing.nodes import ConstantNode, History, StrategyNode
|
|
11
11
|
from trilogy.utility import unique
|
|
12
12
|
|
|
13
13
|
LOGGER_PREFIX = "[GEN_BASIC_NODE]"
|
|
@@ -51,11 +51,14 @@ def gen_basic_node(
|
|
|
51
51
|
)
|
|
52
52
|
synonyms: list[BuildConcept] = []
|
|
53
53
|
ignored_optional: set[str] = set()
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
# when we are getting an attribute, if there is anything else
|
|
56
56
|
# that is an attribute of the same struct in local optional
|
|
57
57
|
# select that value for discovery as well
|
|
58
|
-
if
|
|
58
|
+
if (
|
|
59
|
+
isinstance(concept.lineage, BuildFunction)
|
|
60
|
+
and concept.lineage.operator == FunctionType.ATTR_ACCESS
|
|
61
|
+
):
|
|
59
62
|
logger.info(
|
|
60
63
|
f"{depth_prefix}{LOGGER_PREFIX} checking for synonyms for attribute access"
|
|
61
64
|
)
|
|
@@ -106,20 +109,28 @@ def gen_basic_node(
|
|
|
106
109
|
logger.info(
|
|
107
110
|
f"{depth_prefix}{LOGGER_PREFIX} Fetching parents {[x.address for x in all_parents]}"
|
|
108
111
|
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
if all_parents:
|
|
113
|
+
parent_node: StrategyNode | None = source_concepts(
|
|
114
|
+
mandatory_list=all_parents,
|
|
115
|
+
environment=environment,
|
|
116
|
+
g=g,
|
|
117
|
+
depth=depth + 1,
|
|
118
|
+
history=history,
|
|
119
|
+
conditions=conditions,
|
|
120
|
+
)
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
if not parent_node:
|
|
123
|
+
logger.info(
|
|
124
|
+
f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
|
|
125
|
+
)
|
|
126
|
+
return None
|
|
127
|
+
else:
|
|
128
|
+
return ConstantNode(
|
|
129
|
+
input_concepts=[],
|
|
130
|
+
output_concepts=[concept],
|
|
131
|
+
environment=environment,
|
|
132
|
+
depth=depth,
|
|
121
133
|
)
|
|
122
|
-
return None
|
|
123
134
|
if parent_node.source_type != SourceType.CONSTANT:
|
|
124
135
|
parent_node.source_type = SourceType.BASIC
|
|
125
136
|
parent_node.add_output_concept(concept)
|
|
@@ -4,6 +4,7 @@ from typing import Callable, List, Tuple
|
|
|
4
4
|
from trilogy.core.enums import Derivation, Purpose
|
|
5
5
|
from trilogy.core.models.build import (
|
|
6
6
|
BuildAggregateWrapper,
|
|
7
|
+
BuildComparison,
|
|
7
8
|
BuildConcept,
|
|
8
9
|
BuildFilterItem,
|
|
9
10
|
BuildFunction,
|
|
@@ -26,7 +27,9 @@ FUNCTION_TYPES = (BuildFunction,)
|
|
|
26
27
|
def resolve_function_parent_concepts(
|
|
27
28
|
concept: BuildConcept, environment: BuildEnvironment
|
|
28
29
|
) -> List[BuildConcept]:
|
|
29
|
-
if not isinstance(
|
|
30
|
+
if not isinstance(
|
|
31
|
+
concept.lineage, (*FUNCTION_TYPES, *AGGREGATE_TYPES, BuildComparison)
|
|
32
|
+
):
|
|
30
33
|
raise ValueError(
|
|
31
34
|
f"Concept {concept} lineage is not function or aggregate, is {type(concept.lineage)}"
|
|
32
35
|
)
|
|
@@ -96,6 +96,8 @@ def build_parent_concepts(
|
|
|
96
96
|
continue
|
|
97
97
|
elif global_filter_is_local_filter:
|
|
98
98
|
same_filter_optional.append(x)
|
|
99
|
+
# also append it to the parent row concepts
|
|
100
|
+
parent_row_concepts.append(x)
|
|
99
101
|
|
|
100
102
|
# sometimes, it's okay to include other local optional above the filter
|
|
101
103
|
# in case it is, prep our list
|
|
@@ -204,11 +206,16 @@ def gen_filter_node(
|
|
|
204
206
|
f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
|
|
205
207
|
)
|
|
206
208
|
return None
|
|
209
|
+
else:
|
|
210
|
+
logger.info(
|
|
211
|
+
f"{padding(depth)}{LOGGER_PREFIX} filter node has row parents {[x.address for x in parent_row_concepts]} from node with output [{[x.address for x in row_parent.output_concepts]}] partial {row_parent.partial_concepts}"
|
|
212
|
+
)
|
|
207
213
|
if global_filter_is_local_filter:
|
|
208
214
|
logger.info(
|
|
209
215
|
f"{padding(depth)}{LOGGER_PREFIX} filter node conditions match global conditions adding row parent {row_parent.output_concepts} with condition {where.conditional}"
|
|
210
216
|
)
|
|
211
217
|
row_parent.add_parents(core_parent_nodes)
|
|
218
|
+
# all local optional will be in the parent already, so we can set outputs
|
|
212
219
|
row_parent.set_output_concepts([concept] + local_optional)
|
|
213
220
|
return row_parent
|
|
214
221
|
if optimized_pushdown:
|
|
@@ -157,19 +157,19 @@ def gen_multiselect_node(
|
|
|
157
157
|
possible_joins = concept_to_relevant_joins(additional_relevant)
|
|
158
158
|
if not local_optional:
|
|
159
159
|
logger.info(
|
|
160
|
-
f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for
|
|
160
|
+
f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for multiselect node; exiting early"
|
|
161
161
|
)
|
|
162
162
|
return node
|
|
163
163
|
if not possible_joins:
|
|
164
164
|
logger.info(
|
|
165
|
-
f"{padding(depth)}{LOGGER_PREFIX} no possible joins for
|
|
165
|
+
f"{padding(depth)}{LOGGER_PREFIX} no possible joins for multiselect node; exiting early"
|
|
166
166
|
)
|
|
167
167
|
return node
|
|
168
168
|
if all(
|
|
169
169
|
[x.address in [y.address for y in node.output_concepts] for x in local_optional]
|
|
170
170
|
):
|
|
171
171
|
logger.info(
|
|
172
|
-
f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base
|
|
172
|
+
f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base multiselect node; exiting early"
|
|
173
173
|
)
|
|
174
174
|
return node
|
|
175
175
|
logger.info(
|
|
@@ -9,6 +9,7 @@ from trilogy.core.models.build import (
|
|
|
9
9
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
10
10
|
from trilogy.core.processing.nodes import (
|
|
11
11
|
History,
|
|
12
|
+
MergeNode,
|
|
12
13
|
StrategyNode,
|
|
13
14
|
UnnestNode,
|
|
14
15
|
WhereSafetyNode,
|
|
@@ -18,6 +19,32 @@ from trilogy.core.processing.utility import padding
|
|
|
18
19
|
LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def get_pseudonym_parents(
|
|
23
|
+
concept: BuildConcept,
|
|
24
|
+
local_optional: List[BuildConcept],
|
|
25
|
+
source_concepts,
|
|
26
|
+
environment: BuildEnvironment,
|
|
27
|
+
g,
|
|
28
|
+
depth,
|
|
29
|
+
history,
|
|
30
|
+
conditions,
|
|
31
|
+
) -> List[StrategyNode]:
|
|
32
|
+
for x in concept.pseudonyms:
|
|
33
|
+
attempt = source_concepts(
|
|
34
|
+
mandatory_list=[environment.alias_origin_lookup[x]] + local_optional,
|
|
35
|
+
environment=environment,
|
|
36
|
+
g=g,
|
|
37
|
+
depth=depth + 1,
|
|
38
|
+
history=history,
|
|
39
|
+
conditions=conditions,
|
|
40
|
+
accept_partial=True,
|
|
41
|
+
)
|
|
42
|
+
if not attempt:
|
|
43
|
+
continue
|
|
44
|
+
return [attempt]
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
|
|
21
48
|
def gen_unnest_node(
|
|
22
49
|
concept: BuildConcept,
|
|
23
50
|
local_optional: List[BuildConcept],
|
|
@@ -29,14 +56,34 @@ def gen_unnest_node(
|
|
|
29
56
|
conditions: BuildWhereClause | None = None,
|
|
30
57
|
) -> StrategyNode | None:
|
|
31
58
|
arguments = []
|
|
59
|
+
join_nodes: list[StrategyNode] = []
|
|
32
60
|
depth_prefix = "\t" * depth
|
|
33
61
|
if isinstance(concept.lineage, BuildFunction):
|
|
34
62
|
arguments = concept.lineage.concept_arguments
|
|
63
|
+
search_optional = local_optional
|
|
64
|
+
if (not arguments) and (local_optional and concept.pseudonyms):
|
|
65
|
+
logger.info(
|
|
66
|
+
f"{padding(depth)}{LOGGER_PREFIX} unnest node for {concept} has no parents; creating solo unnest node"
|
|
67
|
+
)
|
|
68
|
+
join_nodes += get_pseudonym_parents(
|
|
69
|
+
concept,
|
|
70
|
+
local_optional,
|
|
71
|
+
source_concepts,
|
|
72
|
+
environment,
|
|
73
|
+
g,
|
|
74
|
+
depth,
|
|
75
|
+
history,
|
|
76
|
+
conditions,
|
|
77
|
+
)
|
|
78
|
+
logger.info(
|
|
79
|
+
f"{padding(depth)}{LOGGER_PREFIX} unnest node for {concept} got join nodes {join_nodes}"
|
|
80
|
+
)
|
|
81
|
+
search_optional = []
|
|
35
82
|
|
|
36
|
-
equivalent_optional = [x for x in
|
|
83
|
+
equivalent_optional = [x for x in search_optional if x.lineage == concept.lineage]
|
|
37
84
|
|
|
38
85
|
non_equivalent_optional = [
|
|
39
|
-
x for x in
|
|
86
|
+
x for x in search_optional if x not in equivalent_optional
|
|
40
87
|
]
|
|
41
88
|
all_parents = arguments + non_equivalent_optional
|
|
42
89
|
logger.info(
|
|
@@ -44,7 +91,8 @@ def gen_unnest_node(
|
|
|
44
91
|
)
|
|
45
92
|
local_conditions = False
|
|
46
93
|
expected_outputs = [concept] + local_optional
|
|
47
|
-
|
|
94
|
+
parent: StrategyNode | None = None
|
|
95
|
+
if arguments or search_optional:
|
|
48
96
|
parent = source_concepts(
|
|
49
97
|
mandatory_list=all_parents,
|
|
50
98
|
environment=environment,
|
|
@@ -86,14 +134,37 @@ def gen_unnest_node(
|
|
|
86
134
|
base = UnnestNode(
|
|
87
135
|
unnest_concepts=[concept] + equivalent_optional,
|
|
88
136
|
input_concepts=arguments + non_equivalent_optional,
|
|
89
|
-
output_concepts=[concept] +
|
|
137
|
+
output_concepts=[concept] + search_optional,
|
|
90
138
|
environment=environment,
|
|
91
139
|
parents=([parent] if parent else []),
|
|
92
140
|
)
|
|
141
|
+
|
|
142
|
+
conditional = conditions.conditional if conditions else None
|
|
143
|
+
if join_nodes:
|
|
144
|
+
logger.info(
|
|
145
|
+
f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} needs to merge with join nodes {join_nodes}"
|
|
146
|
+
)
|
|
147
|
+
for x in join_nodes:
|
|
148
|
+
logger.info(
|
|
149
|
+
f"{depth_prefix}{LOGGER_PREFIX} join node {x} with partial {x.partial_concepts}"
|
|
150
|
+
)
|
|
151
|
+
pseudonyms = [
|
|
152
|
+
environment.alias_origin_lookup[p] for p in concept.pseudonyms
|
|
153
|
+
]
|
|
154
|
+
x.add_partial_concepts(pseudonyms)
|
|
155
|
+
return MergeNode(
|
|
156
|
+
input_concepts=base.output_concepts
|
|
157
|
+
+ [j for n in join_nodes for j in n.output_concepts],
|
|
158
|
+
output_concepts=[concept] + local_optional,
|
|
159
|
+
environment=environment,
|
|
160
|
+
parents=[base] + join_nodes,
|
|
161
|
+
conditions=conditional if local_conditions is True else None,
|
|
162
|
+
preexisting_conditions=(
|
|
163
|
+
conditional if conditional and local_conditions is False else None
|
|
164
|
+
),
|
|
165
|
+
)
|
|
93
166
|
# we need to sometimes nest an unnest node,
|
|
94
167
|
# as unnest operations are not valid in all situations
|
|
95
|
-
# TODO: inline this node when we can detect it's safe
|
|
96
|
-
conditional = conditions.conditional if conditions else None
|
|
97
168
|
new = WhereSafetyNode(
|
|
98
169
|
input_concepts=base.output_concepts,
|
|
99
170
|
output_concepts=base.output_concepts,
|
|
@@ -21,6 +21,7 @@ from trilogy.core.models.author import (
|
|
|
21
21
|
Concept,
|
|
22
22
|
ConceptRef,
|
|
23
23
|
CustomType,
|
|
24
|
+
DeriveClause,
|
|
24
25
|
Expr,
|
|
25
26
|
FilterItem,
|
|
26
27
|
Function,
|
|
@@ -197,7 +198,7 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
|
|
|
197
198
|
for x in self.where_clause.concept_arguments:
|
|
198
199
|
if isinstance(x, UndefinedConcept):
|
|
199
200
|
validate = environment.concepts.get(x.address)
|
|
200
|
-
if validate:
|
|
201
|
+
if validate and self.where_clause:
|
|
201
202
|
self.where_clause = (
|
|
202
203
|
self.where_clause.with_reference_replacement(
|
|
203
204
|
x.address, validate.reference
|
|
@@ -352,11 +353,13 @@ class MultiSelectStatement(HasUUID, SelectTypeMixin, BaseModel):
|
|
|
352
353
|
local_concepts: Annotated[
|
|
353
354
|
EnvironmentConceptDict, PlainValidator(validate_concepts)
|
|
354
355
|
] = Field(default_factory=EnvironmentConceptDict)
|
|
356
|
+
derive: DeriveClause | None = None
|
|
355
357
|
|
|
356
358
|
def as_lineage(self, environment: Environment):
|
|
357
359
|
return MultiSelectLineage(
|
|
358
360
|
selects=[x.as_lineage(environment) for x in self.selects],
|
|
359
361
|
align=self.align,
|
|
362
|
+
derive=self.derive,
|
|
360
363
|
namespace=self.namespace,
|
|
361
364
|
# derived_concepts = self.derived_concepts,
|
|
362
365
|
limit=self.limit,
|