pytrilogy 0.0.3.66__tar.gz → 0.0.3.68__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 (150) hide show
  1. {pytrilogy-0.0.3.66/pytrilogy.egg-info → pytrilogy-0.0.3.68}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_statements.py +1 -1
  4. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/exceptions.py +4 -4
  6. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/build.py +38 -3
  7. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/environment.py +5 -6
  8. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/concept_strategies_v3.py +15 -6
  9. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/discovery_node_factory.py +12 -8
  10. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/discovery_utility.py +1 -9
  11. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/discovery_validation.py +17 -4
  12. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/filter_node.py +25 -5
  13. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/group_node.py +1 -1
  14. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/node_merge_node.py +46 -31
  15. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/select_merge_node.py +32 -25
  16. pytrilogy-0.0.3.68/trilogy/core/processing/node_generators/select_node.py +94 -0
  17. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/synonym_node.py +19 -20
  18. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/base_node.py +12 -3
  19. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/group_node.py +1 -1
  20. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/utility.py +17 -43
  21. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/query_processor.py +1 -0
  22. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/base.py +3 -1
  23. pytrilogy-0.0.3.66/trilogy/core/processing/node_generators/select_node.py +0 -56
  24. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/README.md +0 -0
  26. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/pytrilogy.egg-info/SOURCES.txt +0 -0
  28. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/setup.cfg +0 -0
  33. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/setup.py +0 -0
  34. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_enums.py +0 -0
  39. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_environment.py +0 -0
  40. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_executor.py +0 -0
  41. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_failure.py +0 -0
  42. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_functions.py +0 -0
  43. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_imports.py +0 -0
  44. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_metadata.py +0 -0
  45. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_models.py +0 -0
  46. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_multi_join_assignments.py +0 -0
  47. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_parse_engine.py +0 -0
  48. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_parsing.py +0 -0
  49. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_parsing_failures.py +0 -0
  50. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_partial_handling.py +0 -0
  51. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_query_processing.py +0 -0
  52. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_query_render.py +0 -0
  53. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_select.py +0 -0
  54. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_show.py +0 -0
  55. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_typing.py +0 -0
  56. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_undefined_concept.py +0 -0
  57. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_user_functions.py +0 -0
  58. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/tests/test_where_clause.py +0 -0
  59. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/authoring/__init__.py +0 -0
  60. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/compiler.py +0 -0
  61. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/constants.py +0 -0
  62. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/__init__.py +0 -0
  63. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/constants.py +0 -0
  64. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/enums.py +0 -0
  65. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/env_processor.py +0 -0
  66. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/environment_helpers.py +0 -0
  67. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/ergonomics.py +0 -0
  68. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/functions.py +0 -0
  69. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/graph_models.py +0 -0
  70. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/internal.py +0 -0
  71. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/__init__.py +0 -0
  72. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/author.py +0 -0
  73. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/build_environment.py +0 -0
  74. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/core.py +0 -0
  75. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/datasource.py +0 -0
  76. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/models/execute.py +0 -0
  77. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/optimization.py +0 -0
  78. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/optimizations/__init__.py +0 -0
  79. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/optimizations/base_optimization.py +0 -0
  80. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/optimizations/inline_datasource.py +0 -0
  81. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  82. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/__init__.py +0 -0
  83. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/discovery_loop.py +0 -0
  84. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/graph_utils.py +0 -0
  85. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/__init__.py +0 -0
  86. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  87. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/common.py +0 -0
  88. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  89. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  90. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  91. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  92. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  93. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  94. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/union_node.py +0 -0
  95. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  96. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/node_generators/window_node.py +0 -0
  97. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/__init__.py +0 -0
  98. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/filter_node.py +0 -0
  99. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/merge_node.py +0 -0
  100. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  101. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  102. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/union_node.py +0 -0
  103. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  104. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/processing/nodes/window_node.py +0 -0
  105. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/statements/__init__.py +0 -0
  106. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/statements/author.py +0 -0
  107. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/statements/build.py +0 -0
  108. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/statements/common.py +0 -0
  109. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/statements/execute.py +0 -0
  110. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/core/utility.py +0 -0
  111. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/__init__.py +0 -0
  112. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/bigquery.py +0 -0
  113. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/common.py +0 -0
  114. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/config.py +0 -0
  115. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/dataframe.py +0 -0
  116. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/duckdb.py +0 -0
  117. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/enums.py +0 -0
  118. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/postgres.py +0 -0
  119. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/presto.py +0 -0
  120. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/snowflake.py +0 -0
  121. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/dialect/sql_server.py +0 -0
  122. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/engine.py +0 -0
  123. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/executor.py +0 -0
  124. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/hooks/__init__.py +0 -0
  125. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/hooks/base_hook.py +0 -0
  126. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/hooks/graph_hook.py +0 -0
  127. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/hooks/query_debugger.py +0 -0
  128. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/metadata/__init__.py +0 -0
  129. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parser.py +0 -0
  130. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/__init__.py +0 -0
  131. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/common.py +0 -0
  132. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/config.py +0 -0
  133. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/exceptions.py +0 -0
  134. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/helpers.py +0 -0
  135. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/parse_engine.py +0 -0
  136. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/render.py +0 -0
  137. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/parsing/trilogy.lark +0 -0
  138. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/py.typed +0 -0
  139. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/render.py +0 -0
  140. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/scripts/__init__.py +0 -0
  141. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/scripts/trilogy.py +0 -0
  142. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/__init__.py +0 -0
  143. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/date.preql +0 -0
  144. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/display.preql +0 -0
  145. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/geography.preql +0 -0
  146. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/money.preql +0 -0
  147. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/net.preql +0 -0
  148. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/ranking.preql +0 -0
  149. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/std/report.preql +0 -0
  150. {pytrilogy-0.0.3.66 → pytrilogy-0.0.3.68}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.66
3
+ Version: 0.0.3.68
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.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.66
3
+ Version: 0.0.3.68
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -59,7 +59,7 @@ def test_io_statement():
59
59
 
60
60
  auto x <- unnest(array);
61
61
 
62
- copy into csv '{target}' from select x -> test;
62
+ copy into csv '{target}' from select x -> test order by test asc;
63
63
  """
64
64
  exec = Dialects.DUCK_DB.default_executor()
65
65
  results = exec.parse_text(text)
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.66"
7
+ __version__ = "0.0.3.68"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -8,7 +8,7 @@ class UndefinedConceptException(Exception):
8
8
  self.suggestions = suggestions
9
9
 
10
10
 
11
- class UnresolvableQueryException(Exception):
11
+ class FrozenEnvironmentException(Exception):
12
12
  pass
13
13
 
14
14
 
@@ -16,15 +16,15 @@ class InvalidSyntaxException(Exception):
16
16
  pass
17
17
 
18
18
 
19
- class NoDatasourceException(Exception):
19
+ class UnresolvableQueryException(Exception):
20
20
  pass
21
21
 
22
22
 
23
- class FrozenEnvironmentException(Exception):
23
+ class NoDatasourceException(UnresolvableQueryException):
24
24
  pass
25
25
 
26
26
 
27
- class AmbiguousRelationshipResolutionException(Exception):
27
+ class AmbiguousRelationshipResolutionException(UnresolvableQueryException):
28
28
  def __init__(self, message, parents: List[set[str]]):
29
29
  super().__init__(self, message)
30
30
  self.message = message
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC
4
+ from collections import defaultdict
4
5
  from datetime import date, datetime
5
6
  from functools import cached_property, singledispatchmethod
6
7
  from typing import (
@@ -1466,6 +1467,20 @@ BuildExpr = (
1466
1467
  BuildConcept.model_rebuild()
1467
1468
 
1468
1469
 
1470
+ def get_canonical_pseudonyms(environment: Environment) -> dict[str, set[str]]:
1471
+ roots: dict[str, set[str]] = defaultdict(set)
1472
+ for k, v in environment.concepts.items():
1473
+ roots[v.address].add(k)
1474
+ for x in v.pseudonyms:
1475
+ roots[v.address].add(x)
1476
+ for k, v in environment.alias_origin_lookup.items():
1477
+ lookup = environment.concepts[k].address
1478
+ roots[lookup].add(v.address)
1479
+ for x2 in v.pseudonyms:
1480
+ roots[lookup].add(x2)
1481
+ return roots
1482
+
1483
+
1469
1484
  class Factory:
1470
1485
 
1471
1486
  def __init__(
@@ -1479,6 +1494,7 @@ class Factory:
1479
1494
  self.local_concepts: dict[str, BuildConcept] = (
1480
1495
  {} if local_concepts is None else local_concepts
1481
1496
  )
1497
+ self.pseudonym_map = get_canonical_pseudonyms(environment)
1482
1498
 
1483
1499
  def instantiate_concept(
1484
1500
  self,
@@ -1656,6 +1672,17 @@ class Factory:
1656
1672
  )
1657
1673
  is_aggregate = Concept.calculate_is_aggregate(build_lineage)
1658
1674
 
1675
+ # if this is a pseudonym, we need to look up the base address
1676
+ if base.address in self.environment.alias_origin_lookup:
1677
+ lookup_address = self.environment.concepts[base.address].address
1678
+ # map only to the canonical concept, not to other merged concepts
1679
+ base_pseudonyms = {lookup_address}
1680
+ else:
1681
+ base_pseudonyms = {
1682
+ x
1683
+ for x in self.pseudonym_map.get(base.address, set())
1684
+ if x != base.address
1685
+ }
1659
1686
  rval = BuildConcept.model_construct(
1660
1687
  name=base.name,
1661
1688
  datatype=base.datatype,
@@ -1666,7 +1693,7 @@ class Factory:
1666
1693
  namespace=base.namespace,
1667
1694
  keys=base.keys,
1668
1695
  modifiers=base.modifiers,
1669
- pseudonyms=base.pseudonyms,
1696
+ pseudonyms=base_pseudonyms,
1670
1697
  ## instantiated values
1671
1698
  derivation=derivation,
1672
1699
  granularity=granularity,
@@ -1688,14 +1715,22 @@ class Factory:
1688
1715
 
1689
1716
  @build.register
1690
1717
  def _(self, base: ColumnAssignment) -> BuildColumnAssignment:
1691
- fetched = self.environment.concepts[base.concept.address]
1718
+ address = base.concept.address
1719
+ fetched = (
1720
+ self.build(
1721
+ self.environment.alias_origin_lookup[address].with_grain(self.grain)
1722
+ )
1723
+ if address in self.environment.alias_origin_lookup
1724
+ else self.build(self.environment.concepts[address].with_grain(self.grain))
1725
+ )
1726
+
1692
1727
  return BuildColumnAssignment.model_construct(
1693
1728
  alias=(
1694
1729
  self.build(base.alias)
1695
1730
  if isinstance(base.alias, Function)
1696
1731
  else base.alias
1697
1732
  ),
1698
- concept=self.build(fetched.with_grain(self.grain)),
1733
+ concept=fetched,
1699
1734
  modifiers=base.modifiers,
1700
1735
  )
1701
1736
 
@@ -362,9 +362,6 @@ class Environment(BaseModel):
362
362
  and x.concept.address != deriv_lookup
363
363
  ]
364
364
  assert len(datasource.columns) < clen
365
- for x in datasource.columns:
366
- logger.info(x)
367
-
368
365
  return None
369
366
 
370
367
  if existing and self.config.allow_duplicate_declaration:
@@ -607,15 +604,15 @@ class Environment(BaseModel):
607
604
  )
608
605
  persisted = f"{PERSISTED_CONCEPT_PREFIX}_" + new_persisted_concept.name
609
606
  # override the current concept source to reflect that it's now coming from a datasource
607
+ base_pseudonyms = new_persisted_concept.pseudonyms or set()
608
+ original_pseudonyms = {*base_pseudonyms, new_persisted_concept.address}
610
609
  if (
611
610
  new_persisted_concept.metadata.concept_source
612
611
  != ConceptSource.PERSIST_STATEMENT
613
612
  ):
614
613
  original_concept = new_persisted_concept.model_copy(
615
614
  deep=True,
616
- update={
617
- "name": persisted,
618
- },
615
+ update={"name": persisted, "pseudonyms": original_pseudonyms},
619
616
  )
620
617
  self.add_concept(
621
618
  original_concept,
@@ -629,6 +626,7 @@ class Environment(BaseModel):
629
626
  ),
630
627
  "derivation": Derivation.ROOT,
631
628
  "purpose": new_persisted_concept.purpose,
629
+ "pseudonyms": {*original_pseudonyms, original_concept.address},
632
630
  }
633
631
  # purpose is used in derivation calculation
634
632
  # which should be fixed, but we'll do in a followup
@@ -650,6 +648,7 @@ class Environment(BaseModel):
650
648
  new_persisted_concept,
651
649
  meta=meta,
652
650
  )
651
+
653
652
  return datasource
654
653
 
655
654
  def delete_datasource(
@@ -224,6 +224,9 @@ def initialize_loop_context(
224
224
  else:
225
225
 
226
226
  completion_mandatory = mandatory_list
227
+ logger.info(
228
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Initialized loop context with mandatory list {[c.address for c in mandatory_list]} and completion mandatory {[c.address for c in completion_mandatory]}"
229
+ )
227
230
  return LoopContext(
228
231
  mandatory_list=mandatory_list,
229
232
  environment=environment,
@@ -330,7 +333,7 @@ def check_for_early_exit(
330
333
  return False
331
334
 
332
335
 
333
- def generate_loop_completion(context: LoopContext, virtual) -> StrategyNode:
336
+ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> StrategyNode:
334
337
  condition_required = True
335
338
  non_virtual = [c for c in context.completion_mandatory if c.address not in virtual]
336
339
  non_virtual_output = [
@@ -367,10 +370,15 @@ def generate_loop_completion(context: LoopContext, virtual) -> StrategyNode:
367
370
  output: StrategyNode = context.stack[0]
368
371
  if non_virtual_different:
369
372
  logger.info(
370
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values"
373
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values by setting outputs to {[x.address for x in output.output_concepts if x.address in non_virtual_output]}"
371
374
  )
372
375
  output.set_output_concepts(
373
- [x for x in output.output_concepts if x.address in non_virtual_output],
376
+ [
377
+ x
378
+ for x in output.output_concepts
379
+ if x.address in non_virtual_output
380
+ or any(c in non_virtual_output for c in x.pseudonyms)
381
+ ],
374
382
  rebuild=False,
375
383
  )
376
384
 
@@ -404,7 +412,7 @@ def generate_loop_completion(context: LoopContext, virtual) -> StrategyNode:
404
412
  elif context.conditions:
405
413
  output.preexisting_conditions = context.conditions.conditional
406
414
  logger.info(
407
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]} with {context.conditions}"
415
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node output {[x.address for x in output.usable_outputs]} partial {[c.address for c in output.partial_concepts]} with {context.conditions}"
408
416
  )
409
417
  if condition_required and context.conditions and non_virtual_different:
410
418
  logger.info(
@@ -419,7 +427,7 @@ def generate_loop_completion(context: LoopContext, virtual) -> StrategyNode:
419
427
  )
420
428
  if result.required:
421
429
  logger.info(
422
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Adding group node"
430
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Adding group node with outputs {[x.address for x in context.original_mandatory]}"
423
431
  )
424
432
  return GroupNode(
425
433
  output_concepts=context.original_mandatory,
@@ -466,6 +474,7 @@ def _search_concepts(
466
474
  )
467
475
 
468
476
  while context.incomplete:
477
+
469
478
  priority_concept = get_priority_concept(
470
479
  context.mandatory_list,
471
480
  context.attempted,
@@ -478,7 +487,7 @@ def _search_concepts(
478
487
  candidates = [
479
488
  c for c in context.mandatory_list if c.address != priority_concept.address
480
489
  ]
481
- # the local conditions list may be override if we end up injecting conditions
490
+ # the local conditions list may be overriden if we end up injecting conditions
482
491
  candidate_list, local_conditions = generate_candidates_restrictive(
483
492
  priority_concept,
484
493
  candidates,
@@ -307,16 +307,17 @@ class RootNodeHandler:
307
307
  def _resolve_root_concepts(
308
308
  self, root_targets: List[BuildConcept]
309
309
  ) -> Optional[StrategyNode]:
310
- synonym_node = self._try_synonym_resolution(root_targets)
311
- if synonym_node:
312
- logger.info(
313
- f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
314
- f"resolved root concepts through synonyms"
315
- )
316
- return synonym_node
317
310
  expanded_node = self._try_merge_expansion(root_targets)
318
311
  if expanded_node:
319
312
  return expanded_node
313
+ if self.ctx.accept_partial:
314
+ synonym_node = self._try_synonym_resolution(root_targets)
315
+ if synonym_node:
316
+ logger.info(
317
+ f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
318
+ f"resolved root concepts through synonyms"
319
+ )
320
+ return synonym_node
320
321
 
321
322
  return None
322
323
 
@@ -351,7 +352,10 @@ class RootNodeHandler:
351
352
  extra = restrict_node_outputs_targets(expanded, root_targets, self.ctx.depth)
352
353
 
353
354
  pseudonyms = [
354
- x for x in extra if any(x.address in y.pseudonyms for y in root_targets)
355
+ x
356
+ for x in extra
357
+ if any(x.address in y.pseudonyms for y in root_targets)
358
+ and x.address not in root_targets
355
359
  ]
356
360
 
357
361
  if pseudonyms:
@@ -1,7 +1,7 @@
1
1
  from typing import List
2
2
 
3
3
  from trilogy.constants import logger
4
- from trilogy.core.enums import Derivation, Granularity
4
+ from trilogy.core.enums import Derivation
5
5
  from trilogy.core.models.build import (
6
6
  BuildConcept,
7
7
  BuildRowsetItem,
@@ -52,14 +52,6 @@ def get_priority_concept(
52
52
  # pass_two = [c for c in all_concepts+filter_only if c.address not in attempted_addresses]
53
53
  for remaining_concept in (pass_one,):
54
54
  priority = (
55
- # find anything that needs no joins first, so we can exit early
56
- [
57
- c
58
- for c in remaining_concept
59
- if c.derivation == Derivation.CONSTANT
60
- and c.granularity == Granularity.SINGLE_ROW
61
- ]
62
- +
63
55
  # then multiselects to remove them from scope
64
56
  [c for c in remaining_concept if c.derivation == Derivation.MULTISELECT]
65
57
  +
@@ -34,6 +34,9 @@ def validate_concept(
34
34
  seen: set[str],
35
35
  environment: BuildEnvironment,
36
36
  ):
37
+ # logger.debug(
38
+ # f"Validating concept {concept.address} with accept_partial={accept_partial}"
39
+ # )
37
40
  found_map[str(node)].add(concept)
38
41
  seen.add(concept.address)
39
42
  if concept not in node.partial_concepts:
@@ -53,12 +56,21 @@ def validate_concept(
53
56
  found_map[str(node)].add(concept)
54
57
  for v_address in concept.pseudonyms:
55
58
  if v_address in seen:
56
- return
57
- v = environment.concepts[v_address]
59
+ continue
60
+ if v_address in environment.alias_origin_lookup:
61
+ # logger.debug(
62
+ # f"Found alias origin for {v_address}: {environment.alias_origin_lookup[v_address]} mapped to {environment.concepts[v_address]}")
63
+ v = environment.alias_origin_lookup[v_address]
64
+ else:
65
+ v = environment.concepts[v_address]
66
+
58
67
  if v.address in seen:
59
- return
68
+
69
+ continue
70
+
60
71
  if v.address == concept.address:
61
- return
72
+
73
+ continue
62
74
  validate_concept(
63
75
  v,
64
76
  node,
@@ -93,6 +105,7 @@ def validate_stack(
93
105
 
94
106
  for concept in resolved.output_concepts:
95
107
  if concept.address in resolved.hidden_concepts:
108
+
96
109
  continue
97
110
 
98
111
  validate_concept(
@@ -57,7 +57,13 @@ def build_parent_concepts(
57
57
  local_optional: List[BuildConcept],
58
58
  conditions: BuildWhereClause | None = None,
59
59
  depth: int = 0,
60
- ):
60
+ ) -> tuple[
61
+ list[BuildConcept],
62
+ list[tuple[BuildConcept, ...]],
63
+ list[BuildConcept],
64
+ bool,
65
+ bool,
66
+ ]:
61
67
  parent_row_concepts, parent_existence_concepts = resolve_filter_parent_concepts(
62
68
  concept, environment
63
69
  )
@@ -66,6 +72,10 @@ def build_parent_concepts(
66
72
  filter_where = concept.lineage.where
67
73
 
68
74
  same_filter_optional: list[BuildConcept] = []
75
+ # mypy struggled here? we shouldn't need explicit bools
76
+ global_filter_is_local_filter: bool = (
77
+ True if (conditions and conditions == filter_where) else False
78
+ )
69
79
 
70
80
  for x in local_optional:
71
81
  if isinstance(x.lineage, FILTER_TYPES):
@@ -79,7 +89,7 @@ def build_parent_concepts(
79
89
  parent_row_concepts.append(arg)
80
90
  same_filter_optional.append(x)
81
91
  continue
82
- elif conditions and conditions == filter_where:
92
+ elif global_filter_is_local_filter:
83
93
  same_filter_optional.append(x)
84
94
 
85
95
  # sometimes, it's okay to include other local optional above the filter
@@ -100,6 +110,7 @@ def build_parent_concepts(
100
110
  parent_existence_concepts,
101
111
  same_filter_optional,
102
112
  is_optimized_pushdown,
113
+ global_filter_is_local_filter,
103
114
  )
104
115
 
105
116
 
@@ -152,6 +163,7 @@ def gen_filter_node(
152
163
  parent_existence_concepts,
153
164
  same_filter_optional,
154
165
  optimized_pushdown,
166
+ global_filter_is_local_filter,
155
167
  ) = build_parent_concepts(
156
168
  concept,
157
169
  environment=environment,
@@ -187,7 +199,13 @@ def gen_filter_node(
187
199
  f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
188
200
  )
189
201
  return None
190
-
202
+ if global_filter_is_local_filter:
203
+ logger.info(
204
+ f"{padding(depth)}{LOGGER_PREFIX} filter node conditions match global conditions adding row parent {row_parent.output_concepts} with condition {where.conditional}"
205
+ )
206
+ row_parent.add_parents(core_parent_nodes)
207
+ row_parent.set_output_concepts([concept] + local_optional)
208
+ return row_parent
191
209
  if optimized_pushdown:
192
210
  logger.info(
193
211
  f"{padding(depth)}{LOGGER_PREFIX} returning optimized filter node with pushdown to parent with condition {where.conditional} across {[concept] + same_filter_optional + row_parent.output_concepts} "
@@ -211,7 +229,8 @@ def gen_filter_node(
211
229
  parent = row_parent
212
230
  parent.add_output_concepts([concept] + same_filter_optional)
213
231
  parent.add_parents(core_parent_nodes)
214
- parent.add_condition(where.conditional)
232
+ if not parent.preexisting_conditions == where.conditional:
233
+ parent.add_condition(where.conditional)
215
234
  parent.add_existence_concepts(flattened_existence, False)
216
235
  parent.grain = BuildGrain.from_concepts(
217
236
  parent.output_concepts,
@@ -225,7 +244,8 @@ def gen_filter_node(
225
244
  parents_for_grain = [
226
245
  x.lineage.content
227
246
  for x in filters
228
- if isinstance(x.lineage.content, BuildConcept)
247
+ if isinstance(x.lineage, BuildFilterItem)
248
+ and isinstance(x.lineage.content, BuildConcept)
229
249
  ]
230
250
  filter_node = FilterNode(
231
251
  input_concepts=unique(
@@ -133,7 +133,7 @@ def gen_group_node(
133
133
  )
134
134
  return group_node
135
135
  missing_optional = [
136
- x.address for x in local_optional if x.address not in group_node.output_concepts
136
+ x.address for x in local_optional if x.address not in group_node.usable_outputs
137
137
  ]
138
138
  if not missing_optional:
139
139
  logger.info(
@@ -42,6 +42,9 @@ def extract_concept(node: str, env: BuildEnvironment):
42
42
 
43
43
  def filter_unique_graphs(graphs: list[list[str]]) -> list[list[str]]:
44
44
  unique_graphs: list[set[str]] = []
45
+
46
+ # sort graphs from largest to smallest
47
+ graphs.sort(key=lambda x: len(x), reverse=True)
45
48
  for graph in graphs:
46
49
  if not any(set(graph).issubset(x) for x in unique_graphs):
47
50
  unique_graphs.append(set(graph))
@@ -110,12 +113,13 @@ def determine_induced_minimal_nodes(
110
113
 
111
114
  try:
112
115
  paths = nx.multi_source_dijkstra_path(H, nodelist)
113
- except nx.exception.NodeNotFound as e:
114
- logger.debug(f"Unable to find paths for {nodelist}- {str(e)}")
116
+ # logger.debug(f"Paths found for {nodelist}")
117
+ except nx.exception.NodeNotFound:
118
+ # logger.debug(f"Unable to find paths for {nodelist}- {str(e)}")
115
119
  return None
116
120
  H.remove_nodes_from(list(x for x in H.nodes if x not in paths))
117
121
  sG: nx.Graph = ax.steinertree.steiner_tree(H, nodelist).copy()
118
- logger.debug("Steiner tree found for nodes %s", nodelist)
122
+ # logger.debug(f"Steiner tree found for nodes {nodelist} {sG.nodes}")
119
123
  final: nx.DiGraph = nx.subgraph(G, sG.nodes).copy()
120
124
 
121
125
  for edge in G.edges:
@@ -154,12 +158,31 @@ def determine_induced_minimal_nodes(
154
158
  return final
155
159
 
156
160
 
161
+ def canonicalize_addresses(
162
+ reduced_concept_set: set[str], environment: BuildEnvironment
163
+ ) -> set[str]:
164
+ """
165
+ Convert a set of concept addresses to their canonical form.
166
+ This is necessary to ensure that we can compare concepts correctly,
167
+ especially when dealing with aliases or pseudonyms.
168
+ """
169
+ return set(
170
+ environment.concepts[x].address if x in environment.concepts else x
171
+ for x in reduced_concept_set
172
+ )
173
+
174
+
157
175
  def detect_ambiguity_and_raise(
158
- all_concepts: list[BuildConcept], reduced_concept_sets: list[set[str]]
176
+ all_concepts: list[BuildConcept],
177
+ reduced_concept_sets_raw: list[set[str]],
178
+ environment: BuildEnvironment,
159
179
  ) -> None:
160
180
  final_candidates: list[set[str]] = []
161
181
  common: set[str] = set()
162
182
  # find all values that show up in every join_additions
183
+ reduced_concept_sets = [
184
+ canonicalize_addresses(x, environment) for x in reduced_concept_sets_raw
185
+ ]
163
186
  for ja in reduced_concept_sets:
164
187
  if not common:
165
188
  common = ja
@@ -198,18 +221,21 @@ def filter_relevant_subgraphs(
198
221
 
199
222
 
200
223
  def filter_duplicate_subgraphs(
201
- subgraphs: list[list[BuildConcept]],
224
+ subgraphs: list[list[BuildConcept]], environment
202
225
  ) -> list[list[BuildConcept]]:
203
226
  seen: list[set[str]] = []
204
227
 
205
228
  for graph in subgraphs:
206
- seen.append(set([x.address for x in graph]))
229
+ seen.append(
230
+ canonicalize_addresses(set([x.address for x in graph]), environment)
231
+ )
207
232
  final = []
208
233
  # sometimes w can get two subcomponents that are the same
209
234
  # due to alias resolution
210
235
  # if so, drop any that are strict subsets.
211
236
  for graph in subgraphs:
212
- set_x = set([x.address for x in graph])
237
+ logger.info(f"Checking graph {graph} for duplicates in {seen}")
238
+ set_x = canonicalize_addresses(set([x.address for x in graph]), environment)
213
239
  if any([set_x.issubset(y) and set_x != y for y in seen]):
214
240
  continue
215
241
  final.append(graph)
@@ -295,7 +321,7 @@ def resolve_weak_components(
295
321
  if not found:
296
322
  return None
297
323
 
298
- detect_ambiguity_and_raise(all_concepts, reduced_concept_sets)
324
+ detect_ambiguity_and_raise(all_concepts, reduced_concept_sets, environment)
299
325
 
300
326
  # take our first one as the actual graph
301
327
  g = found[0]
@@ -316,7 +342,7 @@ def resolve_weak_components(
316
342
  if not sub_component:
317
343
  continue
318
344
  subgraphs.append(sub_component)
319
- final = filter_duplicate_subgraphs(subgraphs)
345
+ final = filter_duplicate_subgraphs(subgraphs, environment)
320
346
  return final
321
347
  # return filter_relevant_subgraphs(subgraphs)
322
348
 
@@ -361,17 +387,25 @@ def subgraphs_to_merge_node(
361
387
  )
362
388
  parents.append(parent)
363
389
  input_c = []
390
+ output_c = []
364
391
  for x in parents:
365
392
  for y in x.usable_outputs:
366
393
  input_c.append(y)
394
+ if y in output_concepts:
395
+ output_c.append(y)
396
+ elif any(y.address in c.pseudonyms for c in output_concepts) or any(
397
+ c.address in y.pseudonyms for c in output_concepts
398
+ ):
399
+ output_c.append(y)
400
+
367
401
  if len(parents) == 1 and enable_early_exit:
368
402
  logger.info(
369
403
  f"{padding(depth)}{LOGGER_PREFIX} only one parent node, exiting early w/ {[c.address for c in parents[0].output_concepts]}"
370
404
  )
371
405
  return parents[0]
372
- return MergeNode(
406
+ rval = MergeNode(
373
407
  input_concepts=unique(input_c, "address"),
374
- output_concepts=output_concepts,
408
+ output_concepts=output_c,
375
409
  environment=environment,
376
410
  parents=parents,
377
411
  depth=depth,
@@ -381,6 +415,7 @@ def subgraphs_to_merge_node(
381
415
  # preexisting_conditions=search_conditions.conditional,
382
416
  # node_joins=[]
383
417
  )
418
+ return rval
384
419
 
385
420
 
386
421
  def gen_merge_node(
@@ -437,24 +472,4 @@ def gen_merge_node(
437
472
  search_conditions=search_conditions,
438
473
  output_concepts=all_concepts,
439
474
  )
440
-
441
- # one concept handling may need to be kicked to alias
442
- if len(all_search_concepts) == 1:
443
- concept = all_search_concepts[0]
444
- for v in concept.pseudonyms:
445
- test = subgraphs_to_merge_node(
446
- [[concept, environment.alias_origin_lookup[v]]],
447
- g=g,
448
- all_concepts=[concept],
449
- environment=environment,
450
- depth=depth,
451
- source_concepts=source_concepts,
452
- history=history,
453
- conditions=conditions,
454
- enable_early_exit=False,
455
- search_conditions=search_conditions,
456
- output_concepts=[concept],
457
- )
458
- if test:
459
- return test
460
475
  return None