pytrilogy 0.0.3.63__tar.gz → 0.0.3.65__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 (149) hide show
  1. {pytrilogy-0.0.3.63/pytrilogy.egg-info → pytrilogy-0.0.3.65}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_select.py +1 -0
  4. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/graph_models.py +45 -1
  6. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/build.py +6 -1
  7. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/environment.py +15 -11
  8. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/execute.py +30 -58
  9. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/concept_strategies_v3.py +31 -15
  10. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/discovery_node_factory.py +2 -3
  11. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/node_merge_node.py +4 -2
  12. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +3 -1
  13. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/select_merge_node.py +65 -26
  14. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/synonym_node.py +4 -2
  15. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/__init__.py +11 -29
  16. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/statements/author.py +1 -1
  17. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/base.py +7 -0
  18. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/hooks/graph_hook.py +65 -12
  19. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/common.py +2 -2
  20. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/render.py +5 -1
  21. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/LICENSE.md +0 -0
  22. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/README.md +0 -0
  23. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/pyproject.toml +0 -0
  24. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/pytrilogy.egg-info/SOURCES.txt +0 -0
  25. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/pytrilogy.egg-info/dependency_links.txt +0 -0
  26. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/pytrilogy.egg-info/entry_points.txt +0 -0
  27. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/pytrilogy.egg-info/requires.txt +0 -0
  28. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/pytrilogy.egg-info/top_level.txt +0 -0
  29. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/setup.cfg +0 -0
  30. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/setup.py +0 -0
  31. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_datatypes.py +0 -0
  32. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_declarations.py +0 -0
  33. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_derived_concepts.py +0 -0
  34. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_discovery_nodes.py +0 -0
  35. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_enums.py +0 -0
  36. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_environment.py +0 -0
  37. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_executor.py +0 -0
  38. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_failure.py +0 -0
  39. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_functions.py +0 -0
  40. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_imports.py +0 -0
  41. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_metadata.py +0 -0
  42. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_models.py +0 -0
  43. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_multi_join_assignments.py +0 -0
  44. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_parse_engine.py +0 -0
  45. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_parsing.py +0 -0
  46. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_parsing_failures.py +0 -0
  47. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_query_render.py +0 -0
  50. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_show.py +0 -0
  51. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_statements.py +0 -0
  52. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_typing.py +0 -0
  53. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_undefined_concept.py +0 -0
  54. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_user_functions.py +0 -0
  55. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/tests/test_where_clause.py +0 -0
  56. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/authoring/__init__.py +0 -0
  57. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/compiler.py +0 -0
  58. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/constants.py +0 -0
  59. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/__init__.py +0 -0
  60. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/constants.py +0 -0
  61. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/enums.py +0 -0
  62. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/env_processor.py +0 -0
  63. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/environment_helpers.py +0 -0
  64. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/ergonomics.py +0 -0
  65. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/exceptions.py +0 -0
  66. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/functions.py +0 -0
  67. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/internal.py +0 -0
  68. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/__init__.py +0 -0
  69. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/author.py +0 -0
  70. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/build_environment.py +0 -0
  71. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/core.py +0 -0
  72. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/models/datasource.py +0 -0
  73. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/optimization.py +0 -0
  74. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/optimizations/__init__.py +0 -0
  75. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/optimizations/base_optimization.py +0 -0
  76. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/optimizations/inline_datasource.py +0 -0
  77. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  78. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/__init__.py +0 -0
  79. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/discovery_loop.py +0 -0
  80. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/discovery_utility.py +0 -0
  81. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/discovery_validation.py +0 -0
  82. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/graph_utils.py +0 -0
  83. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/__init__.py +0 -0
  84. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  85. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/common.py +0 -0
  86. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  87. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/group_node.py +0 -0
  88. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  89. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  90. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  91. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  92. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  93. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/select_node.py +0 -0
  94. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/union_node.py +0 -0
  95. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  96. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/node_generators/window_node.py +0 -0
  97. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/base_node.py +0 -0
  98. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/filter_node.py +0 -0
  99. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/group_node.py +0 -0
  100. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/merge_node.py +0 -0
  101. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  102. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  103. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/union_node.py +0 -0
  104. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  105. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/nodes/window_node.py +0 -0
  106. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/processing/utility.py +0 -0
  107. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/query_processor.py +0 -0
  108. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/statements/__init__.py +0 -0
  109. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/statements/build.py +0 -0
  110. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/statements/common.py +0 -0
  111. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/statements/execute.py +0 -0
  112. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/core/utility.py +0 -0
  113. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/__init__.py +0 -0
  114. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/bigquery.py +0 -0
  115. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/common.py +0 -0
  116. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/config.py +0 -0
  117. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/dataframe.py +0 -0
  118. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/duckdb.py +0 -0
  119. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/enums.py +0 -0
  120. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/postgres.py +0 -0
  121. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/presto.py +0 -0
  122. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/snowflake.py +0 -0
  123. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/dialect/sql_server.py +0 -0
  124. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/engine.py +0 -0
  125. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/executor.py +0 -0
  126. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/hooks/__init__.py +0 -0
  127. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/hooks/base_hook.py +0 -0
  128. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/hooks/query_debugger.py +0 -0
  129. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/metadata/__init__.py +0 -0
  130. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parser.py +0 -0
  131. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/__init__.py +0 -0
  132. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/config.py +0 -0
  133. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/exceptions.py +0 -0
  134. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/helpers.py +0 -0
  135. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/parse_engine.py +0 -0
  136. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/parsing/trilogy.lark +0 -0
  137. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/py.typed +0 -0
  138. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/render.py +0 -0
  139. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/scripts/__init__.py +0 -0
  140. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/scripts/trilogy.py +0 -0
  141. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/__init__.py +0 -0
  142. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/date.preql +0 -0
  143. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/display.preql +0 -0
  144. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/geography.preql +0 -0
  145. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/money.preql +0 -0
  146. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/net.preql +0 -0
  147. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/ranking.preql +0 -0
  148. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/std/report.preql +0 -0
  149. {pytrilogy-0.0.3.63 → pytrilogy-0.0.3.65}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.63
3
+ Version: 0.0.3.65
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.63
3
+ Version: 0.0.3.65
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -122,6 +122,7 @@ def test_double_aggregate():
122
122
 
123
123
 
124
124
  def test_modifiers():
125
+
125
126
  q1 = """
126
127
  const a <- 1;
127
128
  const b <- 2;
@@ -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.63"
7
+ __version__ = "0.0.3.65"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -1,6 +1,50 @@
1
1
  import networkx as nx
2
2
 
3
- from trilogy.core.models.build import BuildConcept, BuildDatasource
3
+ from trilogy.core.models.build import BuildConcept, BuildDatasource, BuildWhereClause
4
+
5
+
6
+ def get_graph_exact_match(
7
+ g: nx.DiGraph, conditions: BuildWhereClause | None
8
+ ) -> set[str]:
9
+ datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
10
+ nx.get_node_attributes(g, "datasource")
11
+ )
12
+ exact: set[str] = set()
13
+ for node in g.nodes:
14
+ if node in datasources:
15
+ ds = datasources[node]
16
+ if isinstance(ds, list):
17
+ exact.add(node)
18
+ continue
19
+
20
+ if not conditions and not ds.non_partial_for:
21
+ exact.add(node)
22
+ continue
23
+ elif conditions:
24
+ if not ds.non_partial_for:
25
+ continue
26
+ if ds.non_partial_for and conditions == ds.non_partial_for:
27
+ exact.add(node)
28
+ continue
29
+ else:
30
+ continue
31
+
32
+ return exact
33
+
34
+
35
+ def prune_sources_for_conditions(
36
+ g: nx.DiGraph,
37
+ conditions: BuildWhereClause | None,
38
+ ):
39
+
40
+ complete = get_graph_exact_match(g, conditions)
41
+ to_remove = []
42
+ for node in g.nodes:
43
+ if node.startswith("ds~") and node not in complete:
44
+ to_remove.append(node)
45
+
46
+ for node in to_remove:
47
+ g.remove_node(node)
4
48
 
5
49
 
6
50
  def concept_to_node(input: BuildConcept) -> str:
@@ -1586,7 +1586,10 @@ class Factory:
1586
1586
 
1587
1587
  return BuildFunction.model_construct(
1588
1588
  operator=base.operator,
1589
- arguments=[rval, *[self.build(c) for c in raw_args[1:]]],
1589
+ arguments=[
1590
+ rval,
1591
+ *[self.handle_constant(self.build(c)) for c in raw_args[1:]],
1592
+ ],
1590
1593
  output_datatype=base.output_datatype,
1591
1594
  output_purpose=base.output_purpose,
1592
1595
  valid_inputs=base.valid_inputs,
@@ -2042,4 +2045,6 @@ class Factory:
2042
2045
  and base.lineage.operator == FunctionType.CONSTANT
2043
2046
  ):
2044
2047
  return BuildParamaterizedConceptReference(concept=base)
2048
+ elif isinstance(base, ConceptRef):
2049
+ return self.handle_constant(self.build(base))
2045
2050
  return base
@@ -603,7 +603,7 @@ class Environment(BaseModel):
603
603
  # too hacky for maintainability
604
604
  if current_derivation not in (Derivation.ROOT, Derivation.CONSTANT):
605
605
  logger.info(
606
- f"A datasource has been added which will persist derived concept {new_persisted_concept.address}"
606
+ f"A datasource has been added which will persist derived concept {new_persisted_concept.address} with derivation {current_derivation}"
607
607
  )
608
608
  persisted = f"{PERSISTED_CONCEPT_PREFIX}_" + new_persisted_concept.name
609
609
  # override the current concept source to reflect that it's now coming from a datasource
@@ -622,17 +622,21 @@ class Environment(BaseModel):
622
622
  meta=meta,
623
623
  force=True,
624
624
  )
625
+ base = {
626
+ "lineage": None,
627
+ "metadata": new_persisted_concept.metadata.model_copy(
628
+ update={"concept_source": ConceptSource.PERSIST_STATEMENT}
629
+ ),
630
+ "derivation": Derivation.ROOT,
631
+ "purpose": new_persisted_concept.purpose,
632
+ }
633
+ # purpose is used in derivation calculation
634
+ # which should be fixed, but we'll do in a followup
635
+ # so override here
636
+ if new_persisted_concept.purpose == Purpose.CONSTANT:
637
+ base["purpose"] = Purpose.KEY
625
638
  new_persisted_concept = new_persisted_concept.model_copy(
626
- deep=True,
627
- update={
628
- "lineage": None,
629
- "metadata": new_persisted_concept.metadata.model_copy(
630
- update={
631
- "concept_source": ConceptSource.PERSIST_STATEMENT
632
- }
633
- ),
634
- "derivation": Derivation.ROOT,
635
- },
639
+ deep=True, update=base
636
640
  )
637
641
  self.add_concept(
638
642
  new_persisted_concept,
@@ -56,6 +56,12 @@ LOGGER_PREFIX = "[MODELS_EXECUTE]"
56
56
  DATASOURCE_TYPES = (BuildDatasource, BuildDatasource)
57
57
 
58
58
 
59
+ class InlinedCTE(BaseModel):
60
+ original_alias: str
61
+ new_alias: str
62
+ new_base: str
63
+
64
+
59
65
  class CTE(BaseModel):
60
66
  name: str
61
67
  source: "QueryDatasource"
@@ -78,6 +84,7 @@ class CTE(BaseModel):
78
84
  limit: Optional[int] = None
79
85
  base_name_override: Optional[str] = None
80
86
  base_alias_override: Optional[str] = None
87
+ inlined_ctes: dict[str, InlinedCTE] = Field(default_factory=dict)
81
88
 
82
89
  @field_validator("join_derived_concepts")
83
90
  def validate_join_derived_concepts(cls, v):
@@ -104,62 +111,6 @@ class CTE(BaseModel):
104
111
  def validate_output_columns(cls, v):
105
112
  return unique(v, "address")
106
113
 
107
- def inline_constant(self, concept: BuildConcept):
108
- if not concept.derivation == Derivation.CONSTANT:
109
- return False
110
- if not isinstance(concept.lineage, BuildFunction):
111
- return False
112
- if not concept.lineage.operator == FunctionType.CONSTANT:
113
- return False
114
- # remove the constant
115
- removed: set = set()
116
- if concept.address in self.source_map:
117
- removed = removed.union(self.source_map[concept.address])
118
- del self.source_map[concept.address]
119
-
120
- if self.condition:
121
- self.condition = self.condition.inline_constant(concept)
122
- # if we've entirely removed the need to join to someplace to get the concept
123
- # drop the join as well.
124
- for removed_cte in removed:
125
- still_required = any(
126
- [
127
- removed_cte in x
128
- for x in self.source_map.values()
129
- or self.existence_source_map.values()
130
- ]
131
- )
132
- if not still_required:
133
- self.joins = [
134
- join
135
- for join in self.joins
136
- if not isinstance(join, Join)
137
- or (
138
- isinstance(join, Join)
139
- and (
140
- join.right_cte.name != removed_cte
141
- and any(
142
- [
143
- x.cte.name != removed_cte
144
- for x in (join.joinkey_pairs or [])
145
- ]
146
- )
147
- )
148
- )
149
- ]
150
- for join in self.joins:
151
- if isinstance(join, UnnestJoin) and concept in join.concepts:
152
- join.rendering_required = False
153
-
154
- self.parent_ctes = [
155
- x for x in self.parent_ctes if x.name != removed_cte
156
- ]
157
- if removed_cte == self.base_name_override:
158
- candidates = [x.name for x in self.parent_ctes]
159
- self.base_name_override = candidates[0] if candidates else None
160
- self.base_alias_override = candidates[0] if candidates else None
161
- return True
162
-
163
114
  @property
164
115
  def comment(self) -> str:
165
116
  base = f"Target: {str(self.grain)}. Group: {self.group_to_grain}"
@@ -240,7 +191,18 @@ class CTE(BaseModel):
240
191
  ]
241
192
  elif v == parent.safe_identifier:
242
193
  self.source_map[k] = [ds_being_inlined.safe_identifier]
243
-
194
+ for k, v in self.existence_source_map.items():
195
+ if isinstance(v, list):
196
+ self.existence_source_map[k] = [
197
+ (
198
+ ds_being_inlined.safe_identifier
199
+ if x == parent.safe_identifier
200
+ else x
201
+ )
202
+ for x in v
203
+ ]
204
+ elif v == parent.safe_identifier:
205
+ self.existence_source_map[k] = [ds_being_inlined.safe_identifier]
244
206
  # zip in any required values for lookups
245
207
  for k in ds_being_inlined.output_lcl.addresses:
246
208
  if k in self.source_map and self.source_map[k]:
@@ -251,6 +213,11 @@ class CTE(BaseModel):
251
213
  ]
252
214
  if force_group:
253
215
  self.group_to_grain = True
216
+ self.inlined_ctes[ds_being_inlined.safe_identifier] = InlinedCTE(
217
+ original_alias=parent.name,
218
+ new_alias=ds_being_inlined.safe_identifier,
219
+ new_base=ds_being_inlined.safe_location,
220
+ )
254
221
  return True
255
222
 
256
223
  def __add__(self, other: "CTE" | "UnionCTE"):
@@ -303,6 +270,10 @@ class CTE(BaseModel):
303
270
  **self.existence_source_map,
304
271
  **other.existence_source_map,
305
272
  }
273
+ self.inlined_ctes = {
274
+ **self.inlined_ctes,
275
+ **other.inlined_ctes,
276
+ }
306
277
 
307
278
  return self
308
279
 
@@ -672,7 +643,7 @@ class QueryDatasource(BaseModel):
672
643
  and CONFIG.validate_missing
673
644
  ):
674
645
  raise SyntaxError(
675
- f"On query datasource from {values} missing source map entry (map: {v}) for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
646
+ f"Missing source map entry for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have map: {v}"
676
647
  )
677
648
  return v
678
649
 
@@ -1057,6 +1028,7 @@ class UnionCTE(BaseModel):
1057
1028
  hidden_concepts: set[str] = Field(default_factory=set)
1058
1029
  partial_concepts: list[BuildConcept] = Field(default_factory=list)
1059
1030
  existence_source_map: Dict[str, list[str]] = Field(default_factory=dict)
1031
+ inlined_ctes: Dict[str, InlinedCTE] = Field(default_factory=dict)
1060
1032
 
1061
1033
  @computed_field # type: ignore
1062
1034
  @property
@@ -54,11 +54,7 @@ def generate_candidates_restrictive(
54
54
  exhausted: set[str],
55
55
  depth: int,
56
56
  conditions: BuildWhereClause | None = None,
57
- ) -> List[BuildConcept]:
58
- # if it's single row, joins are irrelevant. Fetch without keys.
59
- if priority_concept.granularity == Granularity.SINGLE_ROW:
60
- return []
61
-
57
+ ) -> tuple[list[BuildConcept], BuildWhereClause | None]:
62
58
  local_candidates = [
63
59
  x
64
60
  for x in list(candidates)
@@ -71,8 +67,16 @@ def generate_candidates_restrictive(
71
67
  logger.info(
72
68
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Injecting additional conditional row arguments as all remaining concepts are roots or constant"
73
69
  )
74
- return unique(list(conditions.row_arguments) + local_candidates, "address")
75
- return local_candidates
70
+ # otherwise, we can ignore the conditions now that we've injected inputs
71
+ return (
72
+ unique(list(conditions.row_arguments) + local_candidates, "address"),
73
+ None,
74
+ )
75
+ # if it's single row, joins are irrelevant. Fetch without keys.
76
+ if priority_concept.granularity == Granularity.SINGLE_ROW:
77
+ return [], conditions
78
+
79
+ return local_candidates, conditions
76
80
 
77
81
 
78
82
  def append_existence_check(
@@ -104,9 +108,7 @@ def append_existence_check(
104
108
  )
105
109
  assert parent, "Could not resolve existence clause"
106
110
  node.add_parents([parent])
107
- logger.info(
108
- f"{LOGGER_PREFIX} fetching existence clause inputs {[str(c) for c in subselect]}"
109
- )
111
+ logger.info(f"{LOGGER_PREFIX} found {[str(c) for c in subselect]}")
110
112
  node.add_existence_concepts([*subselect])
111
113
 
112
114
 
@@ -440,7 +442,19 @@ def _search_concepts(
440
442
  accept_partial: bool = False,
441
443
  conditions: BuildWhereClause | None = None,
442
444
  ) -> StrategyNode | None:
445
+ # check for direct materialization first
446
+ candidate = history.gen_select_node(
447
+ mandatory_list,
448
+ environment,
449
+ g,
450
+ depth + 1,
451
+ fail_if_not_found=False,
452
+ accept_partial=accept_partial,
453
+ conditions=conditions,
454
+ )
443
455
 
456
+ if candidate:
457
+ return candidate
444
458
  context = initialize_loop_context(
445
459
  mandatory_list=mandatory_list,
446
460
  environment=environment,
@@ -460,19 +474,21 @@ def _search_concepts(
460
474
  )
461
475
 
462
476
  local_conditions = evaluate_loop_conditions(context, priority_concept)
463
- logger.info(
464
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} priority concept is {str(priority_concept)} derivation {priority_concept.derivation} granularity {priority_concept.granularity} with conditions {local_conditions}"
465
- )
466
477
 
467
478
  candidates = [
468
479
  c for c in context.mandatory_list if c.address != priority_concept.address
469
480
  ]
470
- candidate_list = generate_candidates_restrictive(
481
+ # the local conditions list may be override if we end up injecting conditions
482
+ candidate_list, local_conditions = generate_candidates_restrictive(
471
483
  priority_concept,
472
484
  candidates,
473
485
  context.skip,
474
486
  depth=depth,
475
- conditions=context.conditions,
487
+ conditions=local_conditions,
488
+ )
489
+
490
+ logger.info(
491
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} priority concept is {str(priority_concept)} derivation {priority_concept.derivation} granularity {priority_concept.granularity} with conditions {local_conditions}"
476
492
  )
477
493
 
478
494
  logger.info(
@@ -438,15 +438,14 @@ def generate_node(
438
438
  )
439
439
 
440
440
  # Try materialized concept first
441
+ # this is worth checking every loop iteration
441
442
  candidate = history.gen_select_node(
442
- concept,
443
- local_optional,
443
+ [concept] + local_optional,
444
444
  environment,
445
445
  g,
446
446
  depth + 1,
447
447
  fail_if_not_found=False,
448
448
  accept_partial=accept_partial,
449
- accept_partial_optional=False,
450
449
  conditions=conditions,
451
450
  )
452
451
 
@@ -6,7 +6,7 @@ from networkx.algorithms import approximation as ax
6
6
  from trilogy.constants import logger
7
7
  from trilogy.core.enums import Derivation
8
8
  from trilogy.core.exceptions import AmbiguousRelationshipResolutionException
9
- from trilogy.core.graph_models import concept_to_node
9
+ from trilogy.core.graph_models import concept_to_node, prune_sources_for_conditions
10
10
  from trilogy.core.models.build import BuildConcept, BuildConditional, BuildWhereClause
11
11
  from trilogy.core.models.build_environment import BuildEnvironment
12
12
  from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
@@ -222,10 +222,12 @@ def resolve_weak_components(
222
222
  environment_graph: nx.DiGraph,
223
223
  filter_downstream: bool = True,
224
224
  accept_partial: bool = False,
225
+ search_conditions: BuildWhereClause | None = None,
225
226
  ) -> list[list[BuildConcept]] | None:
226
227
  break_flag = False
227
228
  found = []
228
229
  search_graph = environment_graph.copy()
230
+ prune_sources_for_conditions(search_graph, conditions=search_conditions)
229
231
  reduced_concept_sets: list[set[str]] = []
230
232
 
231
233
  # loop through, removing new nodes we find
@@ -239,7 +241,6 @@ def resolve_weak_components(
239
241
  if "__preql_internal" not in c.address
240
242
  ]
241
243
  )
242
- logger.debug(f"Resolving weak components for {node_list} in {search_graph.nodes}")
243
244
  synonyms: set[str] = set()
244
245
  for x in all_concepts:
245
246
  synonyms = synonyms.union(x.pseudonyms)
@@ -407,6 +408,7 @@ def gen_merge_node(
407
408
  g,
408
409
  filter_downstream=filter_downstream,
409
410
  accept_partial=accept_partial,
411
+ search_conditions=search_conditions,
410
412
  )
411
413
  if not weak_resolve:
412
414
  logger.info(
@@ -169,7 +169,9 @@ def is_fully_covered(
169
169
  return current_end >= end
170
170
 
171
171
 
172
- def get_union_sources(datasources: list[BuildDatasource], concepts: list[BuildConcept]):
172
+ def get_union_sources(
173
+ datasources: list[BuildDatasource], concepts: list[BuildConcept]
174
+ ) -> List[list[BuildDatasource]]:
173
175
  candidates: list[BuildDatasource] = []
174
176
  for x in datasources:
175
177
  if all([c.address in x.output_concepts for c in concepts]):
@@ -5,7 +5,11 @@ import networkx as nx
5
5
 
6
6
  from trilogy.constants import logger
7
7
  from trilogy.core.enums import Derivation
8
- from trilogy.core.graph_models import concept_to_node
8
+ from trilogy.core.graph_models import (
9
+ concept_to_node,
10
+ get_graph_exact_match,
11
+ prune_sources_for_conditions,
12
+ )
9
13
  from trilogy.core.models.build import (
10
14
  BuildConcept,
11
15
  BuildDatasource,
@@ -57,26 +61,6 @@ def get_graph_partial_nodes(
57
61
  return partial
58
62
 
59
63
 
60
- def get_graph_exact_match(
61
- g: nx.DiGraph, conditions: BuildWhereClause | None
62
- ) -> set[str]:
63
- datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
64
- nx.get_node_attributes(g, "datasource")
65
- )
66
- exact: set[str] = set()
67
- for node in g.nodes:
68
- if node in datasources:
69
- ds = datasources[node]
70
- if not isinstance(ds, list):
71
- if ds.non_partial_for and conditions == ds.non_partial_for:
72
- exact.add(node)
73
- continue
74
- else:
75
- continue
76
-
77
- return exact
78
-
79
-
80
64
  def get_graph_grains(g: nx.DiGraph) -> dict[str, list[str]]:
81
65
  datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
82
66
  nx.get_node_attributes(g, "datasource")
@@ -95,6 +79,34 @@ def get_graph_grains(g: nx.DiGraph) -> dict[str, list[str]]:
95
79
  return grain_length
96
80
 
97
81
 
82
+ def subgraph_is_complete(
83
+ nodes: list[str], targets: set[str], mapping: dict[str, str], g: nx.DiGraph
84
+ ) -> bool:
85
+ mapped = set([mapping.get(n, n) for n in nodes])
86
+ passed = all([t in mapped for t in targets])
87
+ if not passed:
88
+ logger.info(
89
+ f"Subgraph {nodes} is not complete, missing targets {targets} - mapped {mapped}"
90
+ )
91
+ return False
92
+ # check if all concepts have a datasource edge
93
+ has_ds_edge = {
94
+ mapping.get(n, n): any(x.startswith("ds~") for x in nx.neighbors(g, n))
95
+ for n in nodes
96
+ if n.startswith("c~")
97
+ }
98
+ has_ds_edge = {k: False for k in targets}
99
+ # check at least one instance of concept has a datasource edge
100
+ for n in nodes:
101
+ if n.startswith("c~"):
102
+ neighbors = nx.neighbors(g, n)
103
+ for neighbor in neighbors:
104
+ if neighbor.startswith("ds~"):
105
+ has_ds_edge[mapping.get(n, n)] = True
106
+ break
107
+ return all(has_ds_edge.values()) and passed
108
+
109
+
98
110
  def create_pruned_concept_graph(
99
111
  g: nx.DiGraph,
100
112
  all_concepts: List[BuildConcept],
@@ -104,6 +116,7 @@ def create_pruned_concept_graph(
104
116
  depth: int = 0,
105
117
  ) -> nx.DiGraph:
106
118
  orig_g = g
119
+
107
120
  g = g.copy()
108
121
  union_options = get_union_sources(datasources, all_concepts)
109
122
  for ds_list in union_options:
@@ -114,7 +127,8 @@ def create_pruned_concept_graph(
114
127
  g.add_node(node_address, datasource=ds_list)
115
128
  for c in common:
116
129
  g.add_edge(node_address, concept_to_node(c))
117
-
130
+ g.add_edge(concept_to_node(c), node_address)
131
+ prune_sources_for_conditions(g, conditions)
118
132
  target_addresses = set([c.address for c in all_concepts])
119
133
  concepts: dict[str, BuildConcept] = nx.get_node_attributes(orig_g, "concept")
120
134
  datasource_map: dict[str, BuildDatasource | list[BuildDatasource]] = (
@@ -126,8 +140,7 @@ def create_pruned_concept_graph(
126
140
  # filter out synonyms
127
141
  if (x := concepts.get(n, None)) and x.address in target_addresses
128
142
  }
129
- # from trilogy.hooks.graph_hook import GraphHook
130
- # GraphHook().query_graph_built(g)
143
+
131
144
  relevant_concepts: list[str] = list(relevant_concepts_pre.keys())
132
145
  relevent_datasets: list[str] = []
133
146
  if not accept_partial:
@@ -149,6 +162,7 @@ def create_pruned_concept_graph(
149
162
  to_remove.append(edge)
150
163
  for edge in to_remove:
151
164
  g.remove_edge(*edge)
165
+
152
166
  for n in g.nodes():
153
167
  if not n.startswith("ds~"):
154
168
  continue
@@ -181,8 +195,15 @@ def create_pruned_concept_graph(
181
195
  if n not in relevent_datasets and n not in relevant_concepts
182
196
  ]
183
197
  )
184
-
198
+ # from trilogy.hooks.graph_hook import GraphHook
199
+ # GraphHook().query_graph_built(g)
185
200
  subgraphs = list(nx.connected_components(g.to_undirected()))
201
+ subgraphs = [
202
+ s
203
+ for s in subgraphs
204
+ if subgraph_is_complete(s, target_addresses, relevant_concepts_pre, g)
205
+ ]
206
+
186
207
  if not subgraphs:
187
208
  logger.info(
188
209
  f"{padding(depth)}{LOGGER_PREFIX} cannot resolve root graph - no subgraphs after node prune"
@@ -486,6 +507,24 @@ def gen_select_merge_node(
486
507
  non_constant = [c for c in all_concepts if c.derivation != Derivation.CONSTANT]
487
508
  constants = [c for c in all_concepts if c.derivation == Derivation.CONSTANT]
488
509
  if not non_constant and constants:
510
+ logger.info(
511
+ f"{padding(depth)}{LOGGER_PREFIX} only constant inputs to discovery ({constants}), returning constant node directly"
512
+ )
513
+ for x in constants:
514
+ logger.info(
515
+ f"{padding(depth)}{LOGGER_PREFIX} {x} {x.lineage} {x.derivation}"
516
+ )
517
+ if conditions:
518
+ if not all(
519
+ [x.derivation == Derivation.CONSTANT for x in conditions.row_arguments]
520
+ ):
521
+ logger.info(
522
+ f"{padding(depth)}{LOGGER_PREFIX} conditions being passed in to constant node {conditions}, but not all concepts are constants."
523
+ )
524
+ return None
525
+ else:
526
+ constants += conditions.row_arguments
527
+
489
528
  return ConstantNode(
490
529
  output_concepts=constants,
491
530
  input_concepts=[],
@@ -494,7 +533,7 @@ def gen_select_merge_node(
494
533
  depth=depth,
495
534
  partial_concepts=[],
496
535
  force_group=False,
497
- preexisting_conditions=conditions.conditional if conditions else None,
536
+ conditions=conditions.conditional if conditions else None,
498
537
  )
499
538
  for attempt in [False, True]:
500
539
  pruned_concept_graph = create_pruned_concept_graph(
@@ -50,7 +50,9 @@ def gen_synonym_node(
50
50
 
51
51
  logger.info(f"{local_prefix} Generating Synonym Node with {len(synonyms)} synonyms")
52
52
  sorted_keys = sorted(synonyms.keys())
53
- combinations_list = list(itertools.product(*(synonyms[obj] for obj in sorted_keys)))
53
+ combinations_list: list[tuple[BuildConcept, ...]] = list(
54
+ itertools.product(*(synonyms[obj] for obj in sorted_keys))
55
+ )
54
56
 
55
57
  def similarity_sort_key(combo):
56
58
  addresses = [x.address for x in combo]
@@ -83,7 +85,7 @@ def gen_synonym_node(
83
85
  f"{local_prefix} checking combination {fingerprint} with {len(combo)} concepts"
84
86
  )
85
87
  attempt: StrategyNode | None = source_concepts(
86
- combo,
88
+ list(combo),
87
89
  history=history,
88
90
  environment=environment,
89
91
  depth=depth,