pytrilogy 0.0.2.26__py3-none-any.whl → 0.0.2.28__py3-none-any.whl

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.

@@ -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.28
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,8 +1,8 @@
1
- trilogy/__init__.py,sha256=Cc2rIa67kJZaRPTgItA7vo9mKdPdLIskVp1BDwgHWbc,291
1
+ trilogy/__init__.py,sha256=Oj4rApJpgEd7VNBVDA5Sy1tHnI0ISyXQcfpiH-fv6UY,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=KiyYnctoZen4Hzv8WG2jeN-IE-dfQbWHdVCUeTZYjBg,1270
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
- trilogy/executor.py,sha256=Gd9KRT1rNAQyF1oDtKMcidg6XWqGMBhPnErrzFpf7Ew,12139
5
+ trilogy/executor.py,sha256=VcZ2U3RUU2al_VJ75AKVwmCJQLltYouxlgTjq4oxPB0,12577
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
@@ -14,20 +14,20 @@ trilogy/core/environment_helpers.py,sha256=DIsoo-GcXmXVPB1JbNh8Oku25Nyef9mexPIdy
14
14
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
15
15
  trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
16
  trilogy/core/functions.py,sha256=IhVpt3n6wEanKHnGu3oA2w6-hKIlxWpEyz7fHN66mpo,10720
17
- trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
17
+ trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=ZPMOWmN4vDvXyLZvyiaN-WZnMDukDwr2nJYFIe6vJKo,158251
19
+ trilogy/core/models.py,sha256=W_0ZfIIEuyHfYsSXGMJOJPNJf7vSljSrRm42nLyiL8w,159702
20
20
  trilogy/core/optimization.py,sha256=od_60A9F8J8Nj24MHgrxl4vwRwmBFH13TMdoMQvgVKs,7717
21
- trilogy/core/query_processor.py,sha256=-fKPlygk3aX1cY60dl4tKNQofKRFl3zhqz5klRIbtq0,17683
21
+ trilogy/core/query_processor.py,sha256=mbcZlgjChrRjDHkdmMbKe-T70UpbBkJhS09MyU5a6UY,17785
22
22
  trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
23
23
  trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
24
24
  trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
25
- trilogy/core/optimizations/inline_datasource.py,sha256=AATzQ6YrtW_1-aQFjQyTYqEYKBoMFhek7ADfBr4uUdQ,3634
25
+ trilogy/core/optimizations/inline_datasource.py,sha256=NqUOVl0pOXF1R_roELVW8I0qN7or2wPtAsRmDD9QJso,3658
26
26
  trilogy/core/optimizations/predicate_pushdown.py,sha256=1l9WnFOSv79e341typG3tTdk0XGl1J_ToQih3LYoGIY,8435
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  trilogy/core/processing/concept_strategies_v3.py,sha256=7MT_x6QFHrbSDmjz21pYdQB5ux419ES4QS-8lO16eyw,36091
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=xNl310JhBda0Vv1SSwg5EtMPmMDAWucxfWd7M53Sb9k,17422
30
+ trilogy/core/processing/utility.py,sha256=xlaKqnoWg-mEwTF-erBF9QXnXZtESrTuYrK2RQb7Wi4,17411
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
32
  trilogy/core/processing/node_generators/basic_node.py,sha256=WQNgJ1MwrMS_BQ-b3XwGGB6eToDykelAVj_fesJuqe0,2069
33
33
  trilogy/core/processing/node_generators/common.py,sha256=eslHTTPFTkmwHwKIuUsbFn54jxj-Avtt-QScqtNwzdg,8945
@@ -37,16 +37,16 @@ trilogy/core/processing/node_generators/group_to_node.py,sha256=R9i_wHipxjXJyfYE
37
37
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=_KO9lqzHQoy4VAviO0ttQlmK0tjaqrJj4SJPhmoIYm8,6229
38
38
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=dIEv5P2MTViAES2MzqJgccYzM3HldjHrQYFwH00cqyc,14003
39
39
  trilogy/core/processing/node_generators/rowset_node.py,sha256=KtdN6t2xM8CJxobc4aQX4W8uX98U6IabeuBF_FtBLR4,4583
40
- trilogy/core/processing/node_generators/select_merge_node.py,sha256=z1sF04MQsMYbLjE__co5Nwi5hTvMeTZACzTnuBe7lsk,11341
40
+ trilogy/core/processing/node_generators/select_merge_node.py,sha256=vE7GoPu2_okO3jS96oA5O3jFsrkiSqIvIP5WkyfFil0,11596
41
41
  trilogy/core/processing/node_generators/select_node.py,sha256=nwXHQF6C-aQUIelx9dyxN2pK3muL-4-6RIqnqQqNwtw,1808
42
42
  trilogy/core/processing/node_generators/unnest_node.py,sha256=cZ26CN338CBnd6asML1OBUtNcDzmNlFpY0Vnade4yrc,2256
43
43
  trilogy/core/processing/node_generators/window_node.py,sha256=jy3FF8uN0VA7yyrBeR40B9CAqR_5qBP4PiS6Gr-f-7w,2590
44
44
  trilogy/core/processing/nodes/__init__.py,sha256=qS5EJDRwwIrCEfS7ibCA2ESE0RPzsAIii1UWd_wNsHA,4760
45
- trilogy/core/processing/nodes/base_node.py,sha256=sc3HrXkWk-xpsAQ7B7ltX1ZejYAkqFiv8Ei8Jg5VGkQ,15579
45
+ trilogy/core/processing/nodes/base_node.py,sha256=8nEG3OPE_LzFXI48-Y6FS8MyO79LY0Sm0EqYz31WJ1Q,15719
46
46
  trilogy/core/processing/nodes/filter_node.py,sha256=GfZ9eghpFDI-s7iQP2UqTljCmn25LT_T5TAxDlh7PkQ,2343
47
47
  trilogy/core/processing/nodes/group_node.py,sha256=PrBHaGq_f8RmokUw9lXLGJ5YbjdP77P7Ag0pgR6e2cU,7293
48
- trilogy/core/processing/nodes/merge_node.py,sha256=2BjE2bTyoMHLfn_pnl1fioJkm1AfWtVKnuzzL4aWS5I,14799
49
- trilogy/core/processing/nodes/select_node_v2.py,sha256=gS9OQgS2TSEK59BQ9R0i83pTHfGJUxv7AkAmT21sYxI,8067
48
+ trilogy/core/processing/nodes/merge_node.py,sha256=W3eCjmJbs8Wfw7Y5AgIY2pP-ntPCrrMe11UG-QGJvA8,14835
49
+ trilogy/core/processing/nodes/select_node_v2.py,sha256=k5WvqmOkLwnP9SFSF5z33a1SFo4nZ-y9ODLi-P05YkI,8281
50
50
  trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
51
51
  trilogy/core/processing/nodes/window_node.py,sha256=ro0QfMFi4ZmIn5Q4D0M_vJWfnHH_C0MN7XkVkx8Gygg,1214
52
52
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -63,21 +63,21 @@ trilogy/dialect/sql_server.py,sha256=owUZbMFrooYIMj1DSLstPWxPO7K7WAUEWNvDKM-DMt0
63
63
  trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
65
65
  trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2467
66
- trilogy/hooks/query_debugger.py,sha256=Pe-Kw1JGngeLqQOMQb0E3-24jXEavqnPCQ-KOfTfjP8,4357
66
+ trilogy/hooks/query_debugger.py,sha256=787umJjdGA057DCC714dqFstzJRUbwmz3MNr66IdpQI,4404
67
67
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
68
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
69
  trilogy/parsing/common.py,sha256=t7yiL_3f6rz_rouF9et84v5orAgs-EprV4V9ghQ6ql4,10024
70
70
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
71
71
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
72
72
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
- trilogy/parsing/parse_engine.py,sha256=nyNyClknlHVYeHHGrSXWDAuV_E_XQSvFLUZqZ2q97kE,64513
74
- trilogy/parsing/render.py,sha256=_Jb1or0XFmrj2mHHv7My1VNdWkcpOAYWnRwFW2sh4U0,14052
75
- trilogy/parsing/trilogy.lark,sha256=NZgFchImZsQ3fyyBh8kwq8esTQOR5QlZ9n6k-F5H8nI,12184
73
+ trilogy/parsing/parse_engine.py,sha256=JwG98fotPpvh5VC-CcHknCTFid9-Zj1Wfo8CyPOnJzs,64431
74
+ trilogy/parsing/render.py,sha256=B9J2GrYQcE76kddMQSeAmvAPX-9pv39mpeSHZ10SNj8,14655
75
+ trilogy/parsing/trilogy.lark,sha256=_z5px2N-e8oLUf7SpPMXXNqbAykDkZOvP4_lPgf5-Uk,12245
76
76
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
78
- pytrilogy-0.0.2.26.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
- pytrilogy-0.0.2.26.dist-info/METADATA,sha256=1tD8kmlqzPcPdU5SiPHkiiSvXXwSqEm4sBvjp1LIDY4,8403
80
- pytrilogy-0.0.2.26.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
81
- pytrilogy-0.0.2.26.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
- pytrilogy-0.0.2.26.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
- pytrilogy-0.0.2.26.dist-info/RECORD,,
78
+ pytrilogy-0.0.2.28.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.28.dist-info/METADATA,sha256=Ftsu-RyQ2c7b05KV4JZm7J9f1DEMup2xjMfLUA-PfWQ,8403
80
+ pytrilogy-0.0.2.28.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
81
+ pytrilogy-0.0.2.28.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.28.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.28.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -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.28"
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):
trilogy/core/models.py CHANGED
@@ -606,6 +606,8 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
606
606
  return self.grain.components_copy if self.grain else []
607
607
 
608
608
  def with_namespace(self, namespace: str) -> "Concept":
609
+ if namespace == self.namespace:
610
+ return self
609
611
  return self.__class__(
610
612
  name=self.name,
611
613
  datatype=self.datatype,
@@ -1719,7 +1721,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1719
1721
  def to_datasource(
1720
1722
  self,
1721
1723
  namespace: str,
1722
- identifier: str,
1724
+ name: str,
1723
1725
  address: Address,
1724
1726
  grain: Grain | None = None,
1725
1727
  ) -> Datasource:
@@ -1753,7 +1755,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1753
1755
  condition = self.having_clause.conditional
1754
1756
 
1755
1757
  new_datasource = Datasource(
1756
- identifier=identifier,
1758
+ name=name,
1757
1759
  address=address,
1758
1760
  grain=grain or self.grain,
1759
1761
  columns=columns,
@@ -2059,7 +2061,7 @@ class MergeStatementV2(HasUUID, Namespaced, BaseModel):
2059
2061
 
2060
2062
 
2061
2063
  class Datasource(HasUUID, Namespaced, BaseModel):
2062
- identifier: str
2064
+ name: str
2063
2065
  columns: List[ColumnAssignment]
2064
2066
  address: Union[Address, str]
2065
2067
  grain: Grain = Field(
@@ -2094,10 +2096,14 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2094
2096
  self.add_column(target, original[0].alias, modifiers)
2095
2097
 
2096
2098
  @property
2097
- def env_label(self) -> str:
2099
+ def identifier(self) -> str:
2098
2100
  if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
2099
- return self.identifier
2100
- return f"{self.namespace}.{self.identifier}"
2101
+ return self.name
2102
+ return f"{self.namespace}.{self.name}"
2103
+
2104
+ @property
2105
+ def safe_identifier(self) -> str:
2106
+ return self.identifier.replace(".", "_")
2101
2107
 
2102
2108
  @property
2103
2109
  def condition(self):
@@ -2166,13 +2172,13 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2166
2172
  return self
2167
2173
 
2168
2174
  def __repr__(self):
2169
- return f"Datasource<{self.namespace}.{self.identifier}@<{self.grain}>"
2175
+ return f"Datasource<{self.identifier}@<{self.grain}>"
2170
2176
 
2171
2177
  def __str__(self):
2172
2178
  return self.__repr__()
2173
2179
 
2174
2180
  def __hash__(self):
2175
- return self.full_name.__hash__()
2181
+ return self.identifier.__hash__()
2176
2182
 
2177
2183
  def with_namespace(self, namespace: str):
2178
2184
  new_namespace = (
@@ -2181,7 +2187,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2181
2187
  else namespace
2182
2188
  )
2183
2189
  return Datasource(
2184
- identifier=self.identifier,
2190
+ name=self.name,
2185
2191
  namespace=new_namespace,
2186
2192
  grain=self.grain.with_namespace(namespace),
2187
2193
  address=self.address,
@@ -2231,19 +2237,6 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2231
2237
  f" {existing}."
2232
2238
  )
2233
2239
 
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
2240
  @property
2248
2241
  def safe_location(self) -> str:
2249
2242
  if isinstance(self.address, Address):
@@ -2298,7 +2291,7 @@ class BaseJoin(BaseModel):
2298
2291
  super().__init__(**data)
2299
2292
  if (
2300
2293
  self.left_datasource
2301
- and self.left_datasource.full_name == self.right_datasource.full_name
2294
+ and self.left_datasource.identifier == self.right_datasource.identifier
2302
2295
  ):
2303
2296
  raise SyntaxError(
2304
2297
  f"Cannot join a dataself to itself, joining {self.left_datasource} and"
@@ -2410,6 +2403,10 @@ class QueryDatasource(BaseModel):
2410
2403
  def __repr__(self):
2411
2404
  return f"{self.identifier}@<{self.grain}>"
2412
2405
 
2406
+ @property
2407
+ def safe_identifier(self):
2408
+ return self.identifier.replace(".", "_")
2409
+
2413
2410
  @property
2414
2411
  def non_partial_concept_addresses(self) -> List[str]:
2415
2412
  return [
@@ -2474,10 +2471,6 @@ class QueryDatasource(BaseModel):
2474
2471
  def name(self):
2475
2472
  return self.identifier
2476
2473
 
2477
- @property
2478
- def full_name(self):
2479
- return self.identifier
2480
-
2481
2474
  @property
2482
2475
  def group_required(self) -> bool:
2483
2476
  if self.force_group is True:
@@ -2524,10 +2517,12 @@ class QueryDatasource(BaseModel):
2524
2517
  merged_datasources = {}
2525
2518
 
2526
2519
  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
2520
+ if ds.safe_identifier in merged_datasources:
2521
+ merged_datasources[ds.safe_identifier] = (
2522
+ merged_datasources[ds.safe_identifier] + ds
2523
+ )
2529
2524
  else:
2530
- merged_datasources[ds.full_name] = ds
2525
+ merged_datasources[ds.safe_identifier] = ds
2531
2526
 
2532
2527
  final_source_map = defaultdict(set)
2533
2528
  for key in self.source_map:
@@ -2538,7 +2533,9 @@ class QueryDatasource(BaseModel):
2538
2533
  if key not in final_source_map:
2539
2534
  final_source_map[key] = other.source_map[key]
2540
2535
  for k, v in final_source_map.items():
2541
- final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2536
+ final_source_map[k] = set(
2537
+ merged_datasources[x.safe_identifier] for x in list(v)
2538
+ )
2542
2539
  self_hidden = self.hidden_concepts or []
2543
2540
  other_hidden = other.hidden_concepts or []
2544
2541
  hidden = [x for x in self_hidden if x.address in other_hidden]
@@ -2578,7 +2575,7 @@ class QueryDatasource(BaseModel):
2578
2575
  )
2579
2576
  # partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
2580
2577
  return (
2581
- "_join_".join([d.full_name for d in self.datasources])
2578
+ "_join_".join([d.identifier for d in self.datasources])
2582
2579
  + (f"_at_{grain}" if grain else "_at_abstract")
2583
2580
  + (f"_filtered_by_{filters}" if filters else "")
2584
2581
  # + (f"_partial_{partial}" if partial else "")
@@ -2594,8 +2591,9 @@ class QueryDatasource(BaseModel):
2594
2591
  for x in self.datasources:
2595
2592
  # query datasources should be referenced by their alias, always
2596
2593
  force_alias = isinstance(x, QueryDatasource)
2594
+ #
2597
2595
  use_raw_name = isinstance(x, Datasource) and not force_alias
2598
- if source and x.identifier != source:
2596
+ if source and x.safe_identifier != source:
2599
2597
  continue
2600
2598
  try:
2601
2599
  return x.get_alias(
@@ -2649,6 +2647,14 @@ class CTE(BaseModel):
2649
2647
  base_name_override: Optional[str] = None
2650
2648
  base_alias_override: Optional[str] = None
2651
2649
 
2650
+ @property
2651
+ def identifier(self):
2652
+ return self.name
2653
+
2654
+ @property
2655
+ def safe_identifier(self):
2656
+ return self.name
2657
+
2652
2658
  @computed_field # type: ignore
2653
2659
  @property
2654
2660
  def output_lcl(self) -> LooseConceptList:
@@ -2746,7 +2752,7 @@ class CTE(BaseModel):
2746
2752
  return False
2747
2753
  if any(
2748
2754
  [
2749
- x.identifier == ds_being_inlined.identifier
2755
+ x.safe_identifier == ds_being_inlined.safe_identifier
2750
2756
  for x in self.source.datasources
2751
2757
  ]
2752
2758
  ):
@@ -2757,39 +2763,49 @@ class CTE(BaseModel):
2757
2763
  *[
2758
2764
  x
2759
2765
  for x in self.source.datasources
2760
- if x.identifier != qds_being_inlined.identifier
2766
+ if x.safe_identifier != qds_being_inlined.safe_identifier
2761
2767
  ],
2762
2768
  ]
2763
2769
  # need to identify this before updating joins
2764
2770
  if self.base_name == parent.name:
2765
2771
  self.base_name_override = ds_being_inlined.safe_location
2766
- self.base_alias_override = ds_being_inlined.identifier
2772
+ self.base_alias_override = ds_being_inlined.safe_identifier
2767
2773
 
2768
2774
  for join in self.joins:
2769
2775
  if isinstance(join, InstantiatedUnnestJoin):
2770
2776
  continue
2771
- if join.left_cte and join.left_cte.name == parent.name:
2777
+ if (
2778
+ join.left_cte
2779
+ and join.left_cte.safe_identifier == parent.safe_identifier
2780
+ ):
2772
2781
  join.inline_cte(parent)
2773
2782
  if join.joinkey_pairs:
2774
2783
  for pair in join.joinkey_pairs:
2775
- if pair.cte and pair.cte.name == parent.name:
2784
+ if pair.cte and pair.cte.safe_identifier == parent.safe_identifier:
2776
2785
  join.inline_cte(parent)
2777
- if join.right_cte.name == parent.name:
2786
+ if join.right_cte.safe_identifier == parent.safe_identifier:
2778
2787
  join.inline_cte(parent)
2779
2788
  for k, v in self.source_map.items():
2780
2789
  if isinstance(v, list):
2781
2790
  self.source_map[k] = [
2782
- ds_being_inlined.name if x == parent.name else x for x in v
2791
+ (
2792
+ ds_being_inlined.safe_identifier
2793
+ if x == parent.safe_identifier
2794
+ else x
2795
+ )
2796
+ for x in v
2783
2797
  ]
2784
- elif v == parent.name:
2785
- self.source_map[k] = [ds_being_inlined.name]
2798
+ elif v == parent.safe_identifier:
2799
+ self.source_map[k] = [ds_being_inlined.safe_identifier]
2786
2800
 
2787
2801
  # zip in any required values for lookups
2788
2802
  for k in ds_being_inlined.output_lcl.addresses:
2789
2803
  if k in self.source_map and self.source_map[k]:
2790
2804
  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]
2805
+ self.source_map[k] = [ds_being_inlined.safe_identifier]
2806
+ self.parent_ctes = [
2807
+ x for x in self.parent_ctes if x.safe_identifier != parent.safe_identifier
2808
+ ]
2793
2809
  if force_group:
2794
2810
  self.group_to_grain = True
2795
2811
  return True
@@ -3006,28 +3022,22 @@ class Join(BaseModel):
3006
3022
  def inline_cte(self, cte: CTE):
3007
3023
  self.inlined_ctes.add(cte.name)
3008
3024
 
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
3025
  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
3026
+ if cte.identifier in self.inlined_ctes:
3027
+ return cte.source.datasources[0].safe_identifier
3028
+ return cte.safe_identifier
3019
3029
 
3020
3030
  @property
3021
3031
  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
3032
+ if self.right_cte.identifier in self.inlined_ctes:
3033
+ return self.right_cte.source.datasources[0].safe_identifier
3034
+ return self.right_cte.safe_identifier
3025
3035
 
3026
3036
  @property
3027
3037
  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
3038
+ if self.right_cte.identifier in self.inlined_ctes:
3039
+ return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].safe_identifier}"
3040
+ return self.right_cte.safe_identifier
3031
3041
 
3032
3042
  @property
3033
3043
  def unique_id(self) -> str:
@@ -3245,7 +3255,6 @@ class EnvironmentConceptDict(dict):
3245
3255
  )
3246
3256
  self.undefined[key] = undefined
3247
3257
  return undefined
3248
-
3249
3258
  matches = self._find_similar_concepts(key)
3250
3259
  message = f"Undefined concept: {key}."
3251
3260
  if matches:
@@ -3255,8 +3264,15 @@ class EnvironmentConceptDict(dict):
3255
3264
  raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
3256
3265
  raise UndefinedConceptException(message, matches)
3257
3266
 
3258
- def _find_similar_concepts(self, concept_name):
3259
- matches = difflib.get_close_matches(concept_name, self.keys())
3267
+ def _find_similar_concepts(self, concept_name: str):
3268
+ def strip_local(input: str):
3269
+ if input.startswith(f"{DEFAULT_NAMESPACE}."):
3270
+ return input[len(DEFAULT_NAMESPACE) + 1 :]
3271
+ return input
3272
+
3273
+ matches = difflib.get_close_matches(
3274
+ strip_local(concept_name), [strip_local(x) for x in self.keys()]
3275
+ )
3260
3276
  return matches
3261
3277
 
3262
3278
  def items(self) -> ItemsView[str, Concept]: # type: ignore
@@ -3306,7 +3322,9 @@ class Environment(BaseModel):
3306
3322
  ] = Field(default_factory=EnvironmentDatasourceDict)
3307
3323
  functions: Dict[str, Function] = Field(default_factory=dict)
3308
3324
  data_types: Dict[str, DataType] = Field(default_factory=dict)
3309
- imports: Dict[str, ImportStatement] = Field(default_factory=dict)
3325
+ imports: Dict[str, list[ImportStatement]] = Field(
3326
+ default_factory=lambda: defaultdict(list)
3327
+ )
3310
3328
  namespace: str = DEFAULT_NAMESPACE
3311
3329
  working_path: str | Path = Field(default_factory=lambda: os.getcwd())
3312
3330
  environment_config: EnvironmentOptions = Field(default_factory=EnvironmentOptions)
@@ -3315,7 +3333,6 @@ class Environment(BaseModel):
3315
3333
 
3316
3334
  materialized_concepts: List[Concept] = Field(default_factory=list)
3317
3335
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3318
- canonical_map: Dict[str, str] = Field(default_factory=dict)
3319
3336
  _parse_count: int = 0
3320
3337
 
3321
3338
  @classmethod
@@ -3420,14 +3437,52 @@ class Environment(BaseModel):
3420
3437
  f"Assignment to concept '{lookup}' is a duplicate declaration;"
3421
3438
  )
3422
3439
 
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)
3440
+ def add_import(
3441
+ self, alias: str, source: Environment, imp_stm: ImportStatement | None = None
3442
+ ):
3443
+ exists = False
3444
+ existing = self.imports[alias]
3445
+ if imp_stm:
3446
+ if any([x.path == imp_stm.path for x in existing]):
3447
+ exists = True
3448
+
3449
+ else:
3450
+ if any([x.path == source.working_path for x in existing]):
3451
+ exists = True
3452
+ imp_stm = ImportStatement(alias=alias, path=Path(source.working_path))
3453
+
3454
+ same_namespace = alias == self.namespace
3455
+
3456
+ if not exists:
3457
+ self.imports[alias].append(imp_stm)
3458
+
3459
+ for k, concept in source.concepts.items():
3460
+ if same_namespace:
3461
+ new = self.add_concept(concept, _ignore_cache=True)
3462
+ else:
3463
+ new = self.add_concept(
3464
+ concept.with_namespace(alias), _ignore_cache=True
3465
+ )
3466
+
3467
+ k = address_with_namespace(k, alias)
3468
+ # set this explicitly, to handle aliasing
3469
+ self.concepts[k] = new
3470
+
3471
+ for _, datasource in source.datasources.items():
3472
+ if same_namespace:
3473
+ self.add_datasource(datasource, _ignore_cache=True)
3474
+ else:
3475
+ self.add_datasource(
3476
+ datasource.with_namespace(alias), _ignore_cache=True
3477
+ )
3478
+ for key, val in source.alias_origin_lookup.items():
3479
+ if same_namespace:
3480
+ self.alias_origin_lookup[key] = val
3481
+ else:
3482
+ self.alias_origin_lookup[address_with_namespace(key, alias)] = (
3483
+ val.with_namespace(alias)
3484
+ )
3485
+
3431
3486
  self.gen_concept_list_caches()
3432
3487
  return self
3433
3488
 
@@ -3438,18 +3493,15 @@ class Environment(BaseModel):
3438
3493
  apath[-1] = apath[-1] + ".preql"
3439
3494
 
3440
3495
  target: Path = Path(self.working_path, *apath)
3496
+ if alias in self.imports:
3497
+ imports = self.imports[alias]
3498
+ for x in imports:
3499
+ if x.path == target:
3500
+ return imports
3441
3501
  if env:
3442
- self.imports[alias] = ImportStatement(
3443
- alias=alias, path=target, environment=env
3502
+ self.imports[alias].append(
3503
+ ImportStatement(alias=alias, path=target, environment=env)
3444
3504
  )
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
3505
  else:
3454
3506
  try:
3455
3507
  with open(target, "r", encoding="utf-8") as f:
@@ -3468,14 +3520,13 @@ class Environment(BaseModel):
3468
3520
  f"Unable to import file {target.parent}, parsing error: {e}"
3469
3521
  )
3470
3522
  env = nparser.environment
3471
- if env:
3472
- for _, concept in env.concepts.items():
3473
- self.add_concept(concept.with_namespace(alias))
3523
+ for _, concept in env.concepts.items():
3524
+ self.add_concept(concept.with_namespace(alias))
3474
3525
 
3475
- for _, datasource in env.datasources.items():
3476
- self.add_datasource(datasource.with_namespace(alias))
3526
+ for _, datasource in env.datasources.items():
3527
+ self.add_datasource(datasource.with_namespace(alias))
3477
3528
  imps = ImportStatement(alias=alias, path=target, environment=env)
3478
- self.imports[alias] = imps
3529
+ self.imports[alias].append(imps)
3479
3530
  return imps
3480
3531
 
3481
3532
  def parse(
@@ -3522,8 +3573,6 @@ class Environment(BaseModel):
3522
3573
  existing = self.validate_concept(concept, meta=meta)
3523
3574
  if existing:
3524
3575
  concept = existing
3525
- if concept.namespace == DEFAULT_NAMESPACE:
3526
- self.concepts[concept.name] = concept
3527
3576
  self.concepts[concept.address] = concept
3528
3577
  from trilogy.core.environment_helpers import generate_related_concepts
3529
3578
 
@@ -3538,8 +3587,14 @@ class Environment(BaseModel):
3538
3587
  meta: Meta | None = None,
3539
3588
  _ignore_cache: bool = False,
3540
3589
  ):
3541
- self.datasources[datasource.env_label] = datasource
3590
+ self.datasources[datasource.identifier] = datasource
3591
+
3592
+ eligible_to_promote_roots = datasource.non_partial_for is None
3593
+ # mark this as canonical source
3542
3594
  for current_concept in datasource.output_concepts:
3595
+ if not eligible_to_promote_roots:
3596
+ continue
3597
+
3543
3598
  current_derivation = current_concept.derivation
3544
3599
  # TODO: refine this section;
3545
3600
  # too hacky for maintainability
@@ -3605,7 +3660,6 @@ class Environment(BaseModel):
3605
3660
  v.pseudonyms.add(source.address)
3606
3661
  if v.address == source.address:
3607
3662
  replacements[k] = target
3608
- self.canonical_map[k] = target.address
3609
3663
  v.pseudonyms.add(target.address)
3610
3664
  # we need to update keys and grains of all concepts
3611
3665
  else:
@@ -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(
@@ -49,6 +49,7 @@ class SelectNode(StrategyNode):
49
49
  conditions: Conditional | Comparison | Parenthetical | None = None,
50
50
  preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
51
51
  hidden_concepts: List[Concept] | None = None,
52
+ render_condition: bool = True,
52
53
  ):
53
54
  super().__init__(
54
55
  input_concepts=input_concepts,
@@ -65,6 +66,7 @@ class SelectNode(StrategyNode):
65
66
  conditions=conditions,
66
67
  preexisting_conditions=preexisting_conditions,
67
68
  hidden_concepts=hidden_concepts,
69
+ render_condition=render_condition,
68
70
  )
69
71
  self.accept_partial = accept_partial
70
72
  self.datasource = datasource
@@ -120,7 +122,8 @@ class SelectNode(StrategyNode):
120
122
  ],
121
123
  nullable_concepts=[c.concept for c in datasource.columns if c.is_nullable],
122
124
  source_type=SourceType.DIRECT_SELECT,
123
- condition=self.conditions,
125
+ # we can skip rendering conditions
126
+ condition=self.conditions if self.render_condition else None,
124
127
  # select nodes should never group
125
128
  force_group=self.force_group,
126
129
  hidden_concepts=self.hidden_concepts,
@@ -128,7 +131,7 @@ class SelectNode(StrategyNode):
128
131
 
129
132
  def resolve_from_constant_datasources(self) -> QueryDatasource:
130
133
  datasource = Datasource(
131
- identifier=CONSTANT_DATASET, address=CONSTANT_DATASET, columns=[]
134
+ name=CONSTANT_DATASET, address=CONSTANT_DATASET, columns=[]
132
135
  )
133
136
  return QueryDatasource(
134
137
  input_concepts=[],
@@ -205,6 +208,7 @@ class SelectNode(StrategyNode):
205
208
  conditions=self.conditions,
206
209
  preexisting_conditions=self.preexisting_conditions,
207
210
  hidden_concepts=self.hidden_concepts,
211
+ render_condition=self.render_condition,
208
212
  )
209
213
 
210
214
 
@@ -66,9 +66,6 @@ def resolve_join_order_v2(
66
66
  ) -> list[JoinOrderOutput]:
67
67
  datasources = [x for x in g.nodes if x.startswith("ds~")]
68
68
  concepts = [x for x in g.nodes if x.startswith("c~")]
69
- # from trilogy.hooks.graph_hook import GraphHook
70
-
71
- # GraphHook().query_graph_built(g)
72
69
 
73
70
  output: list[JoinOrderOutput] = []
74
71
  pivot_map = {
@@ -78,7 +75,7 @@ def resolve_join_order_v2(
78
75
  pivots = list(
79
76
  sorted(
80
77
  [x for x in pivot_map if len(pivot_map[x]) > 1],
81
- key=lambda x: len(pivot_map[x]),
78
+ key=lambda x: (len(pivot_map[x]), len(x), x),
82
79
  )
83
80
  )
84
81
  solo = [x for x in pivot_map if len(pivot_map[x]) == 1]
@@ -95,7 +92,7 @@ def resolve_join_order_v2(
95
92
  root = pivots.pop()
96
93
 
97
94
  # sort so less partials is last and eligible lefts are
98
- def score_key(x: str) -> int:
95
+ def score_key(x: str) -> tuple[int, int, str]:
99
96
  base = 1
100
97
  # if it's left, higher weight
101
98
  if x in eligible_left:
@@ -103,7 +100,7 @@ def resolve_join_order_v2(
103
100
  # if it has the concept as a partial, lower weight
104
101
  if root in partials.get(x, []):
105
102
  base -= 1
106
- return base
103
+ return (base, len(x), x)
107
104
 
108
105
  # get remainig un-joined datasets
109
106
  to_join = sorted(
@@ -296,7 +293,7 @@ def add_node_join_concept(
296
293
 
297
294
 
298
295
  def resolve_instantiated_concept(
299
- concept: Concept, datasource: QueryDatasource
296
+ concept: Concept, datasource: QueryDatasource | Datasource
300
297
  ) -> Concept:
301
298
  if concept.address in datasource.output_concepts:
302
299
  return concept
@@ -309,14 +306,14 @@ def resolve_instantiated_concept(
309
306
 
310
307
 
311
308
  def get_node_joins(
312
- datasources: List[QueryDatasource],
309
+ datasources: List[QueryDatasource | Datasource],
313
310
  environment: Environment,
314
311
  # concepts:List[Concept],
315
312
  ):
316
313
 
317
314
  graph = nx.Graph()
318
315
  partials: dict[str, list[str]] = {}
319
- ds_node_map: dict[str, QueryDatasource] = {}
316
+ ds_node_map: dict[str, QueryDatasource | Datasource] = {}
320
317
  concept_map: dict[str, Concept] = {}
321
318
  for datasource in datasources:
322
319
  ds_node = f"ds~{datasource.identifier}"
@@ -5,7 +5,7 @@ from trilogy.core.graph_models import ReferenceGraph
5
5
  from trilogy.core.constants import CONSTANT_DATASET
6
6
  from trilogy.core.processing.concept_strategies_v3 import source_query_concepts
7
7
  from trilogy.core.enums import BooleanOperator
8
- from trilogy.constants import CONFIG, DEFAULT_NAMESPACE
8
+ from trilogy.constants import CONFIG
9
9
  from trilogy.core.processing.nodes import SelectNode, StrategyNode, History
10
10
  from trilogy.core.models import (
11
11
  Concept,
@@ -55,12 +55,12 @@ def base_join_to_join(
55
55
 
56
56
  def get_datasource_cte(datasource: Datasource | QueryDatasource) -> CTE:
57
57
  for cte in ctes:
58
- if cte.source.full_name == datasource.full_name:
58
+ if cte.source.identifier == datasource.identifier:
59
59
  return cte
60
60
  for cte in ctes:
61
- if cte.source.datasources[0].full_name == datasource.full_name:
61
+ if cte.source.datasources[0].identifier == datasource.identifier:
62
62
  return cte
63
- raise ValueError(f"Could not find CTE for datasource {datasource.full_name}")
63
+ raise ValueError(f"Could not find CTE for datasource {datasource.identifier}")
64
64
 
65
65
  if base_join.left_datasource is not None:
66
66
  left_cte = get_datasource_cte(base_join.left_datasource)
@@ -109,7 +109,7 @@ def generate_source_map(
109
109
  # now populate anything derived in this level
110
110
  for qdk, qdv in query_datasource.source_map.items():
111
111
  unnest = [x for x in qdv if isinstance(x, UnnestJoin)]
112
- for x in unnest:
112
+ for _ in unnest:
113
113
  source_map[qdk] = []
114
114
  if (
115
115
  qdk not in source_map
@@ -119,16 +119,18 @@ def generate_source_map(
119
119
  source_map[qdk] = []
120
120
  basic = [x for x in qdv if isinstance(x, Datasource)]
121
121
  for base in basic:
122
- source_map[qdk].append(base.name)
122
+ source_map[qdk].append(base.safe_identifier)
123
123
 
124
124
  ctes = [x for x in qdv if isinstance(x, QueryDatasource)]
125
125
  if ctes:
126
- names = set([x.name for x in ctes])
127
- matches = [cte for cte in all_new_ctes if cte.source.name in names]
126
+ names = set([x.safe_identifier for x in ctes])
127
+ matches = [
128
+ cte for cte in all_new_ctes if cte.source.safe_identifier in names
129
+ ]
128
130
 
129
131
  if not matches and names:
130
132
  raise SyntaxError(
131
- f"Missing parent CTEs for source map; expecting {names}, have {[cte.source.name for cte in all_new_ctes]}"
133
+ f"Missing parent CTEs for source map; expecting {names}, have {[cte.source.safe_identifier for cte in all_new_ctes]}"
132
134
  )
133
135
  for cte in matches:
134
136
  output_address = [
@@ -137,11 +139,11 @@ def generate_source_map(
137
139
  if x.address not in [z.address for z in cte.partial_concepts]
138
140
  ]
139
141
  if qdk in output_address:
140
- source_map[qdk].append(cte.name)
142
+ source_map[qdk].append(cte.safe_identifier)
141
143
  # now do a pass that accepts partials
142
144
  for cte in matches:
143
145
  if qdk not in source_map:
144
- source_map[qdk] = [cte.name]
146
+ source_map[qdk] = [cte.safe_identifier]
145
147
  if qdk not in source_map:
146
148
  if not qdv:
147
149
  source_map[qdk] = []
@@ -154,8 +156,10 @@ def generate_source_map(
154
156
  # as they cannot be referenced in row resolution
155
157
  existence_source_map: Dict[str, list[str]] = defaultdict(list)
156
158
  for ek, ev in query_datasource.existence_source_map.items():
157
- names = set([x.name for x in ev])
158
- ematches = [cte.name for cte in all_new_ctes if cte.source.name in names]
159
+ ids = set([x.safe_identifier for x in ev])
160
+ ematches = [
161
+ cte.name for cte in all_new_ctes if cte.source.safe_identifier in ids
162
+ ]
159
163
  existence_source_map[ek] = ematches
160
164
  return {
161
165
  k: [] if not v else list(set(v)) for k, v in source_map.items()
@@ -209,7 +213,7 @@ def resolve_cte_base_name_and_alias_v2(
209
213
  and not source.datasources[0].name == CONSTANT_DATASET
210
214
  ):
211
215
  ds = source.datasources[0]
212
- return ds.safe_location, ds.identifier
216
+ return ds.safe_location, ds.safe_identifier
213
217
 
214
218
  joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
215
219
  if joins and len(joins) > 0:
@@ -268,17 +272,17 @@ def datasource_to_ctes(
268
272
  # this is required to ensure that constant datasets
269
273
  # render properly on initial access; since they have
270
274
  # no actual source
271
- if source.full_name == DEFAULT_NAMESPACE + "_" + CONSTANT_DATASET:
275
+ if source.name == CONSTANT_DATASET:
272
276
  source_map = {k: [] for k in query_datasource.source_map}
273
277
  existence_map = source_map
274
278
  else:
275
279
  source_map = {
276
- k: [] if not v else [source.identifier]
280
+ k: [] if not v else [source.safe_identifier]
277
281
  for k, v in query_datasource.source_map.items()
278
282
  }
279
283
  existence_map = source_map
280
284
 
281
- human_id = generate_cte_name(query_datasource.full_name, name_map)
285
+ human_id = generate_cte_name(query_datasource.identifier, name_map)
282
286
 
283
287
  final_joins = [base_join_to_join(join, parents) for join in query_datasource.joins]
284
288
 
trilogy/executor.py CHANGED
@@ -276,6 +276,20 @@ class Executor(object):
276
276
  output.append(compiled_sql)
277
277
  return output
278
278
 
279
+ def parse_file(self, file: str | Path, persist: bool = False) -> Generator[
280
+ ProcessedQuery
281
+ | ProcessedQueryPersist
282
+ | ProcessedShowStatement
283
+ | ProcessedRawSQLStatement
284
+ | ProcessedCopyStatement,
285
+ None,
286
+ None,
287
+ ]:
288
+ file = Path(file)
289
+ with open(file, "r") as f:
290
+ command = f.read()
291
+ return self.parse_text_generator(command, persist=persist)
292
+
279
293
  def parse_text(
280
294
  self, command: str, persist: bool = False
281
295
  ) -> List[
@@ -319,9 +333,11 @@ class Executor(object):
319
333
  x = self.generator.generate_queries(
320
334
  self.environment, [t], hooks=self.hooks
321
335
  )[0]
336
+
337
+ yield x
338
+
322
339
  if persist and isinstance(x, ProcessedQueryPersist):
323
340
  self.environment.add_datasource(x.datasource)
324
- yield x
325
341
 
326
342
  def execute_raw_sql(
327
343
  self, command: str, variables: dict | None = None
@@ -77,7 +77,11 @@ def print_recursive_nodes(
77
77
  ]
78
78
  ]
79
79
  for child in input.parents:
80
- display += print_recursive_nodes(child, mode=mode, depth=depth + 1)
80
+ display += print_recursive_nodes(
81
+ child,
82
+ mode=mode,
83
+ depth=depth + 1,
84
+ )
81
85
  return display
82
86
 
83
87
 
@@ -123,6 +123,13 @@ from trilogy.parsing.common import (
123
123
  arbitrary_to_concept,
124
124
  process_function_args,
125
125
  )
126
+ from dataclasses import dataclass
127
+
128
+
129
+ @dataclass
130
+ class WholeGrainWrapper:
131
+ where: WhereClause
132
+
126
133
 
127
134
  CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
128
135
 
@@ -566,9 +573,11 @@ class ParseToObjects(Transformer):
566
573
  return args
567
574
 
568
575
  def grain_clause(self, args) -> Grain:
569
- # namespace=self.environment.namespace,
570
576
  return Grain(components=[self.environment.concepts[a] for a in args[0]])
571
577
 
578
+ def whole_grain_clause(self, args) -> WholeGrainWrapper:
579
+ return WholeGrainWrapper(where=args[0])
580
+
572
581
  def MULTILINE_STRING(self, args) -> str:
573
582
  return args[3:-3]
574
583
 
@@ -582,11 +591,14 @@ class ParseToObjects(Transformer):
582
591
  grain: Optional[Grain] = None
583
592
  address: Optional[Address] = None
584
593
  where: Optional[WhereClause] = None
594
+ non_partial_for: Optional[WhereClause] = None
585
595
  for val in args[1:]:
586
596
  if isinstance(val, Address):
587
597
  address = val
588
598
  elif isinstance(val, Grain):
589
599
  grain = val
600
+ elif isinstance(val, WholeGrainWrapper):
601
+ non_partial_for = val.where
590
602
  elif isinstance(val, Query):
591
603
  address = Address(location=f"({val.text})", is_query=True)
592
604
  elif isinstance(val, WhereClause):
@@ -596,7 +608,7 @@ class ParseToObjects(Transformer):
596
608
  "Malformed datasource, missing address or query declaration"
597
609
  )
598
610
  datasource = Datasource(
599
- identifier=name,
611
+ name=name,
600
612
  columns=columns,
601
613
  # grain will be set by default from args
602
614
  # TODO: move to factory
@@ -604,6 +616,7 @@ class ParseToObjects(Transformer):
604
616
  address=address,
605
617
  namespace=self.environment.namespace,
606
618
  where=where,
619
+ non_partial_for=non_partial_for,
607
620
  )
608
621
  for column in columns:
609
622
  column.concept = column.concept.with_grain(datasource.grain)
@@ -801,20 +814,10 @@ class ParseToObjects(Transformer):
801
814
  except Exception as e:
802
815
  raise ImportError(f"Unable to import file {target}, parsing error: {e}")
803
816
 
804
- for _, concept in nparser.environment.concepts.items():
805
- self.environment.add_concept(
806
- concept.with_namespace(alias), _ignore_cache=True
807
- )
808
-
809
- for _, datasource in nparser.environment.datasources.items():
810
- self.environment.add_datasource(
811
- datasource.with_namespace(alias), _ignore_cache=True
812
- )
813
817
  imps = ImportStatement(
814
818
  alias=alias, path=Path(args[0]), environment=nparser.environment
815
819
  )
816
- self.environment.imports[alias] = imps
817
- self.environment.gen_concept_list_caches()
820
+ self.environment.add_import(alias, nparser.environment, imps)
818
821
  return imps
819
822
 
820
823
  @v_args(meta=True)
@@ -841,7 +844,7 @@ class ParseToObjects(Transformer):
841
844
  if self.environment.namespace
842
845
  else DEFAULT_NAMESPACE
843
846
  ),
844
- identifier=identifier,
847
+ name=identifier,
845
848
  address=Address(location=address),
846
849
  grain=grain,
847
850
  )
trilogy/parsing/render.py CHANGED
@@ -80,6 +80,9 @@ class Renderer:
80
80
  metrics = []
81
81
  # first, keys
82
82
  for concept in arg.concepts.values():
83
+ if "__preql_internal" in concept.address:
84
+ continue
85
+
83
86
  # don't render anything that came from an import
84
87
  if concept.namespace in arg.imports:
85
88
  continue
@@ -122,10 +125,10 @@ class Renderer:
122
125
  for datasource in arg.datasources.values()
123
126
  if datasource.namespace == DEFAULT_NAMESPACE
124
127
  ]
125
- rendered_imports = [
126
- self.to_string(import_statement)
127
- for import_statement in arg.imports.values()
128
- ]
128
+ rendered_imports = []
129
+ for _, imports in arg.imports.items():
130
+ for import_statement in imports:
131
+ rendered_imports.append(self.to_string(import_statement))
129
132
  components = []
130
133
  if rendered_imports:
131
134
  components.append(rendered_imports)
@@ -133,19 +136,26 @@ class Renderer:
133
136
  components.append(rendered_concepts)
134
137
  if rendered_datasources:
135
138
  components.append(rendered_datasources)
139
+
136
140
  final = "\n\n".join("\n".join(x) for x in components)
137
141
  return final
138
142
 
139
143
  @to_string.register
140
144
  def _(self, arg: Datasource):
141
145
  assignments = ",\n ".join([self.to_string(x) for x in arg.columns])
146
+ if arg.non_partial_for:
147
+ non_partial = f"\ncomplete where {self.to_string(arg.non_partial_for)}"
148
+ else:
149
+ non_partial = ""
142
150
  base = f"""datasource {arg.name} (
143
151
  {assignments}
144
152
  )
145
- {self.to_string(arg.grain)}
153
+ {self.to_string(arg.grain)}{non_partial}
146
154
  {self.to_string(arg.address)}"""
155
+
147
156
  if arg.where:
148
157
  base += f"\nwhere {self.to_string(arg.where)}"
158
+
149
159
  base += ";"
150
160
  return base
151
161
 
@@ -214,9 +224,15 @@ class Renderer:
214
224
 
215
225
  @to_string.register
216
226
  def _(self, arg: "ColumnAssignment"):
227
+ if arg.modifiers:
228
+ modifiers = "".join(
229
+ [self.to_string(modifier) for modifier in arg.modifiers]
230
+ )
231
+ else:
232
+ modifiers = ""
217
233
  if isinstance(arg.alias, str):
218
- return f"{arg.alias}: {self.to_string(arg.concept)}"
219
- return f"{self.to_string(arg.alias)}: {self.to_string(arg.concept)}"
234
+ return f"{arg.alias}: {modifiers}{self.to_string(arg.concept)}"
235
+ return f"{self.to_string(arg.alias)}: {modifiers}{self.to_string(arg.concept)}"
220
236
 
221
237
  @to_string.register
222
238
  def _(self, arg: "RawColumnExpr"):
@@ -352,6 +368,8 @@ class Renderer:
352
368
 
353
369
  @to_string.register
354
370
  def _(self, arg: "ImportStatement"):
371
+ if arg.alias == DEFAULT_NAMESPACE:
372
+ return f"import {arg.path};"
355
373
  return f"import {arg.path} as {arg.alias};"
356
374
 
357
375
  @to_string.register
@@ -35,8 +35,10 @@
35
35
  prop_ident: "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "." IDENTIFIER
36
36
 
37
37
  // datasource concepts
38
- datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? (address | query) where?
39
-
38
+ datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? whole_grain_clause? (address | query) where?
39
+
40
+ whole_grain_clause: "complete" where
41
+
40
42
  grain_clause: "grain" "(" column_list ")"
41
43
 
42
44
  address: "address" (QUOTED_ADDRESS | ADDRESS)