pytrilogy 0.0.2.26__tar.gz → 0.0.2.27__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 (107) hide show
  1. {pytrilogy-0.0.2.26/pytrilogy.egg-info → pytrilogy-0.0.2.27}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_parsing.py +22 -0
  4. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_partial_handling.py +1 -1
  5. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_query_processing.py +1 -1
  6. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/graph_models.py +2 -2
  8. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/models.py +111 -85
  9. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/inline_datasource.py +4 -4
  10. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/select_merge_node.py +7 -1
  11. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/base_node.py +3 -0
  12. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/merge_node.py +10 -10
  13. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/select_node_v2.py +6 -2
  14. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/utility.py +3 -3
  15. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/query_processor.py +21 -17
  16. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/query_debugger.py +5 -1
  17. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/parse_engine.py +17 -14
  18. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/render.py +25 -7
  19. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/trilogy.lark +4 -2
  20. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/LICENSE.md +0 -0
  21. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/README.md +0 -0
  22. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pyproject.toml +0 -0
  23. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/SOURCES.txt +0 -0
  24. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/dependency_links.txt +0 -0
  25. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/entry_points.txt +0 -0
  26. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/requires.txt +0 -0
  27. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/pytrilogy.egg-info/top_level.txt +0 -0
  28. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/setup.cfg +0 -0
  29. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/setup.py +0 -0
  30. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_datatypes.py +0 -0
  31. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_declarations.py +0 -0
  32. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_derived_concepts.py +0 -0
  33. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_discovery_nodes.py +0 -0
  34. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_environment.py +0 -0
  35. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_functions.py +0 -0
  36. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_imports.py +0 -0
  37. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_metadata.py +0 -0
  38. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_models.py +0 -0
  39. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_multi_join_assignments.py +0 -0
  40. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_select.py +0 -0
  41. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_show.py +0 -0
  42. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_statements.py +0 -0
  43. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_undefined_concept.py +0 -0
  44. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/tests/test_where_clause.py +0 -0
  45. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/compiler.py +0 -0
  46. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/constants.py +0 -0
  47. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/__init__.py +0 -0
  48. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/constants.py +0 -0
  49. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/enums.py +0 -0
  50. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/env_processor.py +0 -0
  51. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/environment_helpers.py +0 -0
  52. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/ergonomics.py +0 -0
  53. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/exceptions.py +0 -0
  54. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/functions.py +0 -0
  55. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/internal.py +0 -0
  56. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimization.py +0 -0
  57. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/__init__.py +0 -0
  58. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/base_optimization.py +0 -0
  59. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/inline_constant.py +0 -0
  60. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  61. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/__init__.py +0 -0
  62. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  63. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/graph_utils.py +0 -0
  64. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/__init__.py +0 -0
  65. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  66. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/common.py +0 -0
  67. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  68. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/group_node.py +0 -0
  69. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  70. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  71. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  72. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  73. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/select_node.py +0 -0
  74. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  75. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/node_generators/window_node.py +0 -0
  76. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/__init__.py +0 -0
  77. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/filter_node.py +0 -0
  78. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/group_node.py +0 -0
  79. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  80. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/core/processing/nodes/window_node.py +0 -0
  81. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/__init__.py +0 -0
  82. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/base.py +0 -0
  83. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/bigquery.py +0 -0
  84. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/common.py +0 -0
  85. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/config.py +0 -0
  86. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/duckdb.py +0 -0
  87. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/enums.py +0 -0
  88. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/postgres.py +0 -0
  89. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/presto.py +0 -0
  90. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/snowflake.py +0 -0
  91. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/dialect/sql_server.py +0 -0
  92. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/engine.py +0 -0
  93. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/executor.py +0 -0
  94. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/__init__.py +0 -0
  95. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/base_hook.py +0 -0
  96. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/hooks/graph_hook.py +0 -0
  97. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/metadata/__init__.py +0 -0
  98. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parser.py +0 -0
  99. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/__init__.py +0 -0
  100. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/common.py +0 -0
  101. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/config.py +0 -0
  102. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/exceptions.py +0 -0
  103. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/parsing/helpers.py +0 -0
  104. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/py.typed +0 -0
  105. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/scripts/__init__.py +0 -0
  106. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/scripts/trilogy.py +0 -0
  107. {pytrilogy-0.0.2.26 → pytrilogy-0.0.2.27}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.26
3
+ Version: 0.0.2.27
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.26
3
+ Version: 0.0.2.27
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -491,6 +491,28 @@ select x;
491
491
  assert "abcdef as test" in results, results
492
492
 
493
493
 
494
+ def test_datasource_where_equivalent():
495
+
496
+ text = """
497
+ key x int;
498
+ key y int;
499
+
500
+ datasource test (
501
+ x:x,
502
+ y:~y)
503
+ grain(x)
504
+ complete where y > 10
505
+ address `abc:def`
506
+ ;
507
+
508
+
509
+ """
510
+ env, parsed = parse_text(text)
511
+
512
+ ds = parsed[-1]
513
+ assert ds.non_partial_for.conditional.right == 10
514
+
515
+
494
516
  def test_filter_concise():
495
517
 
496
518
  text = """
@@ -74,7 +74,7 @@ def setup_titanic(env: Environment):
74
74
 
75
75
  env.add_datasource(
76
76
  Datasource(
77
- identifier="raw_data",
77
+ name="raw_data",
78
78
  address="raw_titanic",
79
79
  columns=[
80
80
  ColumnAssignment(alias="passengerid", concept=id),
@@ -134,7 +134,7 @@ def test_query_aggregation(test_environment, test_environment_graph):
134
134
  environment=test_environment, graph=test_environment_graph, statement=select
135
135
  )
136
136
 
137
- assert {datasource.identifier} == {"local_revenue_at_local_order_id_at_abstract"}
137
+ assert {datasource.identifier} == {"revenue_at_local_order_id_at_abstract"}
138
138
  check = datasource
139
139
  assert len(check.input_concepts) == 2
140
140
  assert check.input_concepts[0].name == "revenue"
@@ -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.26"
7
+ __version__ = "0.0.2.27"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -6,7 +6,7 @@ from trilogy.core.models import Concept, Datasource
6
6
  def concept_to_node(input: Concept) -> str:
7
7
  # if input.purpose == Purpose.METRIC:
8
8
  # return f"c~{input.namespace}.{input.name}@{input.grain}"
9
- return f"c~{input.namespace}.{input.name}@{input.grain}"
9
+ return f"c~{input.address}@{input.grain}"
10
10
 
11
11
 
12
12
  def datasource_to_node(input: Datasource) -> str:
@@ -14,7 +14,7 @@ def datasource_to_node(input: Datasource) -> str:
14
14
  # return "ds~join~" + ",".join(
15
15
  # [datasource_to_node(sub) for sub in input.datasources]
16
16
  # )
17
- return f"ds~{input.namespace}.{input.identifier}"
17
+ return f"ds~{input.identifier}"
18
18
 
19
19
 
20
20
  class ReferenceGraph(nx.DiGraph):
@@ -1719,7 +1719,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1719
1719
  def to_datasource(
1720
1720
  self,
1721
1721
  namespace: str,
1722
- identifier: str,
1722
+ name: str,
1723
1723
  address: Address,
1724
1724
  grain: Grain | None = None,
1725
1725
  ) -> Datasource:
@@ -1753,7 +1753,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1753
1753
  condition = self.having_clause.conditional
1754
1754
 
1755
1755
  new_datasource = Datasource(
1756
- identifier=identifier,
1756
+ name=name,
1757
1757
  address=address,
1758
1758
  grain=grain or self.grain,
1759
1759
  columns=columns,
@@ -2059,7 +2059,7 @@ class MergeStatementV2(HasUUID, Namespaced, BaseModel):
2059
2059
 
2060
2060
 
2061
2061
  class Datasource(HasUUID, Namespaced, BaseModel):
2062
- identifier: str
2062
+ name: str
2063
2063
  columns: List[ColumnAssignment]
2064
2064
  address: Union[Address, str]
2065
2065
  grain: Grain = Field(
@@ -2094,10 +2094,14 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2094
2094
  self.add_column(target, original[0].alias, modifiers)
2095
2095
 
2096
2096
  @property
2097
- def env_label(self) -> str:
2097
+ def identifier(self) -> str:
2098
2098
  if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
2099
- return self.identifier
2100
- return f"{self.namespace}.{self.identifier}"
2099
+ return self.name
2100
+ return f"{self.namespace}.{self.name}"
2101
+
2102
+ @property
2103
+ def safe_identifier(self) -> str:
2104
+ return self.identifier.replace(".", "_")
2101
2105
 
2102
2106
  @property
2103
2107
  def condition(self):
@@ -2166,13 +2170,13 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2166
2170
  return self
2167
2171
 
2168
2172
  def __repr__(self):
2169
- return f"Datasource<{self.namespace}.{self.identifier}@<{self.grain}>"
2173
+ return f"Datasource<{self.identifier}@<{self.grain}>"
2170
2174
 
2171
2175
  def __str__(self):
2172
2176
  return self.__repr__()
2173
2177
 
2174
2178
  def __hash__(self):
2175
- return self.full_name.__hash__()
2179
+ return self.identifier.__hash__()
2176
2180
 
2177
2181
  def with_namespace(self, namespace: str):
2178
2182
  new_namespace = (
@@ -2181,7 +2185,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2181
2185
  else namespace
2182
2186
  )
2183
2187
  return Datasource(
2184
- identifier=self.identifier,
2188
+ name=self.name,
2185
2189
  namespace=new_namespace,
2186
2190
  grain=self.grain.with_namespace(namespace),
2187
2191
  address=self.address,
@@ -2231,19 +2235,6 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2231
2235
  f" {existing}."
2232
2236
  )
2233
2237
 
2234
- @property
2235
- def name(self) -> str:
2236
- return self.identifier
2237
- # TODO: namespace all references
2238
- # return f'{self.namespace}_{self.identifier}'
2239
-
2240
- @property
2241
- def full_name(self) -> str:
2242
- if not self.namespace:
2243
- return self.identifier
2244
- namespace = self.namespace.replace(".", "_") if self.namespace else ""
2245
- return f"{namespace}_{self.identifier}"
2246
-
2247
2238
  @property
2248
2239
  def safe_location(self) -> str:
2249
2240
  if isinstance(self.address, Address):
@@ -2298,7 +2289,7 @@ class BaseJoin(BaseModel):
2298
2289
  super().__init__(**data)
2299
2290
  if (
2300
2291
  self.left_datasource
2301
- and self.left_datasource.full_name == self.right_datasource.full_name
2292
+ and self.left_datasource.identifier == self.right_datasource.identifier
2302
2293
  ):
2303
2294
  raise SyntaxError(
2304
2295
  f"Cannot join a dataself to itself, joining {self.left_datasource} and"
@@ -2410,6 +2401,10 @@ class QueryDatasource(BaseModel):
2410
2401
  def __repr__(self):
2411
2402
  return f"{self.identifier}@<{self.grain}>"
2412
2403
 
2404
+ @property
2405
+ def safe_identifier(self):
2406
+ return self.identifier.replace(".", "_")
2407
+
2413
2408
  @property
2414
2409
  def non_partial_concept_addresses(self) -> List[str]:
2415
2410
  return [
@@ -2474,10 +2469,6 @@ class QueryDatasource(BaseModel):
2474
2469
  def name(self):
2475
2470
  return self.identifier
2476
2471
 
2477
- @property
2478
- def full_name(self):
2479
- return self.identifier
2480
-
2481
2472
  @property
2482
2473
  def group_required(self) -> bool:
2483
2474
  if self.force_group is True:
@@ -2524,10 +2515,12 @@ class QueryDatasource(BaseModel):
2524
2515
  merged_datasources = {}
2525
2516
 
2526
2517
  for ds in [*self.datasources, *other.datasources]:
2527
- if ds.full_name in merged_datasources:
2528
- merged_datasources[ds.full_name] = merged_datasources[ds.full_name] + ds
2518
+ if ds.safe_identifier in merged_datasources:
2519
+ merged_datasources[ds.safe_identifier] = (
2520
+ merged_datasources[ds.safe_identifier] + ds
2521
+ )
2529
2522
  else:
2530
- merged_datasources[ds.full_name] = ds
2523
+ merged_datasources[ds.safe_identifier] = ds
2531
2524
 
2532
2525
  final_source_map = defaultdict(set)
2533
2526
  for key in self.source_map:
@@ -2538,7 +2531,9 @@ class QueryDatasource(BaseModel):
2538
2531
  if key not in final_source_map:
2539
2532
  final_source_map[key] = other.source_map[key]
2540
2533
  for k, v in final_source_map.items():
2541
- final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2534
+ final_source_map[k] = set(
2535
+ merged_datasources[x.safe_identifier] for x in list(v)
2536
+ )
2542
2537
  self_hidden = self.hidden_concepts or []
2543
2538
  other_hidden = other.hidden_concepts or []
2544
2539
  hidden = [x for x in self_hidden if x.address in other_hidden]
@@ -2578,7 +2573,7 @@ class QueryDatasource(BaseModel):
2578
2573
  )
2579
2574
  # partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
2580
2575
  return (
2581
- "_join_".join([d.full_name for d in self.datasources])
2576
+ "_join_".join([d.identifier for d in self.datasources])
2582
2577
  + (f"_at_{grain}" if grain else "_at_abstract")
2583
2578
  + (f"_filtered_by_{filters}" if filters else "")
2584
2579
  # + (f"_partial_{partial}" if partial else "")
@@ -2594,8 +2589,9 @@ class QueryDatasource(BaseModel):
2594
2589
  for x in self.datasources:
2595
2590
  # query datasources should be referenced by their alias, always
2596
2591
  force_alias = isinstance(x, QueryDatasource)
2592
+ #
2597
2593
  use_raw_name = isinstance(x, Datasource) and not force_alias
2598
- if source and x.identifier != source:
2594
+ if source and x.safe_identifier != source:
2599
2595
  continue
2600
2596
  try:
2601
2597
  return x.get_alias(
@@ -2649,6 +2645,14 @@ class CTE(BaseModel):
2649
2645
  base_name_override: Optional[str] = None
2650
2646
  base_alias_override: Optional[str] = None
2651
2647
 
2648
+ @property
2649
+ def identifier(self):
2650
+ return self.name
2651
+
2652
+ @property
2653
+ def safe_identifier(self):
2654
+ return self.name
2655
+
2652
2656
  @computed_field # type: ignore
2653
2657
  @property
2654
2658
  def output_lcl(self) -> LooseConceptList:
@@ -2746,7 +2750,7 @@ class CTE(BaseModel):
2746
2750
  return False
2747
2751
  if any(
2748
2752
  [
2749
- x.identifier == ds_being_inlined.identifier
2753
+ x.safe_identifier == ds_being_inlined.safe_identifier
2750
2754
  for x in self.source.datasources
2751
2755
  ]
2752
2756
  ):
@@ -2757,39 +2761,49 @@ class CTE(BaseModel):
2757
2761
  *[
2758
2762
  x
2759
2763
  for x in self.source.datasources
2760
- if x.identifier != qds_being_inlined.identifier
2764
+ if x.safe_identifier != qds_being_inlined.safe_identifier
2761
2765
  ],
2762
2766
  ]
2763
2767
  # need to identify this before updating joins
2764
2768
  if self.base_name == parent.name:
2765
2769
  self.base_name_override = ds_being_inlined.safe_location
2766
- self.base_alias_override = ds_being_inlined.identifier
2770
+ self.base_alias_override = ds_being_inlined.safe_identifier
2767
2771
 
2768
2772
  for join in self.joins:
2769
2773
  if isinstance(join, InstantiatedUnnestJoin):
2770
2774
  continue
2771
- if join.left_cte and join.left_cte.name == parent.name:
2775
+ if (
2776
+ join.left_cte
2777
+ and join.left_cte.safe_identifier == parent.safe_identifier
2778
+ ):
2772
2779
  join.inline_cte(parent)
2773
2780
  if join.joinkey_pairs:
2774
2781
  for pair in join.joinkey_pairs:
2775
- if pair.cte and pair.cte.name == parent.name:
2782
+ if pair.cte and pair.cte.safe_identifier == parent.safe_identifier:
2776
2783
  join.inline_cte(parent)
2777
- if join.right_cte.name == parent.name:
2784
+ if join.right_cte.safe_identifier == parent.safe_identifier:
2778
2785
  join.inline_cte(parent)
2779
2786
  for k, v in self.source_map.items():
2780
2787
  if isinstance(v, list):
2781
2788
  self.source_map[k] = [
2782
- ds_being_inlined.name if x == parent.name else x for x in v
2789
+ (
2790
+ ds_being_inlined.safe_identifier
2791
+ if x == parent.safe_identifier
2792
+ else x
2793
+ )
2794
+ for x in v
2783
2795
  ]
2784
- elif v == parent.name:
2785
- self.source_map[k] = [ds_being_inlined.name]
2796
+ elif v == parent.safe_identifier:
2797
+ self.source_map[k] = [ds_being_inlined.safe_identifier]
2786
2798
 
2787
2799
  # zip in any required values for lookups
2788
2800
  for k in ds_being_inlined.output_lcl.addresses:
2789
2801
  if k in self.source_map and self.source_map[k]:
2790
2802
  continue
2791
- self.source_map[k] = [ds_being_inlined.name]
2792
- self.parent_ctes = [x for x in self.parent_ctes if x.name != parent.name]
2803
+ self.source_map[k] = [ds_being_inlined.safe_identifier]
2804
+ self.parent_ctes = [
2805
+ x for x in self.parent_ctes if x.safe_identifier != parent.safe_identifier
2806
+ ]
2793
2807
  if force_group:
2794
2808
  self.group_to_grain = True
2795
2809
  return True
@@ -3006,28 +3020,22 @@ class Join(BaseModel):
3006
3020
  def inline_cte(self, cte: CTE):
3007
3021
  self.inlined_ctes.add(cte.name)
3008
3022
 
3009
- # @property
3010
- # def left_name(self) -> str:
3011
- # if self.left_cte.name in self.inlined_ctes:
3012
- # return self.left_cte.source.datasources[0].identifier
3013
- # return self.left_cte.name
3014
-
3015
3023
  def get_name(self, cte: CTE):
3016
- if cte.name in self.inlined_ctes:
3017
- return cte.source.datasources[0].identifier
3018
- return cte.name
3024
+ if cte.identifier in self.inlined_ctes:
3025
+ return cte.source.datasources[0].safe_identifier
3026
+ return cte.safe_identifier
3019
3027
 
3020
3028
  @property
3021
3029
  def right_name(self) -> str:
3022
- if self.right_cte.name in self.inlined_ctes:
3023
- return self.right_cte.source.datasources[0].identifier
3024
- return self.right_cte.name
3030
+ if self.right_cte.identifier in self.inlined_ctes:
3031
+ return self.right_cte.source.datasources[0].safe_identifier
3032
+ return self.right_cte.safe_identifier
3025
3033
 
3026
3034
  @property
3027
3035
  def right_ref(self) -> str:
3028
- if self.right_cte.name in self.inlined_ctes:
3029
- return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].identifier}"
3030
- return self.right_cte.name
3036
+ if self.right_cte.identifier in self.inlined_ctes:
3037
+ return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].safe_identifier}"
3038
+ return self.right_cte.safe_identifier
3031
3039
 
3032
3040
  @property
3033
3041
  def unique_id(self) -> str:
@@ -3306,7 +3314,9 @@ class Environment(BaseModel):
3306
3314
  ] = Field(default_factory=EnvironmentDatasourceDict)
3307
3315
  functions: Dict[str, Function] = Field(default_factory=dict)
3308
3316
  data_types: Dict[str, DataType] = Field(default_factory=dict)
3309
- imports: Dict[str, ImportStatement] = Field(default_factory=dict)
3317
+ imports: Dict[str, list[ImportStatement]] = Field(
3318
+ default_factory=lambda: defaultdict(list)
3319
+ )
3310
3320
  namespace: str = DEFAULT_NAMESPACE
3311
3321
  working_path: str | Path = Field(default_factory=lambda: os.getcwd())
3312
3322
  environment_config: EnvironmentOptions = Field(default_factory=EnvironmentOptions)
@@ -3420,14 +3430,28 @@ class Environment(BaseModel):
3420
3430
  f"Assignment to concept '{lookup}' is a duplicate declaration;"
3421
3431
  )
3422
3432
 
3423
- def add_import(self, alias: str, environment: Environment):
3424
- self.imports[alias] = ImportStatement(
3425
- alias=alias, path=Path(environment.working_path)
3426
- )
3427
- for key, concept in environment.concepts.items():
3428
- self.concepts[f"{alias}.{key}"] = concept.with_namespace(alias)
3429
- for key, datasource in environment.datasources.items():
3430
- self.datasources[f"{alias}.{key}"] = datasource.with_namespace(alias)
3433
+ def add_import(
3434
+ self, alias: str, source: Environment, imp_stm: ImportStatement | None = None
3435
+ ):
3436
+ exists = False
3437
+ existing = self.imports[alias]
3438
+ if imp_stm:
3439
+ if any([x.path == imp_stm.path for x in existing]):
3440
+ exists = True
3441
+
3442
+ else:
3443
+ if any([x.path == source.working_path for x in existing]):
3444
+ exists = True
3445
+ imp_stm = ImportStatement(alias=alias, path=Path(source.working_path))
3446
+
3447
+ if not exists:
3448
+ self.imports[alias].append(imp_stm)
3449
+
3450
+ for _, concept in source.concepts.items():
3451
+ self.add_concept(concept.with_namespace(alias), _ignore_cache=True)
3452
+
3453
+ for _, datasource in source.datasources.items():
3454
+ self.add_datasource(datasource.with_namespace(alias), _ignore_cache=True)
3431
3455
  self.gen_concept_list_caches()
3432
3456
  return self
3433
3457
 
@@ -3438,18 +3462,15 @@ class Environment(BaseModel):
3438
3462
  apath[-1] = apath[-1] + ".preql"
3439
3463
 
3440
3464
  target: Path = Path(self.working_path, *apath)
3465
+ if alias in self.imports:
3466
+ imports = self.imports[alias]
3467
+ for x in imports:
3468
+ if x.path == target:
3469
+ return imports
3441
3470
  if env:
3442
- self.imports[alias] = ImportStatement(
3443
- alias=alias, path=target, environment=env
3471
+ self.imports[alias].append(
3472
+ ImportStatement(alias=alias, path=target, environment=env)
3444
3473
  )
3445
-
3446
- elif alias in self.imports:
3447
- current = self.imports[alias]
3448
- env = self.imports[alias].environment
3449
- if current.path != target:
3450
- raise ImportError(
3451
- f"Attempted to import {target} with alias {alias} but {alias} is already imported from {current.path}"
3452
- )
3453
3474
  else:
3454
3475
  try:
3455
3476
  with open(target, "r", encoding="utf-8") as f:
@@ -3468,14 +3489,13 @@ class Environment(BaseModel):
3468
3489
  f"Unable to import file {target.parent}, parsing error: {e}"
3469
3490
  )
3470
3491
  env = nparser.environment
3471
- if env:
3472
- for _, concept in env.concepts.items():
3473
- self.add_concept(concept.with_namespace(alias))
3492
+ for _, concept in env.concepts.items():
3493
+ self.add_concept(concept.with_namespace(alias))
3474
3494
 
3475
- for _, datasource in env.datasources.items():
3476
- self.add_datasource(datasource.with_namespace(alias))
3495
+ for _, datasource in env.datasources.items():
3496
+ self.add_datasource(datasource.with_namespace(alias))
3477
3497
  imps = ImportStatement(alias=alias, path=target, environment=env)
3478
- self.imports[alias] = imps
3498
+ self.imports[alias].append(imps)
3479
3499
  return imps
3480
3500
 
3481
3501
  def parse(
@@ -3538,8 +3558,14 @@ class Environment(BaseModel):
3538
3558
  meta: Meta | None = None,
3539
3559
  _ignore_cache: bool = False,
3540
3560
  ):
3541
- self.datasources[datasource.env_label] = datasource
3561
+ self.datasources[datasource.identifier] = datasource
3562
+
3563
+ eligible_to_promote_roots = datasource.non_partial_for is None
3564
+ # mark this as canonical source
3542
3565
  for current_concept in datasource.output_concepts:
3566
+ if not eligible_to_promote_roots:
3567
+ continue
3568
+
3543
3569
  current_derivation = current_concept.derivation
3544
3570
  # TODO: refine this section;
3545
3571
  # too hacky for maintainability
@@ -63,14 +63,14 @@ class InlineDatasource(OptimizationRule):
63
63
  for replaceable in to_inline:
64
64
  if replaceable.name not in self.candidates[cte.name]:
65
65
  self.candidates[cte.name].add(replaceable.name)
66
- self.count[replaceable.source.name] += 1
66
+ self.count[replaceable.source.identifier] += 1
67
67
  return True
68
68
  if (
69
- self.count[replaceable.source.name]
69
+ self.count[replaceable.source.identifier]
70
70
  > CONFIG.optimizations.constant_inline_cutoff
71
71
  ):
72
72
  self.log(
73
- f"Skipping inlining raw datasource {replaceable.source.name} ({replaceable.name}) due to multiple references"
73
+ f"Skipping inlining raw datasource {replaceable.source.identifier} ({replaceable.name}) due to multiple references"
74
74
  )
75
75
  continue
76
76
  if not replaceable.source.datasources[0].grain.issubset(replaceable.grain):
@@ -81,7 +81,7 @@ class InlineDatasource(OptimizationRule):
81
81
  result = cte.inline_parent_datasource(replaceable, force_group=force_group)
82
82
  if result:
83
83
  self.log(
84
- f"Inlined parent {replaceable.name} with {replaceable.source.name}"
84
+ f"Inlined parent {replaceable.name} with {replaceable.source.identifier}"
85
85
  )
86
86
  optimized = True
87
87
  else:
@@ -193,6 +193,7 @@ def create_select_node(
193
193
  g,
194
194
  environment: Environment,
195
195
  depth: int,
196
+ conditions: WhereClause | None = None,
196
197
  ) -> StrategyNode:
197
198
  ds_name = ds_name.split("~")[1]
198
199
  all_concepts = [
@@ -231,6 +232,7 @@ def create_select_node(
231
232
  c.concept for c in datasource.columns if c.is_nullable and c.concept in all_lcl
232
233
  ]
233
234
  nullable_lcl = LooseConceptList(concepts=nullable_concepts)
235
+ partial_is_full = conditions and (conditions == datasource.non_partial_for)
234
236
 
235
237
  bcandidate: StrategyNode = SelectNode(
236
238
  input_concepts=[c.concept for c in datasource.columns],
@@ -239,12 +241,15 @@ def create_select_node(
239
241
  g=g,
240
242
  parents=[],
241
243
  depth=depth,
242
- partial_concepts=[c for c in all_concepts if c in partial_lcl],
244
+ partial_concepts=(
245
+ [] if partial_is_full else [c for c in all_concepts if c in partial_lcl]
246
+ ),
243
247
  nullable_concepts=[c for c in all_concepts if c in nullable_lcl],
244
248
  accept_partial=accept_partial,
245
249
  datasource=datasource,
246
250
  grain=Grain(components=all_concepts),
247
251
  conditions=datasource.where.conditional if datasource.where else None,
252
+ render_condition=not partial_is_full,
248
253
  )
249
254
 
250
255
  # we need to nest the group node one further
@@ -312,6 +317,7 @@ def gen_select_merge_node(
312
317
  accept_partial=accept_partial,
313
318
  environment=environment,
314
319
  depth=depth,
320
+ conditions=conditions,
315
321
  )
316
322
  for k, subgraph in sub_nodes.items()
317
323
  ]
@@ -165,6 +165,7 @@ class StrategyNode:
165
165
  hidden_concepts: List[Concept] | None = None,
166
166
  existence_concepts: List[Concept] | None = None,
167
167
  virtual_output_concepts: List[Concept] | None = None,
168
+ render_condition: bool = True,
168
169
  ):
169
170
  self.input_concepts: List[Concept] = (
170
171
  unique(input_concepts, "address") if input_concepts else []
@@ -208,6 +209,7 @@ class StrategyNode:
208
209
  )
209
210
  self.validate_parents()
210
211
  self.log = True
212
+ self.render_condition = render_condition
211
213
 
212
214
  def add_parents(self, parents: list["StrategyNode"]):
213
215
  self.parents += parents
@@ -380,6 +382,7 @@ class StrategyNode:
380
382
  hidden_concepts=list(self.hidden_concepts),
381
383
  existence_concepts=list(self.existence_concepts),
382
384
  virtual_output_concepts=list(self.virtual_output_concepts),
385
+ render_condition=self.render_condition,
383
386
  )
384
387
 
385
388
 
@@ -89,8 +89,8 @@ def deduplicate_nodes_and_joins(
89
89
  joins = [
90
90
  j
91
91
  for j in joins
92
- if j.left_node.resolve().full_name not in removed
93
- and j.right_node.resolve().full_name not in removed
92
+ if j.left_node.resolve().identifier not in removed
93
+ and j.right_node.resolve().identifier not in removed
94
94
  ]
95
95
  return joins, merged
96
96
 
@@ -155,8 +155,8 @@ class MergeNode(StrategyNode):
155
155
  for join in node_joins:
156
156
  left = join.left_node.resolve()
157
157
  right = join.right_node.resolve()
158
- if left.full_name == right.full_name:
159
- raise SyntaxError(f"Cannot join node {left.full_name} to itself")
158
+ if left.identifier == right.identifier:
159
+ raise SyntaxError(f"Cannot join node {left.identifier} to itself")
160
160
  joins.append(
161
161
  BaseJoin(
162
162
  left_datasource=left,
@@ -168,7 +168,7 @@ class MergeNode(StrategyNode):
168
168
  )
169
169
  return joins
170
170
 
171
- def create_full_joins(self, dataset_list: List[QueryDatasource]):
171
+ def create_full_joins(self, dataset_list: List[QueryDatasource | Datasource]):
172
172
  joins = []
173
173
  seen = set()
174
174
  for left_value in dataset_list:
@@ -198,7 +198,7 @@ class MergeNode(StrategyNode):
198
198
  environment: Environment,
199
199
  ) -> List[BaseJoin | UnnestJoin]:
200
200
  # only finally, join between them for unique values
201
- dataset_list: List[QueryDatasource] = sorted(
201
+ dataset_list: List[QueryDatasource | Datasource] = sorted(
202
202
  final_datasets, key=lambda x: -len(x.grain.components_copy)
203
203
  )
204
204
 
@@ -238,13 +238,13 @@ class MergeNode(StrategyNode):
238
238
  merged: dict[str, QueryDatasource | Datasource] = {}
239
239
  final_joins: List[NodeJoin] | None = self.node_joins
240
240
  for source in parent_sources:
241
- if source.full_name in merged:
241
+ if source.identifier in merged:
242
242
  logger.info(
243
- f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.full_name} into existing"
243
+ f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.identifier} into existing"
244
244
  )
245
- merged[source.full_name] = merged[source.full_name] + source
245
+ merged[source.identifier] = merged[source.identifier] + source
246
246
  else:
247
- merged[source.full_name] = source
247
+ merged[source.identifier] = source
248
248
 
249
249
  # it's possible that we have more sources than we need
250
250
  final_joins, merged = deduplicate_nodes_and_joins(