pytrilogy 0.0.2.22__tar.gz → 0.0.2.25__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.22/pytrilogy.egg-info → pytrilogy-0.0.2.25}/PKG-INFO +1 -1
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25/pytrilogy.egg-info}/PKG-INFO +1 -1
- pytrilogy-0.0.2.25/tests/test_imports.py +42 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_models.py +9 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_parsing.py +31 -13
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_show.py +0 -1
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_statements.py +21 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/enums.py +10 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/env_processor.py +12 -6
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/environment_helpers.py +0 -1
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/functions.py +2 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/models.py +113 -14
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/concept_strategies_v3.py +23 -4
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/node_merge_node.py +4 -4
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/utility.py +11 -6
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/query_processor.py +20 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/base.py +21 -3
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/executor.py +41 -36
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/parse_engine.py +42 -11
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/render.py +23 -3
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/trilogy.lark +11 -3
- pytrilogy-0.0.2.22/tests/test_imports.py +0 -23
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/README.md +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pyproject.toml +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/SOURCES.txt +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/setup.cfg +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/setup.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_select.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/constants.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/graph_models.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/inline_constant.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/inline_datasource.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/common.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/filter_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/base_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/merge_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/common.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/duckdb.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/presto.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/common.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/utility.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from trilogy import Environment
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_multi_environment():
|
|
6
|
+
basic = Environment()
|
|
7
|
+
|
|
8
|
+
basic.parse(
|
|
9
|
+
"""
|
|
10
|
+
const pi <- 3.14;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
""",
|
|
14
|
+
namespace="math",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
basic.parse(
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
select math.pi;
|
|
21
|
+
"""
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert basic.concepts["math.pi"].name == "pi"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_test_alias_free_import():
|
|
28
|
+
basic = Environment(working_path=Path(__file__).parent)
|
|
29
|
+
|
|
30
|
+
basic.parse(
|
|
31
|
+
"""
|
|
32
|
+
import test_env;
|
|
33
|
+
|
|
34
|
+
key id2 int;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
""",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert basic.concepts["id"].name == "id"
|
|
41
|
+
assert basic.concepts["id2"].name == "id2"
|
|
42
|
+
assert basic.concepts["id"].namespace == basic.concepts["id2"].namespace
|
|
@@ -15,8 +15,11 @@ from trilogy.core.models import (
|
|
|
15
15
|
Concept,
|
|
16
16
|
AggregateWrapper,
|
|
17
17
|
RowsetItem,
|
|
18
|
+
TupleWrapper,
|
|
19
|
+
DataType,
|
|
18
20
|
)
|
|
19
21
|
from trilogy import parse
|
|
22
|
+
from copy import deepcopy
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def test_cte_merge(test_environment, test_environment_graph):
|
|
@@ -312,3 +315,9 @@ select avg_greater_ten;
|
|
|
312
315
|
lineage = env.concepts["avg_greater_ten"].lineage
|
|
313
316
|
assert isinstance(lineage, AggregateWrapper)
|
|
314
317
|
assert isinstance(lineage.function.concept_arguments[0].lineage, RowsetItem)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_tuple_clone():
|
|
321
|
+
x = TupleWrapper([1, 2, 3], type=DataType.INTEGER)
|
|
322
|
+
y = deepcopy(x)
|
|
323
|
+
assert y == x
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from trilogy.core.enums import Purpose, ComparisonOperator
|
|
2
2
|
from trilogy.core.models import (
|
|
3
3
|
DataType,
|
|
4
|
-
Parenthetical,
|
|
5
4
|
ProcessedQuery,
|
|
6
5
|
ShowStatement,
|
|
7
6
|
SelectStatement,
|
|
8
7
|
Environment,
|
|
9
8
|
Comparison,
|
|
9
|
+
TupleWrapper,
|
|
10
10
|
)
|
|
11
11
|
from trilogy.core.functions import argument_to_purpose, function_args_to_output_purpose
|
|
12
12
|
from trilogy.parsing.parse_engine import (
|
|
@@ -27,11 +27,11 @@ def test_in():
|
|
|
27
27
|
right = query.where_clause.conditional.right
|
|
28
28
|
assert isinstance(
|
|
29
29
|
right,
|
|
30
|
-
|
|
30
|
+
TupleWrapper,
|
|
31
31
|
), type(right)
|
|
32
|
-
assert right
|
|
32
|
+
assert right[0] == 1
|
|
33
33
|
rendered = BaseDialect().render_expr(right)
|
|
34
|
-
assert rendered.strip() == "(
|
|
34
|
+
assert rendered.strip() == "(1,2,3)".strip()
|
|
35
35
|
|
|
36
36
|
_, parsed = parse_text(
|
|
37
37
|
"const order_id <- 3; SELECT order_id WHERE order_id IN (1,);"
|
|
@@ -40,11 +40,11 @@ def test_in():
|
|
|
40
40
|
right = query.where_clause.conditional.right
|
|
41
41
|
assert isinstance(
|
|
42
42
|
right,
|
|
43
|
-
|
|
43
|
+
TupleWrapper,
|
|
44
44
|
), type(right)
|
|
45
|
-
assert right
|
|
45
|
+
assert right[0] == 1
|
|
46
46
|
rendered = BaseDialect().render_expr(right)
|
|
47
|
-
assert rendered.strip() == "(
|
|
47
|
+
assert rendered.strip() == "(1)".strip()
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def test_not_in():
|
|
@@ -53,13 +53,10 @@ def test_not_in():
|
|
|
53
53
|
)
|
|
54
54
|
query: ProcessedQuery = parsed[-1]
|
|
55
55
|
right = query.where_clause.conditional.right
|
|
56
|
-
assert isinstance(
|
|
57
|
-
|
|
58
|
-
Parenthetical,
|
|
59
|
-
), type(right)
|
|
60
|
-
assert right.content[0] == 1
|
|
56
|
+
assert isinstance(right, TupleWrapper), type(right)
|
|
57
|
+
assert right[0] == 1
|
|
61
58
|
rendered = BaseDialect().render_expr(right)
|
|
62
|
-
assert rendered.strip() == "(
|
|
59
|
+
assert rendered.strip() == "(1,2,3)".strip()
|
|
63
60
|
|
|
64
61
|
|
|
65
62
|
def test_is_not_null():
|
|
@@ -516,3 +513,24 @@ select filtered_test;
|
|
|
516
513
|
results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
|
|
517
514
|
|
|
518
515
|
assert "filtered_test" in results, results
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def test_unnest_parsing():
|
|
519
|
+
x = """
|
|
520
|
+
key scalar int;
|
|
521
|
+
property scalar.int_array list<int>;
|
|
522
|
+
|
|
523
|
+
key split <- unnest(int_array);
|
|
524
|
+
|
|
525
|
+
datasource avalues (
|
|
526
|
+
int_array: int_array,
|
|
527
|
+
scalar: scalar
|
|
528
|
+
)
|
|
529
|
+
grain (scalar)
|
|
530
|
+
query '''(
|
|
531
|
+
select [1,2,3,4] as int_array, 2 as scalar
|
|
532
|
+
)''';
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
env, parsed = parse_text(x)
|
|
536
|
+
assert env.concepts["split"].datatype == DataType.INTEGER
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
from trilogy.parser import parse
|
|
2
|
+
from trilogy import Dialects
|
|
3
|
+
from trilogy.core.models import ProcessedCopyStatement
|
|
4
|
+
from pathlib import Path
|
|
2
5
|
|
|
3
6
|
# from trilogy.compiler import compile
|
|
4
7
|
|
|
@@ -43,6 +46,24 @@ datasource posts (
|
|
|
43
46
|
parse(text)
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
def test_io_statement():
|
|
50
|
+
target = Path(__file__).parent / "test_io_statement.csv"
|
|
51
|
+
if target.exists():
|
|
52
|
+
target.unlink()
|
|
53
|
+
text = f"""const array <- [1,2,3,4];
|
|
54
|
+
|
|
55
|
+
auto x <- unnest(array);
|
|
56
|
+
|
|
57
|
+
copy into csv '{target}' from select x -> test;
|
|
58
|
+
"""
|
|
59
|
+
exec = Dialects.DUCK_DB.default_executor()
|
|
60
|
+
results = exec.parse_text(text)
|
|
61
|
+
assert isinstance(results[-1], ProcessedCopyStatement)
|
|
62
|
+
for z in results:
|
|
63
|
+
exec.execute_query(z)
|
|
64
|
+
assert target.exists(), "csv file was not created"
|
|
65
|
+
|
|
66
|
+
|
|
46
67
|
def test_datasource_where():
|
|
47
68
|
text = """key user_id int metadata(description="the description");
|
|
48
69
|
property user_id.display_name string metadata(description="The display name ");
|
|
@@ -292,3 +292,13 @@ class SelectFiltering(Enum):
|
|
|
292
292
|
NONE = "none"
|
|
293
293
|
EXPLICIT = "explicit" # the filtering contains only selected values
|
|
294
294
|
IMPLICIT = "implicit" # the filtering contains unselected values
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class IOType(Enum):
|
|
298
|
+
CSV = "csv"
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def _missing_(cls, value):
|
|
302
|
+
if isinstance(value, str) and value.lower() != value:
|
|
303
|
+
return IOType(value.lower())
|
|
304
|
+
return super()._missing_(value)
|
|
@@ -6,17 +6,20 @@ from trilogy.core.graph_models import (
|
|
|
6
6
|
from trilogy.core.models import Environment, Concept, Datasource
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def add_concept(
|
|
9
|
+
def add_concept(
|
|
10
|
+
concept: Concept, g: ReferenceGraph, concept_mapping: dict[str, Concept]
|
|
11
|
+
):
|
|
10
12
|
g.add_node(concept)
|
|
11
13
|
# if we have sources, recursively add them
|
|
12
14
|
node_name = concept_to_node(concept)
|
|
13
15
|
if concept.concept_arguments:
|
|
14
16
|
for source in concept.concept_arguments:
|
|
15
17
|
generic = source.with_default_grain()
|
|
16
|
-
add_concept(generic, g)
|
|
18
|
+
add_concept(generic, g, concept_mapping)
|
|
17
19
|
|
|
18
20
|
g.add_edge(generic, node_name)
|
|
19
|
-
for
|
|
21
|
+
for ps_address in concept.pseudonyms:
|
|
22
|
+
pseudonym = concept_mapping[ps_address]
|
|
20
23
|
pseudonym = pseudonym.with_default_grain()
|
|
21
24
|
pseudonym_node = concept_to_node(pseudonym)
|
|
22
25
|
if (pseudonym_node, node_name) in g.edges and (
|
|
@@ -28,7 +31,7 @@ def add_concept(concept: Concept, g: ReferenceGraph):
|
|
|
28
31
|
continue
|
|
29
32
|
g.add_edge(pseudonym_node, node_name, pseudonym=True)
|
|
30
33
|
g.add_edge(node_name, pseudonym_node, pseudonym=True)
|
|
31
|
-
add_concept(pseudonym, g)
|
|
34
|
+
add_concept(pseudonym, g, concept_mapping)
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
def generate_adhoc_graph(
|
|
@@ -37,10 +40,11 @@ def generate_adhoc_graph(
|
|
|
37
40
|
restrict_to_listed: bool = False,
|
|
38
41
|
) -> ReferenceGraph:
|
|
39
42
|
g = ReferenceGraph()
|
|
43
|
+
concept_mapping = {x.address: x for x in concepts}
|
|
40
44
|
|
|
41
45
|
# add all parsed concepts
|
|
42
46
|
for concept in concepts:
|
|
43
|
-
add_concept(concept, g)
|
|
47
|
+
add_concept(concept, g, concept_mapping)
|
|
44
48
|
|
|
45
49
|
for dataset in datasources:
|
|
46
50
|
node = datasource_to_node(dataset)
|
|
@@ -66,5 +70,7 @@ def generate_graph(
|
|
|
66
70
|
) -> ReferenceGraph:
|
|
67
71
|
|
|
68
72
|
return generate_adhoc_graph(
|
|
69
|
-
list(environment.concepts.values())
|
|
73
|
+
list(environment.concepts.values())
|
|
74
|
+
+ list(environment.alias_origin_lookup.values()),
|
|
75
|
+
list(environment.datasources.values()),
|
|
70
76
|
)
|
|
@@ -104,6 +104,8 @@ def Unnest(args: list[Concept]) -> Function:
|
|
|
104
104
|
output = arg_to_datatype(args[0])
|
|
105
105
|
if isinstance(output, (ListType)):
|
|
106
106
|
output = output.value_data_type
|
|
107
|
+
else:
|
|
108
|
+
output = DataType.STRING
|
|
107
109
|
return Function(
|
|
108
110
|
operator=FunctionType.UNNEST,
|
|
109
111
|
arguments=args,
|
|
@@ -65,6 +65,7 @@ from trilogy.core.enums import (
|
|
|
65
65
|
ShowCategory,
|
|
66
66
|
Granularity,
|
|
67
67
|
SelectFiltering,
|
|
68
|
+
IOType,
|
|
68
69
|
)
|
|
69
70
|
from trilogy.core.exceptions import UndefinedConceptException, InvalidSyntaxException
|
|
70
71
|
from trilogy.utility import unique
|
|
@@ -81,6 +82,9 @@ LT = TypeVar("LT")
|
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
def is_compatible_datatype(left, right):
|
|
85
|
+
# for unknown types, we can't make any assumptions
|
|
86
|
+
if right == DataType.UNKNOWN or left == DataType.UNKNOWN:
|
|
87
|
+
return True
|
|
84
88
|
if left == right:
|
|
85
89
|
return True
|
|
86
90
|
if {left, right} == {DataType.NUMERIC, DataType.FLOAT}:
|
|
@@ -98,6 +102,12 @@ def get_version():
|
|
|
98
102
|
return __version__
|
|
99
103
|
|
|
100
104
|
|
|
105
|
+
def address_with_namespace(address: str, namespace: str) -> str:
|
|
106
|
+
if address.split(".", 1)[0] == DEFAULT_NAMESPACE:
|
|
107
|
+
return f"{namespace}.{address.split('.',1)[1]}"
|
|
108
|
+
return f"{namespace}.{address}"
|
|
109
|
+
|
|
110
|
+
|
|
101
111
|
def get_concept_arguments(expr) -> List["Concept"]:
|
|
102
112
|
output = []
|
|
103
113
|
if isinstance(expr, Concept):
|
|
@@ -432,7 +442,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
432
442
|
keys: Optional[Tuple["Concept", ...]] = None
|
|
433
443
|
grain: "Grain" = Field(default=None, validate_default=True)
|
|
434
444
|
modifiers: Optional[List[Modifier]] = Field(default_factory=list)
|
|
435
|
-
pseudonyms:
|
|
445
|
+
pseudonyms: set[str] = Field(default_factory=set)
|
|
436
446
|
_address_cache: str | None = None
|
|
437
447
|
|
|
438
448
|
def __hash__(self):
|
|
@@ -458,7 +468,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
458
468
|
def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
|
|
459
469
|
if self.address == source.address:
|
|
460
470
|
new = target.with_grain(self.grain.with_merge(source, target, modifiers))
|
|
461
|
-
new.pseudonyms
|
|
471
|
+
new.pseudonyms.add(self.address)
|
|
462
472
|
return new
|
|
463
473
|
return self.__class__(
|
|
464
474
|
name=self.name,
|
|
@@ -612,9 +622,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
612
622
|
else None
|
|
613
623
|
),
|
|
614
624
|
modifiers=self.modifiers,
|
|
615
|
-
pseudonyms={
|
|
616
|
-
k: v.with_namespace(namespace) for k, v in self.pseudonyms.items()
|
|
617
|
-
},
|
|
625
|
+
pseudonyms={address_with_namespace(v, namespace) for v in self.pseudonyms},
|
|
618
626
|
)
|
|
619
627
|
|
|
620
628
|
def with_select_context(
|
|
@@ -858,7 +866,7 @@ class Grain(Mergeable, BaseModel):
|
|
|
858
866
|
)
|
|
859
867
|
else:
|
|
860
868
|
v2 = unique(v, "address")
|
|
861
|
-
final = []
|
|
869
|
+
final: List[Concept] = []
|
|
862
870
|
for sub in v2:
|
|
863
871
|
if sub.purpose in (Purpose.PROPERTY, Purpose.METRIC) and sub.keys:
|
|
864
872
|
if all([c in v2 for c in sub.keys]):
|
|
@@ -912,6 +920,20 @@ class Grain(Mergeable, BaseModel):
|
|
|
912
920
|
[c.name == ALL_ROWS_CONCEPT for c in self.components]
|
|
913
921
|
)
|
|
914
922
|
|
|
923
|
+
@property
|
|
924
|
+
def synonym_set(self) -> set[str]:
|
|
925
|
+
base = []
|
|
926
|
+
for x in self.components_copy:
|
|
927
|
+
if isinstance(x.lineage, RowsetItem):
|
|
928
|
+
base.append(x.lineage.content.address)
|
|
929
|
+
for c in x.lineage.content.pseudonyms:
|
|
930
|
+
base.append(c)
|
|
931
|
+
else:
|
|
932
|
+
base.append(x.address)
|
|
933
|
+
for c in x.pseudonyms:
|
|
934
|
+
base.append(c)
|
|
935
|
+
return set(base)
|
|
936
|
+
|
|
915
937
|
@cached_property
|
|
916
938
|
def set(self) -> set[str]:
|
|
917
939
|
base = []
|
|
@@ -927,7 +949,11 @@ class Grain(Mergeable, BaseModel):
|
|
|
927
949
|
return self.set == set([c.address for c in other])
|
|
928
950
|
if not isinstance(other, Grain):
|
|
929
951
|
return False
|
|
930
|
-
|
|
952
|
+
if self.set == other.set:
|
|
953
|
+
return True
|
|
954
|
+
elif self.synonym_set == other.synonym_set:
|
|
955
|
+
return True
|
|
956
|
+
return False
|
|
931
957
|
|
|
932
958
|
def issubset(self, other: "Grain"):
|
|
933
959
|
return self.set.issubset(other.set)
|
|
@@ -1778,6 +1804,16 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
|
|
|
1778
1804
|
)
|
|
1779
1805
|
|
|
1780
1806
|
|
|
1807
|
+
class CopyStatement(BaseModel):
|
|
1808
|
+
target: str
|
|
1809
|
+
target_type: IOType
|
|
1810
|
+
meta: Optional[Metadata] = Field(default_factory=lambda: Metadata())
|
|
1811
|
+
select: SelectStatement
|
|
1812
|
+
|
|
1813
|
+
def refresh_bindings(self, environment: Environment):
|
|
1814
|
+
self.select.refresh_bindings(environment)
|
|
1815
|
+
|
|
1816
|
+
|
|
1781
1817
|
class AlignItem(Namespaced, BaseModel):
|
|
1782
1818
|
alias: str
|
|
1783
1819
|
concepts: List[Concept]
|
|
@@ -2248,7 +2284,7 @@ class BaseJoin(BaseModel):
|
|
|
2248
2284
|
for ds in [self.left_datasource, self.right_datasource]:
|
|
2249
2285
|
synonyms = []
|
|
2250
2286
|
for c in ds.output_concepts:
|
|
2251
|
-
synonyms += list(c.pseudonyms
|
|
2287
|
+
synonyms += list(c.pseudonyms)
|
|
2252
2288
|
if (
|
|
2253
2289
|
concept.address not in [c.address for c in ds.output_concepts]
|
|
2254
2290
|
and concept.address not in synonyms
|
|
@@ -2823,9 +2859,21 @@ class CTE(BaseModel):
|
|
|
2823
2859
|
return self.parent_ctes[0].name
|
|
2824
2860
|
return self.name
|
|
2825
2861
|
|
|
2862
|
+
def get_concept(self, address: str) -> Concept | None:
|
|
2863
|
+
for cte in self.parent_ctes:
|
|
2864
|
+
if address in cte.output_columns:
|
|
2865
|
+
match = [x for x in cte.output_columns if x.address == address].pop()
|
|
2866
|
+
return match
|
|
2867
|
+
|
|
2868
|
+
for array in [self.source.input_concepts, self.source.output_concepts]:
|
|
2869
|
+
match_list = [x for x in array if x.address == address]
|
|
2870
|
+
if match_list:
|
|
2871
|
+
return match_list.pop()
|
|
2872
|
+
return None
|
|
2873
|
+
|
|
2826
2874
|
def get_alias(self, concept: Concept, source: str | None = None) -> str:
|
|
2827
2875
|
for cte in self.parent_ctes:
|
|
2828
|
-
if concept.address in
|
|
2876
|
+
if concept.address in cte.output_columns:
|
|
2829
2877
|
if source and source != cte.name:
|
|
2830
2878
|
continue
|
|
2831
2879
|
return concept.safe_address
|
|
@@ -2977,7 +3025,7 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
|
|
|
2977
3025
|
) -> "UndefinedConcept" | Concept:
|
|
2978
3026
|
if self.address == source.address:
|
|
2979
3027
|
new = target.with_grain(self.grain.with_merge(source, target, modifiers))
|
|
2980
|
-
new.pseudonyms
|
|
3028
|
+
new.pseudonyms.add(self.address)
|
|
2981
3029
|
return new
|
|
2982
3030
|
return self.__class__(
|
|
2983
3031
|
name=self.name,
|
|
@@ -3509,6 +3557,7 @@ class Environment(BaseModel):
|
|
|
3509
3557
|
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
3510
3558
|
):
|
|
3511
3559
|
replacements = {}
|
|
3560
|
+
|
|
3512
3561
|
# exit early if we've run this
|
|
3513
3562
|
if source.address in self.alias_origin_lookup:
|
|
3514
3563
|
if self.concepts[source.address] == target:
|
|
@@ -3517,11 +3566,11 @@ class Environment(BaseModel):
|
|
|
3517
3566
|
for k, v in self.concepts.items():
|
|
3518
3567
|
|
|
3519
3568
|
if v.address == target.address:
|
|
3520
|
-
v.pseudonyms
|
|
3569
|
+
v.pseudonyms.add(source.address)
|
|
3521
3570
|
if v.address == source.address:
|
|
3522
3571
|
replacements[k] = target
|
|
3523
3572
|
self.canonical_map[k] = target.address
|
|
3524
|
-
v.pseudonyms
|
|
3573
|
+
v.pseudonyms.add(target.address)
|
|
3525
3574
|
# we need to update keys and grains of all concepts
|
|
3526
3575
|
else:
|
|
3527
3576
|
replacements[k] = v.with_merge(source, target, modifiers)
|
|
@@ -3599,6 +3648,7 @@ class Comparison(
|
|
|
3599
3648
|
MagicConstants,
|
|
3600
3649
|
WindowItem,
|
|
3601
3650
|
AggregateWrapper,
|
|
3651
|
+
TupleWrapper,
|
|
3602
3652
|
]
|
|
3603
3653
|
operator: ComparisonOperator
|
|
3604
3654
|
|
|
@@ -4258,13 +4308,23 @@ class ProcessedQuery(BaseModel):
|
|
|
4258
4308
|
order_by: Optional[OrderBy] = None
|
|
4259
4309
|
|
|
4260
4310
|
|
|
4261
|
-
class
|
|
4311
|
+
class PersistQueryMixin(BaseModel):
|
|
4262
4312
|
output_to: MaterializedDataset
|
|
4263
4313
|
datasource: Datasource
|
|
4264
4314
|
# base:Dataset
|
|
4265
4315
|
|
|
4266
4316
|
|
|
4267
|
-
class ProcessedQueryPersist(ProcessedQuery,
|
|
4317
|
+
class ProcessedQueryPersist(ProcessedQuery, PersistQueryMixin):
|
|
4318
|
+
pass
|
|
4319
|
+
|
|
4320
|
+
|
|
4321
|
+
class CopyQueryMixin(BaseModel):
|
|
4322
|
+
target: str
|
|
4323
|
+
target_type: IOType
|
|
4324
|
+
# base:Dataset
|
|
4325
|
+
|
|
4326
|
+
|
|
4327
|
+
class ProcessedCopyStatement(ProcessedQuery, CopyQueryMixin):
|
|
4268
4328
|
pass
|
|
4269
4329
|
|
|
4270
4330
|
|
|
@@ -4523,6 +4583,37 @@ class Parenthetical(
|
|
|
4523
4583
|
return base
|
|
4524
4584
|
|
|
4525
4585
|
|
|
4586
|
+
class TupleWrapper(Generic[VT], tuple):
|
|
4587
|
+
"""Used to distinguish parsed tuple objects from other tuples"""
|
|
4588
|
+
|
|
4589
|
+
def __init__(self, val, type: DataType, **kwargs):
|
|
4590
|
+
super().__init__()
|
|
4591
|
+
self.type = type
|
|
4592
|
+
self.val = val
|
|
4593
|
+
|
|
4594
|
+
def __getnewargs__(self):
|
|
4595
|
+
return (self.val, self.type)
|
|
4596
|
+
|
|
4597
|
+
def __new__(cls, val, type: DataType, **kwargs):
|
|
4598
|
+
return super().__new__(cls, tuple(val))
|
|
4599
|
+
# self.type = type
|
|
4600
|
+
|
|
4601
|
+
@classmethod
|
|
4602
|
+
def __get_pydantic_core_schema__(
|
|
4603
|
+
cls, source_type: Any, handler: Callable[[Any], core_schema.CoreSchema]
|
|
4604
|
+
) -> core_schema.CoreSchema:
|
|
4605
|
+
args = get_args(source_type)
|
|
4606
|
+
if args:
|
|
4607
|
+
schema = handler(Tuple[args]) # type: ignore
|
|
4608
|
+
else:
|
|
4609
|
+
schema = handler(Tuple)
|
|
4610
|
+
return core_schema.no_info_after_validator_function(cls.validate, schema)
|
|
4611
|
+
|
|
4612
|
+
@classmethod
|
|
4613
|
+
def validate(cls, v):
|
|
4614
|
+
return cls(v, type=arg_to_datatype(v[0]))
|
|
4615
|
+
|
|
4616
|
+
|
|
4526
4617
|
class PersistStatement(BaseModel):
|
|
4527
4618
|
datasource: Datasource
|
|
4528
4619
|
select: SelectStatement
|
|
@@ -4589,6 +4680,12 @@ def list_to_wrapper(args):
|
|
|
4589
4680
|
return ListWrapper(args, type=types[0])
|
|
4590
4681
|
|
|
4591
4682
|
|
|
4683
|
+
def tuple_to_wrapper(args):
|
|
4684
|
+
types = [arg_to_datatype(arg) for arg in args]
|
|
4685
|
+
assert len(set(types)) == 1
|
|
4686
|
+
return TupleWrapper(args, type=types[0])
|
|
4687
|
+
|
|
4688
|
+
|
|
4592
4689
|
def dict_to_map_wrapper(arg):
|
|
4593
4690
|
key_types = [arg_to_datatype(arg) for arg in arg.keys()]
|
|
4594
4691
|
|
|
@@ -4644,6 +4741,8 @@ def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | Numeric
|
|
|
4644
4741
|
return arg.function.output_datatype
|
|
4645
4742
|
elif isinstance(arg, Parenthetical):
|
|
4646
4743
|
return arg_to_datatype(arg.content)
|
|
4744
|
+
elif isinstance(arg, TupleWrapper):
|
|
4745
|
+
return ListType(type=arg.type)
|
|
4647
4746
|
elif isinstance(arg, WindowItem):
|
|
4648
4747
|
if arg.type in (WindowType.RANK, WindowType.ROW_NUMBER):
|
|
4649
4748
|
return DataType.INTEGER
|
|
@@ -455,7 +455,20 @@ def generate_node(
|
|
|
455
455
|
if x.address not in [y.address for y in root_targets]
|
|
456
456
|
and x not in ex_resolve.grain.components
|
|
457
457
|
]
|
|
458
|
-
|
|
458
|
+
|
|
459
|
+
pseudonyms = [
|
|
460
|
+
x
|
|
461
|
+
for x in extra
|
|
462
|
+
if any(x.address in y.pseudonyms for y in root_targets)
|
|
463
|
+
]
|
|
464
|
+
# if we're only connected by a pseudonym, keep those in output
|
|
465
|
+
expanded.set_output_concepts(root_targets + pseudonyms)
|
|
466
|
+
# but hide them
|
|
467
|
+
if pseudonyms:
|
|
468
|
+
logger.info(
|
|
469
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Hiding pseudonyms{[c.address for c in pseudonyms]}"
|
|
470
|
+
)
|
|
471
|
+
expanded.hide_output_concepts(pseudonyms)
|
|
459
472
|
|
|
460
473
|
logger.info(
|
|
461
474
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in root_targets]} via concept addition; removing extra {[c.address for c in extra]}"
|
|
@@ -480,6 +493,7 @@ def validate_concept(
|
|
|
480
493
|
found_map: dict[str, set[Concept]],
|
|
481
494
|
accept_partial: bool,
|
|
482
495
|
seen: set[str],
|
|
496
|
+
environment: Environment,
|
|
483
497
|
):
|
|
484
498
|
|
|
485
499
|
found_map[str(node)].add(concept)
|
|
@@ -500,10 +514,11 @@ def validate_concept(
|
|
|
500
514
|
if accept_partial:
|
|
501
515
|
found_addresses.add(concept.address)
|
|
502
516
|
found_map[str(node)].add(concept)
|
|
503
|
-
for
|
|
504
|
-
|
|
517
|
+
for v_address in concept.pseudonyms:
|
|
518
|
+
v = environment.concepts[v_address]
|
|
519
|
+
if v == concept.address:
|
|
505
520
|
return
|
|
506
|
-
if v
|
|
521
|
+
if v in seen:
|
|
507
522
|
return
|
|
508
523
|
validate_concept(
|
|
509
524
|
v,
|
|
@@ -515,10 +530,12 @@ def validate_concept(
|
|
|
515
530
|
found_map,
|
|
516
531
|
accept_partial,
|
|
517
532
|
seen=seen,
|
|
533
|
+
environment=environment,
|
|
518
534
|
)
|
|
519
535
|
|
|
520
536
|
|
|
521
537
|
def validate_stack(
|
|
538
|
+
environment: Environment,
|
|
522
539
|
stack: List[StrategyNode],
|
|
523
540
|
concepts: List[Concept],
|
|
524
541
|
mandatory_with_filter: List[Concept],
|
|
@@ -546,6 +563,7 @@ def validate_stack(
|
|
|
546
563
|
found_map,
|
|
547
564
|
accept_partial,
|
|
548
565
|
seen,
|
|
566
|
+
environment,
|
|
549
567
|
)
|
|
550
568
|
for concept in node.virtual_output_concepts:
|
|
551
569
|
if concept.address in non_partial_addresses:
|
|
@@ -807,6 +825,7 @@ def _search_concepts(
|
|
|
807
825
|
break
|
|
808
826
|
attempted.add(priority_concept.address)
|
|
809
827
|
complete, found, missing, partial, virtual = validate_stack(
|
|
828
|
+
environment,
|
|
810
829
|
stack,
|
|
811
830
|
mandatory_list,
|
|
812
831
|
completion_mandatory,
|
{pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/node_merge_node.py
RENAMED
|
@@ -209,9 +209,9 @@ def resolve_weak_components(
|
|
|
209
209
|
for c in all_concepts
|
|
210
210
|
if "__preql_internal" not in c.address
|
|
211
211
|
]
|
|
212
|
-
synonyms:
|
|
212
|
+
synonyms: set[str] = set()
|
|
213
213
|
for x in all_concepts:
|
|
214
|
-
synonyms
|
|
214
|
+
synonyms = synonyms.union(x.pseudonyms)
|
|
215
215
|
while break_flag is not True:
|
|
216
216
|
count += 1
|
|
217
217
|
if count > AMBIGUITY_CHECK_LIMIT:
|
|
@@ -385,9 +385,9 @@ def gen_merge_node(
|
|
|
385
385
|
# one concept handling may need to be kicked to alias
|
|
386
386
|
if len(all_concepts) == 1:
|
|
387
387
|
concept = all_concepts[0]
|
|
388
|
-
for
|
|
388
|
+
for v in concept.pseudonyms:
|
|
389
389
|
test = subgraphs_to_merge_node(
|
|
390
|
-
[[concept, v]],
|
|
390
|
+
[[concept, environment.alias_origin_lookup[v]]],
|
|
391
391
|
g=g,
|
|
392
392
|
all_concepts=[concept],
|
|
393
393
|
environment=environment,
|