pytrilogy 0.0.2.3__tar.gz → 0.0.2.5__tar.gz
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.3/pytrilogy.egg-info → pytrilogy-0.0.2.5}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5/pytrilogy.egg-info}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_parsing.py +41 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_where_clause.py +55 -2
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/enums.py +1 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/functions.py +9 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/models.py +28 -7
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/inline_datasource.py +6 -2
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/predicate_pushdown.py +3 -44
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/concept_strategies_v3.py +1 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/common.py +3 -3
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/filter_node.py +20 -16
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/group_node.py +28 -2
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/utility.py +46 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/query_processor.py +7 -2
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/base.py +34 -8
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/bigquery.py +3 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/duckdb.py +5 -5
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/postgres.py +3 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/presto.py +3 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/snowflake.py +3 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/sql_server.py +3 -1
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/common.py +1 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/parse_engine.py +74 -2
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/trilogy.lark +7 -3
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/README.md +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pyproject.toml +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/SOURCES.txt +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/setup.cfg +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/setup.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_models.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_select.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/constants.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/environment_helpers.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/graph_models.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/optimizations/inline_constant.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/base_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/merge_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/common.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/executor.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/parsing/render.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/utility.py +0 -0
|
@@ -407,3 +407,44 @@ property id.labels map<string, int>;
|
|
|
407
407
|
)
|
|
408
408
|
assert env.concepts["labels"].datatype.key_type == DataType.STRING
|
|
409
409
|
assert env.concepts["labels"].datatype.value_type == DataType.INTEGER
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_map_string_access():
|
|
413
|
+
env, parsed = parse_text(
|
|
414
|
+
"""
|
|
415
|
+
const labels <- { 'a': 1, 'b': 2, 'c': 3 };
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
select
|
|
419
|
+
labels['a'] as label_a,
|
|
420
|
+
;
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
)
|
|
424
|
+
assert env.concepts["labels"].datatype.key_type == DataType.STRING
|
|
425
|
+
assert env.concepts["labels"].datatype.value_type == DataType.INTEGER
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def test_struct_attr_access():
|
|
429
|
+
|
|
430
|
+
text = """
|
|
431
|
+
const labels <- struct(a=1, b=2, c=3);
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
select
|
|
435
|
+
labels,
|
|
436
|
+
labels.a as label_a
|
|
437
|
+
;
|
|
438
|
+
|
|
439
|
+
"""
|
|
440
|
+
env, parsed = parse_text(text)
|
|
441
|
+
assert env.concepts["labels"].datatype.fields_map["a"] == 1
|
|
442
|
+
|
|
443
|
+
assert env.concepts["labels.a"].concept_arguments[0].name == "labels"
|
|
444
|
+
|
|
445
|
+
results = Dialects.DUCK_DB.default_executor().execute_text(text)[0]
|
|
446
|
+
|
|
447
|
+
assert results.fetchall()[0] == (
|
|
448
|
+
{"a": 1, "b": 2, "c": 3},
|
|
449
|
+
1,
|
|
450
|
+
)
|
|
@@ -3,6 +3,7 @@ from trilogy.core.models import SelectStatement, Grain, Parenthetical
|
|
|
3
3
|
from trilogy.core.query_processor import process_query
|
|
4
4
|
from trilogy.dialect.base import BaseDialect
|
|
5
5
|
from trilogy.parser import parse
|
|
6
|
+
from trilogy.core.processing.utility import is_scalar_condition
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def test_select_where(test_environment):
|
|
@@ -44,6 +45,8 @@ where
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def test_select_where_agg(test_environment):
|
|
48
|
+
from trilogy.hooks.query_debugger import DebuggingHook
|
|
49
|
+
|
|
47
50
|
declarations = """
|
|
48
51
|
property my_favorite_order_revenue <- filter revenue where order_id in (1,2,3);
|
|
49
52
|
|
|
@@ -54,10 +57,15 @@ select
|
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
"""
|
|
57
|
-
env, parsed = parse(
|
|
60
|
+
env, parsed = parse(
|
|
61
|
+
declarations,
|
|
62
|
+
environment=test_environment,
|
|
63
|
+
)
|
|
58
64
|
select: SelectStatement = parsed[-1]
|
|
59
65
|
|
|
60
|
-
BaseDialect().compile_statement(
|
|
66
|
+
BaseDialect().compile_statement(
|
|
67
|
+
process_query(test_environment, select, hooks=[DebuggingHook()])
|
|
68
|
+
)
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
def test_select_where_joins(test_environment):
|
|
@@ -205,3 +213,48 @@ where
|
|
|
205
213
|
|
|
206
214
|
"""
|
|
207
215
|
env, parsed = parse(declarations, environment=test_environment)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_where_scalar(test_environment):
|
|
219
|
+
declarations = """
|
|
220
|
+
select
|
|
221
|
+
category_name
|
|
222
|
+
where
|
|
223
|
+
count(order_id) > 1
|
|
224
|
+
;
|
|
225
|
+
"""
|
|
226
|
+
env, parsed = parse(declarations, environment=test_environment)
|
|
227
|
+
select: SelectStatement = parsed[-1]
|
|
228
|
+
|
|
229
|
+
assert is_scalar_condition(select.where_clause.conditional) is False
|
|
230
|
+
_ = BaseDialect().compile_statement(process_query(test_environment, select))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_case_where(test_environment):
|
|
234
|
+
from trilogy.hooks.query_debugger import DebuggingHook
|
|
235
|
+
|
|
236
|
+
declarations = """property order_id_even_name <- CASE
|
|
237
|
+
when order_id %2 = 0 then 'even'
|
|
238
|
+
else 'odd'
|
|
239
|
+
END;
|
|
240
|
+
|
|
241
|
+
const test <- 1;
|
|
242
|
+
|
|
243
|
+
auto order_even_class_filter <- filter category_id where order_id_even_name = 'even' and 1= test;
|
|
244
|
+
|
|
245
|
+
select
|
|
246
|
+
category_id,
|
|
247
|
+
category_name
|
|
248
|
+
where
|
|
249
|
+
category_name like '%abc%' and category_id not in order_even_class_filter
|
|
250
|
+
and category_id = test
|
|
251
|
+
;"""
|
|
252
|
+
env, parsed = parse(declarations, environment=test_environment)
|
|
253
|
+
select: SelectStatement = parsed[-1]
|
|
254
|
+
|
|
255
|
+
query = BaseDialect().compile_statement(
|
|
256
|
+
process_query(test_environment, select, hooks=[DebuggingHook()])
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# check to make sure our subselect is well-formed
|
|
260
|
+
assert "`category_id` not in (select" in query, query
|
|
@@ -251,11 +251,19 @@ def MapAccess(args: list[Concept]):
|
|
|
251
251
|
)
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
def get_attr_datatype(
|
|
255
|
+
arg: Concept, lookup
|
|
256
|
+
) -> DataType | ListType | StructType | MapType | NumericType:
|
|
257
|
+
if isinstance(arg.datatype, StructType):
|
|
258
|
+
return arg_to_datatype(arg.datatype.fields_map[lookup])
|
|
259
|
+
return arg.datatype
|
|
260
|
+
|
|
261
|
+
|
|
254
262
|
def AttrAccess(args: list[Concept]):
|
|
255
263
|
return Function(
|
|
256
264
|
operator=FunctionType.ATTR_ACCESS,
|
|
257
265
|
arguments=args,
|
|
258
|
-
output_datatype=args[0]
|
|
266
|
+
output_datatype=get_attr_datatype(args[0], args[1]), # type: ignore
|
|
259
267
|
output_purpose=Purpose.PROPERTY,
|
|
260
268
|
valid_inputs=[
|
|
261
269
|
{DataType.STRUCT},
|
|
@@ -296,7 +296,7 @@ class MapType(BaseModel):
|
|
|
296
296
|
|
|
297
297
|
class StructType(BaseModel):
|
|
298
298
|
fields: List[ALL_TYPES]
|
|
299
|
-
fields_map: Dict[str, Concept] = Field(default_factory=dict)
|
|
299
|
+
fields_map: Dict[str, Concept | int | float | str] = Field(default_factory=dict)
|
|
300
300
|
|
|
301
301
|
@property
|
|
302
302
|
def data_type(self):
|
|
@@ -419,6 +419,23 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
419
419
|
def __hash__(self):
|
|
420
420
|
return hash(str(self))
|
|
421
421
|
|
|
422
|
+
@property
|
|
423
|
+
def is_aggregate(self):
|
|
424
|
+
if (
|
|
425
|
+
self.lineage
|
|
426
|
+
and isinstance(self.lineage, Function)
|
|
427
|
+
and self.lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
428
|
+
):
|
|
429
|
+
return True
|
|
430
|
+
if (
|
|
431
|
+
self.lineage
|
|
432
|
+
and isinstance(self.lineage, AggregateWrapper)
|
|
433
|
+
and self.lineage.function.operator
|
|
434
|
+
in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
435
|
+
):
|
|
436
|
+
return True
|
|
437
|
+
return False
|
|
438
|
+
|
|
422
439
|
def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
|
|
423
440
|
if self.address == source.address:
|
|
424
441
|
new = target.with_grain(self.grain.with_merge(source, target, modifiers))
|
|
@@ -2416,7 +2433,13 @@ class CTE(BaseModel):
|
|
|
2416
2433
|
# if we've entirely removed the need to join to someplace to get the concept
|
|
2417
2434
|
# drop the join as well.
|
|
2418
2435
|
for removed_cte in removed:
|
|
2419
|
-
still_required = any(
|
|
2436
|
+
still_required = any(
|
|
2437
|
+
[
|
|
2438
|
+
removed_cte in x
|
|
2439
|
+
for x in self.source_map.values()
|
|
2440
|
+
or self.existence_source_map.values()
|
|
2441
|
+
]
|
|
2442
|
+
)
|
|
2420
2443
|
if not still_required:
|
|
2421
2444
|
self.joins = [
|
|
2422
2445
|
join
|
|
@@ -2434,6 +2457,7 @@ class CTE(BaseModel):
|
|
|
2434
2457
|
candidates = [x.name for x in self.parent_ctes]
|
|
2435
2458
|
self.base_name_override = candidates[0] if candidates else None
|
|
2436
2459
|
self.base_alias_override = candidates[0] if candidates else None
|
|
2460
|
+
return True
|
|
2437
2461
|
|
|
2438
2462
|
def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
|
|
2439
2463
|
qds_being_inlined = parent.source
|
|
@@ -3298,7 +3322,7 @@ class Comparison(
|
|
|
3298
3322
|
and self.operator == other.operator
|
|
3299
3323
|
)
|
|
3300
3324
|
|
|
3301
|
-
def inline_constant(self, constant: Concept)
|
|
3325
|
+
def inline_constant(self, constant: Concept):
|
|
3302
3326
|
assert isinstance(constant.lineage, Function)
|
|
3303
3327
|
new_val = constant.lineage.arguments[0]
|
|
3304
3328
|
if isinstance(self.left, ConstantInlineable):
|
|
@@ -3315,10 +3339,7 @@ class Comparison(
|
|
|
3315
3339
|
else:
|
|
3316
3340
|
new_right = self.right
|
|
3317
3341
|
|
|
3318
|
-
|
|
3319
|
-
new_right = new_val
|
|
3320
|
-
|
|
3321
|
-
return Comparison(
|
|
3342
|
+
return self.__class__(
|
|
3322
3343
|
left=new_left,
|
|
3323
3344
|
right=new_right,
|
|
3324
3345
|
operator=self.operator,
|
|
@@ -43,10 +43,14 @@ class InlineDatasource(OptimizationRule):
|
|
|
43
43
|
continue
|
|
44
44
|
root_outputs = {x.address for x in root.output_concepts}
|
|
45
45
|
cte_outputs = {x.address for x in cte.output_columns}
|
|
46
|
+
inherited = {x for x, v in cte.source_map.items() if v}
|
|
46
47
|
# cte_inherited_outputs = {x.address for x in parent_cte.output_columns if parent_cte.source_map.get(x.address)}
|
|
47
48
|
grain_components = {x.address for x in root.grain.components}
|
|
48
|
-
if not
|
|
49
|
-
|
|
49
|
+
if not inherited.issubset(root_outputs):
|
|
50
|
+
cte_missing = inherited - root_outputs
|
|
51
|
+
self.log(
|
|
52
|
+
f"Not all {parent_cte.name} require inputs are found on datasource, missing {cte_missing}"
|
|
53
|
+
)
|
|
50
54
|
continue
|
|
51
55
|
if not grain_components.issubset(cte_outputs):
|
|
52
56
|
self.log("Not all datasource components in cte outputs, forcing group")
|
|
@@ -6,16 +6,9 @@ from trilogy.core.models import (
|
|
|
6
6
|
SubselectComparison,
|
|
7
7
|
Comparison,
|
|
8
8
|
Parenthetical,
|
|
9
|
-
Function,
|
|
10
|
-
FilterItem,
|
|
11
|
-
MagicConstants,
|
|
12
|
-
Concept,
|
|
13
|
-
WindowItem,
|
|
14
|
-
AggregateWrapper,
|
|
15
|
-
DataType,
|
|
16
9
|
)
|
|
17
10
|
from trilogy.core.optimizations.base_optimization import OptimizationRule
|
|
18
|
-
from trilogy.core.
|
|
11
|
+
from trilogy.core.processing.utility import is_scalar_condition
|
|
19
12
|
|
|
20
13
|
|
|
21
14
|
def decompose_condition(
|
|
@@ -56,40 +49,6 @@ def is_child_of(a, comparison):
|
|
|
56
49
|
return base
|
|
57
50
|
|
|
58
51
|
|
|
59
|
-
def is_basic(
|
|
60
|
-
element: (
|
|
61
|
-
int
|
|
62
|
-
| str
|
|
63
|
-
| float
|
|
64
|
-
| list
|
|
65
|
-
| WindowItem
|
|
66
|
-
| FilterItem
|
|
67
|
-
| Concept
|
|
68
|
-
| Comparison
|
|
69
|
-
| Conditional
|
|
70
|
-
| Parenthetical
|
|
71
|
-
| Function
|
|
72
|
-
| AggregateWrapper
|
|
73
|
-
| MagicConstants
|
|
74
|
-
| DataType
|
|
75
|
-
),
|
|
76
|
-
) -> bool:
|
|
77
|
-
if isinstance(element, Parenthetical):
|
|
78
|
-
return is_basic(element.content)
|
|
79
|
-
elif isinstance(element, SubselectComparison):
|
|
80
|
-
return True
|
|
81
|
-
elif isinstance(element, Comparison):
|
|
82
|
-
return is_basic(element.left) and is_basic(element.right)
|
|
83
|
-
elif isinstance(element, Function):
|
|
84
|
-
if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
|
|
85
|
-
return False
|
|
86
|
-
elif isinstance(element, AggregateWrapper):
|
|
87
|
-
return is_basic(element.function)
|
|
88
|
-
elif isinstance(element, Conditional):
|
|
89
|
-
return is_basic(element.left) and is_basic(element.right)
|
|
90
|
-
return True
|
|
91
|
-
|
|
92
|
-
|
|
93
52
|
class PredicatePushdown(OptimizationRule):
|
|
94
53
|
|
|
95
54
|
def __init__(self, *args, **kwargs) -> None:
|
|
@@ -187,13 +146,13 @@ class PredicatePushdown(OptimizationRule):
|
|
|
187
146
|
)
|
|
188
147
|
optimized = False
|
|
189
148
|
for candidate in candidates:
|
|
190
|
-
if not
|
|
149
|
+
if not is_scalar_condition(candidate):
|
|
191
150
|
self.debug(
|
|
192
151
|
f"Skipping {candidate} as not a basic [no aggregate, etc] condition"
|
|
193
152
|
)
|
|
194
153
|
continue
|
|
195
154
|
self.log(
|
|
196
|
-
f"Checking candidate {candidate}, {type(candidate)}, {
|
|
155
|
+
f"Checking candidate {candidate}, {type(candidate)}, scalar: {is_scalar_condition(candidate)}"
|
|
197
156
|
)
|
|
198
157
|
for parent_cte in cte.parent_ctes:
|
|
199
158
|
local_pushdown = self._check_parent(
|
|
@@ -47,7 +47,7 @@ def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
|
|
|
47
47
|
|
|
48
48
|
def resolve_filter_parent_concepts(
|
|
49
49
|
concept: Concept,
|
|
50
|
-
) -> Tuple[Concept, List[Concept], List[Concept]]:
|
|
50
|
+
) -> Tuple[Concept, List[Concept], List[Tuple[Concept, ...]]]:
|
|
51
51
|
if not isinstance(concept.lineage, FilterItem):
|
|
52
52
|
raise ValueError(
|
|
53
53
|
f"Concept {concept} lineage is not filter item, is {type(concept.lineage)}"
|
|
@@ -58,7 +58,7 @@ def resolve_filter_parent_concepts(
|
|
|
58
58
|
base_rows += concept.lineage.where.row_arguments
|
|
59
59
|
# TODO: pass tuple groups through
|
|
60
60
|
for ctuple in concept.lineage.where.existence_arguments:
|
|
61
|
-
base_existence
|
|
61
|
+
base_existence.append(ctuple)
|
|
62
62
|
if direct_parent.grain:
|
|
63
63
|
base_rows += direct_parent.grain.components_copy
|
|
64
64
|
if (
|
|
@@ -71,7 +71,7 @@ def resolve_filter_parent_concepts(
|
|
|
71
71
|
return (
|
|
72
72
|
concept.lineage.content,
|
|
73
73
|
unique(base_rows, "address"),
|
|
74
|
-
|
|
74
|
+
base_existence,
|
|
75
75
|
)
|
|
76
76
|
return concept.lineage.content, unique(base_rows, "address"), []
|
|
77
77
|
|
{pytrilogy-0.0.2.3 → pytrilogy-0.0.2.5}/trilogy/core/processing/node_generators/filter_node.py
RENAMED
|
@@ -74,27 +74,31 @@ def gen_filter_node(
|
|
|
74
74
|
return parent
|
|
75
75
|
|
|
76
76
|
core_parents.append(parent)
|
|
77
|
+
|
|
77
78
|
if parent_existence_concepts:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
parent_existence = source_concepts(
|
|
82
|
-
mandatory_list=parent_existence_concepts,
|
|
83
|
-
environment=environment,
|
|
84
|
-
g=g,
|
|
85
|
-
depth=depth + 1,
|
|
86
|
-
history=history,
|
|
87
|
-
)
|
|
88
|
-
if not parent_existence:
|
|
79
|
+
for existence_tuple in parent_existence_concepts:
|
|
80
|
+
if not existence_tuple:
|
|
81
|
+
continue
|
|
89
82
|
logger.info(
|
|
90
|
-
f"{padding(depth)}{LOGGER_PREFIX} filter existence
|
|
83
|
+
f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in existence_tuple]}"
|
|
91
84
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
parent_existence = source_concepts(
|
|
86
|
+
mandatory_list=list(existence_tuple),
|
|
87
|
+
environment=environment,
|
|
88
|
+
g=g,
|
|
89
|
+
depth=depth + 1,
|
|
90
|
+
history=history,
|
|
91
|
+
)
|
|
92
|
+
if not parent_existence:
|
|
93
|
+
logger.info(
|
|
94
|
+
f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
|
|
95
|
+
)
|
|
96
|
+
return None
|
|
97
|
+
core_parents.append(parent_existence)
|
|
98
|
+
flattened_existence = [x for y in parent_existence_concepts for x in y]
|
|
95
99
|
filter_node = FilterNode(
|
|
96
100
|
input_concepts=unique(
|
|
97
|
-
[immediate_parent] + parent_row_concepts +
|
|
101
|
+
[immediate_parent] + parent_row_concepts + flattened_existence,
|
|
98
102
|
"address",
|
|
99
103
|
),
|
|
100
104
|
output_concepts=[concept, immediate_parent] + parent_row_concepts,
|
|
@@ -19,7 +19,7 @@ from trilogy.core.processing.nodes.base_node import (
|
|
|
19
19
|
concept_list_to_grain,
|
|
20
20
|
)
|
|
21
21
|
from trilogy.utility import unique
|
|
22
|
-
|
|
22
|
+
from trilogy.core.processing.utility import is_scalar_condition
|
|
23
23
|
|
|
24
24
|
LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
|
|
25
25
|
|
|
@@ -111,7 +111,8 @@ class GroupNode(StrategyNode):
|
|
|
111
111
|
f" {parent.grain}"
|
|
112
112
|
)
|
|
113
113
|
source_type = SourceType.GROUP
|
|
114
|
-
|
|
114
|
+
|
|
115
|
+
base = QueryDatasource(
|
|
115
116
|
input_concepts=self.input_concepts,
|
|
116
117
|
output_concepts=self.output_concepts,
|
|
117
118
|
datasources=parent_sources,
|
|
@@ -134,6 +135,31 @@ class GroupNode(StrategyNode):
|
|
|
134
135
|
partial_concepts=self.partial_concepts,
|
|
135
136
|
condition=self.conditions,
|
|
136
137
|
)
|
|
138
|
+
# if there is a condition on a group node and it's not scalar
|
|
139
|
+
# inject an additional CTE
|
|
140
|
+
if self.conditions:
|
|
141
|
+
logger.info("CONDITIONS")
|
|
142
|
+
logger.info(str(self.conditions))
|
|
143
|
+
logger.info(is_scalar_condition(self.conditions))
|
|
144
|
+
if self.conditions and not is_scalar_condition(self.conditions):
|
|
145
|
+
base.condition = None
|
|
146
|
+
base.output_concepts = self.output_concepts + self.conditions.row_arguments
|
|
147
|
+
return QueryDatasource(
|
|
148
|
+
input_concepts=base.output_concepts,
|
|
149
|
+
output_concepts=self.output_concepts,
|
|
150
|
+
datasources=[base],
|
|
151
|
+
source_type=SourceType.SELECT,
|
|
152
|
+
source_map=resolve_concept_map(
|
|
153
|
+
[base],
|
|
154
|
+
targets=self.output_concepts,
|
|
155
|
+
inherited_inputs=base.output_concepts,
|
|
156
|
+
),
|
|
157
|
+
joins=[],
|
|
158
|
+
grain=grain,
|
|
159
|
+
partial_concepts=self.partial_concepts,
|
|
160
|
+
condition=self.conditions,
|
|
161
|
+
)
|
|
162
|
+
return base
|
|
137
163
|
|
|
138
164
|
def copy(self) -> "GroupNode":
|
|
139
165
|
return GroupNode(
|
|
@@ -8,6 +8,16 @@ from trilogy.core.models import (
|
|
|
8
8
|
QueryDatasource,
|
|
9
9
|
LooseConceptList,
|
|
10
10
|
Environment,
|
|
11
|
+
Conditional,
|
|
12
|
+
SubselectComparison,
|
|
13
|
+
Comparison,
|
|
14
|
+
Parenthetical,
|
|
15
|
+
Function,
|
|
16
|
+
FilterItem,
|
|
17
|
+
MagicConstants,
|
|
18
|
+
WindowItem,
|
|
19
|
+
AggregateWrapper,
|
|
20
|
+
DataType,
|
|
11
21
|
)
|
|
12
22
|
|
|
13
23
|
from trilogy.core.enums import Purpose, Granularity
|
|
@@ -18,6 +28,8 @@ from collections import defaultdict
|
|
|
18
28
|
from logging import Logger
|
|
19
29
|
from pydantic import BaseModel
|
|
20
30
|
|
|
31
|
+
from trilogy.core.enums import FunctionClass
|
|
32
|
+
|
|
21
33
|
|
|
22
34
|
class NodeType(Enum):
|
|
23
35
|
CONCEPT = 1
|
|
@@ -332,3 +344,37 @@ def get_disconnected_components(
|
|
|
332
344
|
x for x in sub_graphs if calculate_graph_relevance(graph, x, all_concepts) > 0
|
|
333
345
|
]
|
|
334
346
|
return len(sub_graphs), sub_graphs
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def is_scalar_condition(
|
|
350
|
+
element: (
|
|
351
|
+
int
|
|
352
|
+
| str
|
|
353
|
+
| float
|
|
354
|
+
| list
|
|
355
|
+
| WindowItem
|
|
356
|
+
| FilterItem
|
|
357
|
+
| Concept
|
|
358
|
+
| Comparison
|
|
359
|
+
| Conditional
|
|
360
|
+
| Parenthetical
|
|
361
|
+
| Function
|
|
362
|
+
| AggregateWrapper
|
|
363
|
+
| MagicConstants
|
|
364
|
+
| DataType
|
|
365
|
+
),
|
|
366
|
+
) -> bool:
|
|
367
|
+
if isinstance(element, Parenthetical):
|
|
368
|
+
return is_scalar_condition(element.content)
|
|
369
|
+
elif isinstance(element, SubselectComparison):
|
|
370
|
+
return True
|
|
371
|
+
elif isinstance(element, Comparison):
|
|
372
|
+
return is_scalar_condition(element.left) and is_scalar_condition(element.right)
|
|
373
|
+
elif isinstance(element, Function):
|
|
374
|
+
if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
|
|
375
|
+
return False
|
|
376
|
+
elif isinstance(element, AggregateWrapper):
|
|
377
|
+
return is_scalar_condition(element.function)
|
|
378
|
+
elif isinstance(element, Conditional):
|
|
379
|
+
return is_scalar_condition(element.left) and is_scalar_condition(element.right)
|
|
380
|
+
return True
|
|
@@ -350,7 +350,11 @@ def get_query_datasources(
|
|
|
350
350
|
|
|
351
351
|
search_concepts: list[Concept] = statement.output_components
|
|
352
352
|
nest_where = statement.where_clause_category == SelectFiltering.IMPLICIT
|
|
353
|
-
|
|
353
|
+
|
|
354
|
+
# if all are aggregates, we've pushed the filtering inside the aggregates anyway
|
|
355
|
+
all_aggregate = all([x.is_aggregate for x in search_concepts])
|
|
356
|
+
|
|
357
|
+
if nest_where and statement.where_clause and not all_aggregate:
|
|
354
358
|
search_concepts = unique(
|
|
355
359
|
statement.where_clause.row_arguments + search_concepts, "address"
|
|
356
360
|
)
|
|
@@ -363,7 +367,8 @@ def get_query_datasources(
|
|
|
363
367
|
)
|
|
364
368
|
ds: GroupNode | SelectNode
|
|
365
369
|
if nest_where and statement.where_clause:
|
|
366
|
-
|
|
370
|
+
if not all_aggregate:
|
|
371
|
+
ods.conditions = statement.where_clause.conditional
|
|
367
372
|
ods.output_concepts = search_concepts
|
|
368
373
|
# ods.hidden_concepts = where_delta
|
|
369
374
|
ods.rebuild_cache()
|