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.

Files changed (108) hide show
  1. {pytrilogy-0.0.2.22/pytrilogy.egg-info → pytrilogy-0.0.2.25}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. pytrilogy-0.0.2.25/tests/test_imports.py +42 -0
  4. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_models.py +9 -0
  5. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_parsing.py +31 -13
  6. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_show.py +0 -1
  7. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_statements.py +21 -0
  8. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/__init__.py +1 -1
  9. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/enums.py +10 -0
  10. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/env_processor.py +12 -6
  11. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/environment_helpers.py +0 -1
  12. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/functions.py +2 -0
  13. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/models.py +113 -14
  14. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/concept_strategies_v3.py +23 -4
  15. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/node_merge_node.py +4 -4
  16. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/utility.py +11 -6
  17. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/query_processor.py +20 -0
  18. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/base.py +21 -3
  19. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/executor.py +41 -36
  20. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/parse_engine.py +42 -11
  21. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/render.py +23 -3
  22. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/trilogy.lark +11 -3
  23. pytrilogy-0.0.2.22/tests/test_imports.py +0 -23
  24. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/README.md +0 -0
  26. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/SOURCES.txt +0 -0
  28. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/setup.cfg +0 -0
  33. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/setup.py +0 -0
  34. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_environment.py +0 -0
  39. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_functions.py +0 -0
  40. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_metadata.py +0 -0
  41. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_multi_join_assignments.py +0 -0
  42. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_partial_handling.py +0 -0
  43. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_query_processing.py +0 -0
  44. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_select.py +0 -0
  45. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_undefined_concept.py +0 -0
  46. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/tests/test_where_clause.py +0 -0
  47. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/compiler.py +0 -0
  48. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/constants.py +0 -0
  49. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/__init__.py +0 -0
  50. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/constants.py +0 -0
  51. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/ergonomics.py +0 -0
  52. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/exceptions.py +0 -0
  53. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/graph_models.py +0 -0
  54. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/internal.py +0 -0
  55. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimization.py +0 -0
  56. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/__init__.py +0 -0
  57. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/base_optimization.py +0 -0
  58. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/inline_constant.py +0 -0
  59. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/inline_datasource.py +0 -0
  60. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  61. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/__init__.py +0 -0
  62. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/graph_utils.py +0 -0
  63. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/__init__.py +0 -0
  64. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  65. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/common.py +0 -0
  66. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  67. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/group_node.py +0 -0
  68. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  69. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  70. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  71. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  72. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/select_node.py +0 -0
  73. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  74. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/node_generators/window_node.py +0 -0
  75. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/__init__.py +0 -0
  76. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/base_node.py +0 -0
  77. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/filter_node.py +0 -0
  78. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/group_node.py +0 -0
  79. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/merge_node.py +0 -0
  80. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  81. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  82. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/core/processing/nodes/window_node.py +0 -0
  83. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/__init__.py +0 -0
  84. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/bigquery.py +0 -0
  85. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/common.py +0 -0
  86. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/config.py +0 -0
  87. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/duckdb.py +0 -0
  88. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/enums.py +0 -0
  89. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/postgres.py +0 -0
  90. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/presto.py +0 -0
  91. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/snowflake.py +0 -0
  92. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/dialect/sql_server.py +0 -0
  93. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/engine.py +0 -0
  94. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/__init__.py +0 -0
  95. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/base_hook.py +0 -0
  96. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/graph_hook.py +0 -0
  97. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/hooks/query_debugger.py +0 -0
  98. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/metadata/__init__.py +0 -0
  99. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parser.py +0 -0
  100. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/__init__.py +0 -0
  101. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/common.py +0 -0
  102. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/config.py +0 -0
  103. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/exceptions.py +0 -0
  104. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/parsing/helpers.py +0 -0
  105. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/py.typed +0 -0
  106. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/scripts/__init__.py +0 -0
  107. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/scripts/trilogy.py +0 -0
  108. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.25}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.22
3
+ Version: 0.0.2.25
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.22
3
+ Version: 0.0.2.25
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -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
- Parenthetical,
30
+ TupleWrapper,
31
31
  ), type(right)
32
- assert right.content[0] == 1
32
+ assert right[0] == 1
33
33
  rendered = BaseDialect().render_expr(right)
34
- assert rendered.strip() == "( 1,2,3 )".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
- Parenthetical,
43
+ TupleWrapper,
44
44
  ), type(right)
45
- assert right.content[0] == 1
45
+ assert right[0] == 1
46
46
  rendered = BaseDialect().render_expr(right)
47
- assert rendered.strip() == "( 1 )".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
- right,
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() == "( 1,2,3 )".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,5 +1,4 @@
1
1
  from trilogy.core.models import ShowStatement
2
- from trilogy.core.query_processor import process_query
3
2
  from trilogy import Dialects
4
3
  from trilogy.parser import parse
5
4
 
@@ -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 ");
@@ -4,6 +4,6 @@ from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.22"
7
+ __version__ = "0.0.2.25"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -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(concept: Concept, g: ReferenceGraph):
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 _, pseudonym in concept.pseudonyms.items():
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()), list(environment.datasources.values())
73
+ list(environment.concepts.values())
74
+ + list(environment.alias_origin_lookup.values()),
75
+ list(environment.datasources.values()),
70
76
  )
@@ -191,4 +191,3 @@ def generate_related_concepts(
191
191
  environment.add_concept(auto, meta=meta)
192
192
  if isinstance(value, Concept):
193
193
  environment.merge_concept(auto, value, modifiers=[])
194
- assert value.pseudonyms is not None
@@ -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: Dict[str, Concept] = Field(default_factory=dict)
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[self.address] = self
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
- return self.set == other.set
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.keys())
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 [x.address for x in cte.output_columns]:
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[self.address] = self
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[source.address] = source
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[target.address] = target
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 ProcessedQueryMixin(BaseModel):
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, ProcessedQueryMixin):
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
- expanded.set_output_concepts(root_targets)
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 _, v in concept.pseudonyms.items():
504
- if v.address == concept.address:
517
+ for v_address in concept.pseudonyms:
518
+ v = environment.concepts[v_address]
519
+ if v == concept.address:
505
520
  return
506
- if v.address in seen:
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,
@@ -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: list[Concept] = []
212
+ synonyms: set[str] = set()
213
213
  for x in all_concepts:
214
- synonyms += x.pseudonyms.values()
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 k, v in concept.pseudonyms.items():
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,