pytrilogy 0.0.1.107__py3-none-any.whl → 0.0.1.108__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.1.107
3
+ Version: 0.0.1.108
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,4 +1,4 @@
1
- trilogy/__init__.py,sha256=ouq-RNu0DVYw8n1C2ekRmcAJ_SL_PFzbRBzo2O814TM,292
1
+ trilogy/__init__.py,sha256=XUWHIoJWL4bt0ApBe3huE8vvIiYJxxfm4krEBMhpLUc,292
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=LxiK2TiVQPEa6tXkxWk9DJHOR3zsGNSqgQuqtOf66cw,518
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
@@ -9,53 +9,52 @@ trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
11
11
  trilogy/core/enums.py,sha256=KEZQTzJ8tlGIukuUwQUIG1FTHOP1B4i0EeCgFjfsbDw,5394
12
- trilogy/core/env_processor.py,sha256=dfiUp5QxcrgG5YI3Py4xP4OTDRZqltWJGQp8PwukkfY,1401
12
+ trilogy/core/env_processor.py,sha256=SU-jpaGfoWLe9sGTeQYG1qjVnwGQ7TwctmnJRlfzluc,1459
13
13
  trilogy/core/environment_helpers.py,sha256=mzBDHhdF9ssZ_-LY8CcaM_ddfJavkpRYrFImUd3cjXI,5972
14
14
  trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
15
15
  trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
16
  trilogy/core/functions.py,sha256=zkRReytiotOBAW-a3Ri5eoejZDYTt2-7Op80ZxZxUmw,9129
17
17
  trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=WH7GHgn1a3xegVo12_NTP7V_ptN-_ObY7s4ZgkPI0D4,108548
20
- trilogy/core/optimization.py,sha256=SpWRQL1biAUvMCijk2I-FCQY2KzXd4eiu3ZlxzVE-uQ,4505
21
- trilogy/core/query_processor.py,sha256=w_CS2TEmSk8Bhk6ukxLavQgQyA9UwcgoPChouLREujQ,11747
19
+ trilogy/core/models.py,sha256=JvqqYCQorhHRa7haVRBYCGmmjYq4jWKjOgPqbf2eTNI,109050
20
+ trilogy/core/optimization.py,sha256=chfzpLVJo9eg8H4e2hdnpRqWMDTQ3tJWPdDfGESa-EU,4510
21
+ trilogy/core/query_processor.py,sha256=6BqLYPwyFkRtueTIRFZi3IcVFTpbpGRNowayhSn3_AY,11805
22
22
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- trilogy/core/processing/concept_strategies_v3.py,sha256=6Z1ODKbvxaWFMRHa3vX87oSBW9XKtC7S7pUxburCVkA,22369
23
+ trilogy/core/processing/concept_strategies_v3.py,sha256=27lZXFLgDEF3sh2MUR7HX_atVz7TC1fJB7z3oxa1TcY,22610
24
24
  trilogy/core/processing/graph_utils.py,sha256=ulCJ4hYAISbUxLD6VM2fah9RBPGIXSEHEPeRBSFl0Rs,1197
25
25
  trilogy/core/processing/utility.py,sha256=Gk35HgyIG2SSUyI5OHZcB0bw1PZUVC_aNc9Sre6xPQU,10535
26
- trilogy/core/processing/node_generators/__init__.py,sha256=1L1TWnGkrSQlLe9ZuMG8DMGfS755v0fjCdz_W1ofCJQ,747
26
+ trilogy/core/processing/node_generators/__init__.py,sha256=LIs6uBEum8LDc-26zjyAwjxa-ay2ok9tKtPjDNvbVkE,757
27
27
  trilogy/core/processing/node_generators/basic_node.py,sha256=tVPmg0r0kDdABkmn6z4sxsk1hKy9yTT_Xvl1eVN2Zck,2162
28
28
  trilogy/core/processing/node_generators/common.py,sha256=RWaynnlCeF8bhGXVLYWpUFrdngMF95TJwCFue73bXZo,8881
29
- trilogy/core/processing/node_generators/concept_merge.py,sha256=oKxRuc7m6wnsrxAaLg3q2-dM2dyFgAvaXCp6nmG--jU,5113
29
+ trilogy/core/processing/node_generators/concept_merge_node.py,sha256=TRbOIjLWfLB0Nl6YmMV1ao0qhPP6OQDd9M3UViWkCBU,6621
30
30
  trilogy/core/processing/node_generators/filter_node.py,sha256=CGALiTzKhPAvXPFAguIQfjf6I3pjlafY0uaaM9MTkIE,3414
31
31
  trilogy/core/processing/node_generators/group_node.py,sha256=xWI1xNIXEOj6jlRGD9hcv2_vVNvY6lpzJl6pQ8HuFBE,2988
32
32
  trilogy/core/processing/node_generators/group_to_node.py,sha256=BzPdYwzoo8gRMH7BDffTTXq4z-mjfCEzvfB5I-P0_nw,2941
33
- trilogy/core/processing/node_generators/merge_node.py,sha256=sQQ9jhw1oAJh649DBAJX6U7r_E_piFS95mxKvm7pxqQ,5818
34
33
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=vP84dnLQy6dtypi6mUbt9sMAcmmrTgQ1Oz4GI6X1IEo,6421
34
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=sQQ9jhw1oAJh649DBAJX6U7r_E_piFS95mxKvm7pxqQ,5818
35
35
  trilogy/core/processing/node_generators/rowset_node.py,sha256=K-aoLi0OSfNADXR5_vxDcNv2dJeFy30XNp_IaaWWJ6o,4684
36
36
  trilogy/core/processing/node_generators/select_node.py,sha256=xeCqIUEubrf3u_QQfbGdf1BG4fO0HYQ64hiFur8NUqY,20080
37
37
  trilogy/core/processing/node_generators/unnest_node.py,sha256=s1VXQZSf1LnX3ISeQ5JzmzmCKUw30-5OK_f0YTB9_48,1031
38
38
  trilogy/core/processing/node_generators/window_node.py,sha256=ekazi5eXxnShpcp-qukXNG4DHFdULoXrX-YWUWLNEpM,2527
39
- trilogy/core/processing/nodes/__init__.py,sha256=ZkDGQksvsM5uNia5rhXFCUJcpTRhoYYFdyfJw-Eiu8s,3674
40
- trilogy/core/processing/nodes/base_node.py,sha256=VaK4rWV8PQMyTPTgStmCbPbV0mmTHyTwBhz0C0N2KG0,8961
39
+ trilogy/core/processing/nodes/__init__.py,sha256=gzKxGSduIQ5QwpMWrmwSYiE8sg2mWejwVn0VvjYc6s0,3879
40
+ trilogy/core/processing/nodes/base_node.py,sha256=075SP0fAqCZGxxE9Rn5-Qfd2mWd9J7NannGFkNfTkuY,9068
41
41
  trilogy/core/processing/nodes/filter_node.py,sha256=DqSRv8voEajPZqzeeiIsxuv4ubvsmeQcCW6x_v2CmOk,1359
42
42
  trilogy/core/processing/nodes/group_node.py,sha256=Y_NWB_AwFrE-YithjZ7lYYDN4e0el4su3ICq2EIr3HA,3837
43
- trilogy/core/processing/nodes/merge_node.py,sha256=pAtRTqkpsfE_pmqPSdeV0rHwFPzclJ3WItRLX8AuuJw,12609
43
+ trilogy/core/processing/nodes/merge_node.py,sha256=a9JkWNSb5ikRnr33DJrlIpZ_OXVK7XDF4FFnqNab4wI,12758
44
44
  trilogy/core/processing/nodes/select_node_v2.py,sha256=tAADeVruch-flFiedbY1zi7ukMG2RpWecvxxZ5aL3ZU,6354
45
45
  trilogy/core/processing/nodes/unnest_node.py,sha256=t4kY3a_dR3iXistPemStfdw0uJfnxwTcoQg1HiDa3xo,1501
46
46
  trilogy/core/processing/nodes/window_node.py,sha256=QjAWgqBZqFSRCPwc7JBmgQJobWW50rsHI0pjJe0Zzg0,926
47
47
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- trilogy/dialect/base.py,sha256=nYrm7Z-GnVVhr5vWHjMghWkGoq2r7ogzoGVasAGTxGo,29223
48
+ trilogy/dialect/base.py,sha256=xPB5mh6471VJLHxNdXrYvi7q7vJC_tioVR1LrLcoZc0,29394
49
49
  trilogy/dialect/bigquery.py,sha256=9vxQn2BMv_oTGQSWQpoN5ho_OgqMWaHH9e-5vQVf44c,2906
50
50
  trilogy/dialect/common.py,sha256=zWrYmvevlXznocw9uGHmY5Ws1rp_kICm9zA_ulTe4eg,2165
51
- trilogy/dialect/config.py,sha256=JdGIiHf2EVoFNTYzqQUy1bMmzqZiFTjcnYglzAMa4dM,3351
51
+ trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
52
52
  trilogy/dialect/duckdb.py,sha256=Ddyt68sr8IL2HnZMenyytoD65FXwY_O2pz1McyS0bis,3075
53
53
  trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
54
54
  trilogy/dialect/postgres.py,sha256=r47xbCA7nfEYENofiVfLZ-SnReNfDmUmW4OSHVkkP4E,3206
55
- trilogy/dialect/presto.py,sha256=rOr-ftb0S5AeEncQgp6jj1iCWlCEkzZuQC3TGMUcemg,2790
55
+ trilogy/dialect/presto.py,sha256=8zjRn8AeYXZQGuUi-afyBWLet8o-LSt6gm5IH7bTdiw,2987
56
56
  trilogy/dialect/snowflake.py,sha256=N3HknYgN-fjD7BLX1Ucj-ss_ku2Ox8DgLsF3BIHutHo,2941
57
57
  trilogy/dialect/sql_server.py,sha256=UrLeA9bxiFJ4qpGsqVJqBybQCyJhetMebe8IzQW1q9s,2900
58
- trilogy/docs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
58
  trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
59
  trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
61
60
  trilogy/hooks/graph_hook.py,sha256=i-Tv9sxZU0sMc-God8bLLz-nAg4-wYafogZtHaU8LXw,801
@@ -70,9 +69,9 @@ trilogy/parsing/parse_engine.py,sha256=TLy56pDatDfzfwbJkrJ-XXB05s_VW9_iRrkwtKR0G
70
69
  trilogy/parsing/render.py,sha256=fxjpq2FZLgllw_d4cru-t_IXNPAz2DmYkT7v9ED0XRI,11540
71
70
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
71
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
73
- pytrilogy-0.0.1.107.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
74
- pytrilogy-0.0.1.107.dist-info/METADATA,sha256=KF68qL9kNj855oUEtUVCCMd5hei-LRfABfoaEWlTa8g,7882
75
- pytrilogy-0.0.1.107.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
76
- pytrilogy-0.0.1.107.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
77
- pytrilogy-0.0.1.107.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
78
- pytrilogy-0.0.1.107.dist-info/RECORD,,
72
+ pytrilogy-0.0.1.108.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
73
+ pytrilogy-0.0.1.108.dist-info/METADATA,sha256=HUP7pQQZBmSTKuUUioE53ys3fCS76Xjwj2CF5SykMOs,7882
74
+ pytrilogy-0.0.1.108.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
75
+ pytrilogy-0.0.1.108.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
76
+ pytrilogy-0.0.1.108.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
77
+ pytrilogy-0.0.1.108.dist-info/RECORD,,
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.1.107"
7
+ __version__ = "0.0.1.108"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -21,6 +21,8 @@ def generate_graph(
21
21
  for source in concept.sources:
22
22
  generic = source.with_default_grain()
23
23
  g.add_edge(generic, node_name)
24
+
25
+ # link the concept to the generic source
24
26
  if concept.derivation == PurposeLineage.MERGE:
25
27
  g.add_edge(node_name, generic)
26
28
  for _, dataset in environment.datasources.items():
trilogy/core/models.py CHANGED
@@ -1512,7 +1512,7 @@ class MergeStatement(Namespaced, BaseModel):
1512
1512
  if z.address == x.address:
1513
1513
  return z
1514
1514
  raise SyntaxError(
1515
- f"Could not find upstream map for multiselect {str(concept)} on cte ({cte})"
1515
+ f"Could not find upstream map for multiselect {str(concept)} on cte ({cte.name})"
1516
1516
  )
1517
1517
 
1518
1518
  def with_namespace(self, namespace: str) -> "MergeStatement":
@@ -1779,6 +1779,7 @@ class QueryDatasource(BaseModel):
1779
1779
  source_type: SourceType = SourceType.SELECT
1780
1780
  partial_concepts: List[Concept] = Field(default_factory=list)
1781
1781
  join_derived_concepts: List[Concept] = Field(default_factory=list)
1782
+ hidden_concepts: List[Concept] = Field(default_factory=list)
1782
1783
  force_group: bool | None = None
1783
1784
 
1784
1785
  @property
@@ -1997,6 +1998,7 @@ class CTE(BaseModel):
1997
1998
  condition: Optional[Union["Conditional", "Comparison", "Parenthetical"]] = None
1998
1999
  partial_concepts: List[Concept] = Field(default_factory=list)
1999
2000
  join_derived_concepts: List[Concept] = Field(default_factory=list)
2001
+ hidden_concepts: List[Concept] = Field(default_factory=list)
2000
2002
  order_by: Optional[OrderBy] = None
2001
2003
  limit: Optional[int] = None
2002
2004
  requires_nesting: bool = True
@@ -2433,6 +2435,7 @@ class Environment(BaseModel):
2433
2435
  cte_name_map: Dict[str, str] = Field(default_factory=dict)
2434
2436
 
2435
2437
  materialized_concepts: List[Concept] = Field(default_factory=list)
2438
+ merged_concepts: Dict[str, Concept] = Field(default_factory=dict)
2436
2439
  _parse_count: int = 0
2437
2440
 
2438
2441
  @classmethod
@@ -2460,7 +2463,7 @@ class Environment(BaseModel):
2460
2463
  f.write(self.model_dump_json())
2461
2464
  return ppath
2462
2465
 
2463
- def gen_materialized_concepts(self) -> None:
2466
+ def gen_concept_list_caches(self) -> None:
2464
2467
  concrete_addresses = set()
2465
2468
  for datasource in self.datasources.values():
2466
2469
  for concept in datasource.output_concepts:
@@ -2468,6 +2471,12 @@ class Environment(BaseModel):
2468
2471
  self.materialized_concepts = [
2469
2472
  c for c in self.concepts.values() if c.address in concrete_addresses
2470
2473
  ]
2474
+ for concept in self.concepts.values():
2475
+ if concept.derivation == PurposeLineage.MERGE:
2476
+ ms = concept.lineage
2477
+ assert isinstance(ms, MergeStatement)
2478
+ for parent in ms.concepts:
2479
+ self.merged_concepts[parent.address] = concept
2471
2480
 
2472
2481
  def validate_concept(self, lookup: str, meta: Meta | None = None):
2473
2482
  existing: Concept = self.concepts.get(lookup) # type: ignore
@@ -2503,7 +2512,7 @@ class Environment(BaseModel):
2503
2512
  self.concepts[f"{alias}.{key}"] = concept.with_namespace(alias)
2504
2513
  for key, datasource in environment.datasources.items():
2505
2514
  self.datasources[f"{alias}.{key}"] = datasource.with_namespace(alias)
2506
- self.gen_materialized_concepts()
2515
+ self.gen_concept_list_caches()
2507
2516
  return self
2508
2517
 
2509
2518
  def add_file_import(self, path: str, alias: str, env: Environment | None = None):
@@ -2602,7 +2611,7 @@ class Environment(BaseModel):
2602
2611
  from trilogy.core.environment_helpers import generate_related_concepts
2603
2612
 
2604
2613
  generate_related_concepts(concept, self)
2605
- self.gen_materialized_concepts()
2614
+ self.gen_concept_list_caches()
2606
2615
  return concept
2607
2616
 
2608
2617
  def add_datasource(
@@ -2612,12 +2621,12 @@ class Environment(BaseModel):
2612
2621
  ):
2613
2622
  if not datasource.namespace or datasource.namespace == DEFAULT_NAMESPACE:
2614
2623
  self.datasources[datasource.name] = datasource
2615
- self.gen_materialized_concepts()
2624
+ self.gen_concept_list_caches()
2616
2625
  return datasource
2617
2626
  self.datasources[datasource.namespace + "." + datasource.identifier] = (
2618
2627
  datasource
2619
2628
  )
2620
- self.gen_materialized_concepts()
2629
+ self.gen_concept_list_caches()
2621
2630
  return datasource
2622
2631
 
2623
2632
 
@@ -136,6 +136,6 @@ def optimize_ctes(
136
136
  select.where_clause.conditional if select.where_clause else None
137
137
  )
138
138
  root_cte.requires_nesting = False
139
- sort_select_output(cte, select)
139
+ sort_select_output(root_cte, select)
140
140
 
141
141
  return filter_irrelevant_ctes(input, root_cte)
@@ -337,7 +337,9 @@ def generate_node(
337
337
 
338
338
 
339
339
  def validate_stack(
340
- stack: List[StrategyNode], concepts: List[Concept], accept_partial: bool = False
340
+ stack: List[StrategyNode],
341
+ concepts: List[Concept],
342
+ accept_partial: bool = False,
341
343
  ) -> tuple[ValidationResult, set[str], set[str], set[str]]:
342
344
  found_map = defaultdict(set)
343
345
  found_addresses: set[str] = set()
@@ -357,6 +359,7 @@ def validate_stack(
357
359
  if accept_partial:
358
360
  found_addresses.add(concept.address)
359
361
  found_map[str(node)].add(concept)
362
+ # zip in those we know we found
360
363
  if not all([c.address in found_addresses for c in concepts]):
361
364
  return (
362
365
  ValidationResult.INCOMPLETE,
@@ -388,7 +391,7 @@ def search_concepts(
388
391
  hist = history.get_history(mandatory_list, accept_partial)
389
392
  if hist is not False:
390
393
  logger.info(
391
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Returning search node from history"
394
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Returning search node from history for {[c.address for c in mandatory_list]} with accept_partial {accept_partial}"
392
395
  )
393
396
  assert not isinstance(hist, bool)
394
397
  return hist
@@ -454,7 +457,6 @@ def _search_concepts(
454
457
  if node:
455
458
  stack.append(node)
456
459
  node.resolve()
457
- found.add(priority_concept.address)
458
460
  # these concepts should not be attempted to be sourced again
459
461
  # as fetching them requires operating on a subset of concepts
460
462
  if priority_concept.derivation in [
@@ -473,11 +475,12 @@ def _search_concepts(
473
475
  complete, found, missing, partial = validate_stack(
474
476
  stack, mandatory_list, accept_partial
475
477
  )
476
- # early exit if we have a complete stack with one node
478
+
477
479
  logger.info(
478
480
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} finished concept loop for {priority_concept} flag for accepting partial addresses is "
479
481
  f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} partial {partial}), attempted {attempted}"
480
482
  )
483
+ # early exit if we have a complete stack with one node
481
484
  # we can only early exit if we have a complete stack
482
485
  # and we are not looking for more non-partial sources
483
486
  if complete == ValidationResult.COMPLETE and (
@@ -514,6 +517,10 @@ def _search_concepts(
514
517
  parents=stack,
515
518
  depth=depth,
516
519
  partial_concepts=all_partial,
520
+ # always hide merge concepts
521
+ hidden_concepts=[
522
+ x for x in mandatory_list if x.derivation == PurposeLineage.MERGE
523
+ ],
517
524
  )
518
525
 
519
526
  # ensure we can resolve our final merge
@@ -5,10 +5,10 @@ from .group_to_node import gen_group_to_node
5
5
  from .basic_node import gen_basic_node
6
6
  from .select_node import gen_select_node
7
7
  from .unnest_node import gen_unnest_node
8
- from .merge_node import gen_merge_node
8
+ from .node_merge_node import gen_merge_node
9
9
  from .rowset_node import gen_rowset_node
10
10
  from .multiselect_node import gen_multiselect_node
11
- from .concept_merge import gen_concept_merge_node
11
+ from .concept_merge_node import gen_concept_merge_node
12
12
 
13
13
  __all__ = [
14
14
  "gen_filter_node",
@@ -7,27 +7,30 @@ from trilogy.core.processing.nodes import MergeNode, NodeJoin, History
7
7
  from trilogy.core.processing.nodes.base_node import concept_list_to_grain, StrategyNode
8
8
  from typing import List
9
9
 
10
- from trilogy.core.enums import JoinType
10
+ from trilogy.core.enums import JoinType, PurposeLineage
11
11
  from trilogy.constants import logger
12
12
  from trilogy.core.processing.utility import padding
13
13
  from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
14
14
  from itertools import combinations
15
15
  from trilogy.core.processing.node_generators.common import resolve_join_order
16
+ from trilogy.utility import unique
17
+
16
18
 
17
19
  LOGGER_PREFIX = "[GEN_CONCEPT_MERGE_NODE]"
18
20
 
19
21
 
20
- def merge_joins(base: MergeStatement, parents: List[StrategyNode]) -> List[NodeJoin]:
22
+ def merge_joins(
23
+ parents: List[StrategyNode], merge_concepts: List[Concept]
24
+ ) -> List[NodeJoin]:
21
25
  output = []
22
26
  for left, right in combinations(parents, 2):
23
27
  output.append(
24
28
  NodeJoin(
25
29
  left_node=left,
26
30
  right_node=right,
27
- concepts=[
28
- base.merge_concept,
29
- ],
31
+ concepts=merge_concepts,
30
32
  join_type=JoinType.FULL,
33
+ filter_to_mutual=True,
31
34
  )
32
35
  )
33
36
  return resolve_join_order(output)
@@ -50,37 +53,70 @@ def gen_concept_merge_node(
50
53
  lineage: MergeStatement = concept.lineage
51
54
 
52
55
  base_parents: List[StrategyNode] = []
56
+
57
+ # get additional concepts that should be merged across the environments
58
+ additional_merge: List[Concept] = [*lineage.concepts]
59
+ for x in local_optional:
60
+ if x.address in environment.merged_concepts:
61
+ ms = environment.merged_concepts[x.address].lineage
62
+ assert isinstance(ms, MergeStatement)
63
+ additional_merge += ms.concepts
64
+
53
65
  for select in lineage.concepts:
54
66
  # if it's a merge concept, filter it out of the optional
55
67
  sub_optional = [
56
68
  x
57
69
  for x in local_optional
58
- if x.address not in lineage.concepts_lcl and x.namespace == select.namespace
70
+ if x.address not in environment.merged_concepts
71
+ and x.namespace == select.namespace
72
+ ]
73
+
74
+ sub_additional_merge = [
75
+ x for x in additional_merge if x.namespace == select.namespace
59
76
  ]
77
+ sub_optional += sub_additional_merge
78
+ final: List[Concept] = unique([select] + sub_optional, "address")
60
79
  snode: StrategyNode = source_concepts(
61
- mandatory_list=[select] + sub_optional,
80
+ mandatory_list=final,
62
81
  environment=environment,
63
82
  g=g,
64
83
  depth=depth + 1,
65
84
  history=history,
66
85
  )
86
+
67
87
  if not snode:
68
88
  logger.info(
69
89
  f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge node for {concept}"
70
90
  )
71
91
  return None
72
- snode.add_output_concept(lineage.merge_concept)
92
+ for x in sub_additional_merge:
93
+ snode.add_output_concept(environment.merged_concepts[x.address])
73
94
  base_parents.append(snode)
74
95
 
75
- node_joins = merge_joins(lineage, base_parents)
96
+ node_joins = merge_joins(
97
+ base_parents,
98
+ unique(
99
+ [environment.merged_concepts[x.address] for x in additional_merge],
100
+ "address",
101
+ ),
102
+ )
76
103
 
77
104
  enrichment = set([x.address for x in local_optional])
78
- outputs = [x for y in base_parents for x in y.output_concepts]
105
+ outputs = [
106
+ x
107
+ for y in base_parents
108
+ for x in y.output_concepts
109
+ if x.derivation != PurposeLineage.MERGE
110
+ ]
79
111
 
80
112
  additional_relevant = [x for x in outputs if x.address in enrichment]
113
+ final_outputs = outputs + additional_relevant + [concept]
81
114
  node = MergeNode(
82
115
  input_concepts=[x for y in base_parents for x in y.output_concepts],
83
- output_concepts=outputs + additional_relevant + [concept],
116
+ output_concepts=[x for x in final_outputs],
117
+ hidden_concepts=[
118
+ x for x in final_outputs if x.derivation == PurposeLineage.MERGE
119
+ ],
84
120
  environment=environment,
85
121
  g=g,
86
122
  depth=depth,
@@ -127,9 +163,19 @@ def gen_concept_merge_node(
127
163
  )
128
164
  return node
129
165
 
166
+ # we still need the hidden concepts to be returned to the search
167
+ # since they must be on the final node
168
+ # to avoid further recursion
169
+ # TODO: let the downstream search know they were found
130
170
  return MergeNode(
131
171
  input_concepts=enrich_node.output_concepts + node.output_concepts,
172
+ # also filter out the
132
173
  output_concepts=node.output_concepts + local_optional,
174
+ hidden_concepts=[
175
+ x
176
+ for x in node.output_concepts + local_optional
177
+ if x.derivation == PurposeLineage.MERGE
178
+ ],
133
179
  environment=environment,
134
180
  g=g,
135
181
  depth=depth,
@@ -27,12 +27,18 @@ class History(BaseModel):
27
27
  self,
28
28
  search: list[Concept],
29
29
  accept_partial: bool = False,
30
+ parent_key: str = "",
30
31
  ) -> StrategyNode | None | bool:
32
+ key = self._concepts_to_lookup(
33
+ search,
34
+ accept_partial,
35
+ )
36
+ if parent_key and parent_key == key:
37
+ raise ValueError(
38
+ f"Parent key {parent_key} is the same as the current key {key}"
39
+ )
31
40
  return self.history.get(
32
- self._concepts_to_lookup(
33
- search,
34
- accept_partial,
35
- ),
41
+ key,
36
42
  False,
37
43
  )
38
44
 
@@ -107,6 +107,7 @@ class StrategyNode:
107
107
  conditions: Conditional | Comparison | Parenthetical | None = None,
108
108
  force_group: bool | None = None,
109
109
  grain: Optional[Grain] = None,
110
+ hidden_concepts: List[Concept] | None = None,
110
111
  ):
111
112
  self.input_concepts: List[Concept] = (
112
113
  unique(input_concepts, "address") if input_concepts else []
@@ -129,6 +130,7 @@ class StrategyNode:
129
130
  self.grain = grain
130
131
  self.force_group = force_group
131
132
  self.tainted = False
133
+ self.hidden_concepts = hidden_concepts or []
132
134
  for parent in self.parents:
133
135
  if not parent:
134
136
  raise SyntaxError("Unresolvable parent")
@@ -104,6 +104,7 @@ class MergeNode(StrategyNode):
104
104
  depth: int = 0,
105
105
  grain: Grain | None = None,
106
106
  conditions: Conditional | None = None,
107
+ hidden_concepts: List[Concept] | None = None,
107
108
  ):
108
109
  super().__init__(
109
110
  input_concepts=input_concepts,
@@ -117,6 +118,7 @@ class MergeNode(StrategyNode):
117
118
  force_group=force_group,
118
119
  grain=grain,
119
120
  conditions=conditions,
121
+ hidden_concepts=hidden_concepts,
120
122
  )
121
123
  self.join_concepts = join_concepts
122
124
  self.force_join_type = force_join_type
@@ -327,5 +329,6 @@ class MergeNode(StrategyNode):
327
329
  partial_concepts=self.partial_concepts,
328
330
  force_group=force_group,
329
331
  condition=self.conditions,
332
+ hidden_concepts=self.hidden_concepts,
330
333
  )
331
334
  return qds
@@ -216,6 +216,7 @@ def datasource_to_ctes(
216
216
  condition=query_datasource.condition,
217
217
  partial_concepts=query_datasource.partial_concepts,
218
218
  join_derived_concepts=query_datasource.join_derived_concepts,
219
+ hidden_concepts=query_datasource.hidden_concepts,
219
220
  )
220
221
  if cte.grain != query_datasource.grain:
221
222
  raise ValueError("Grain was corrupted in CTE generation")
trilogy/dialect/base.py CHANGED
@@ -437,6 +437,7 @@ class BaseDialect:
437
437
  self.render_concept_sql(c, cte)
438
438
  for c in cte.output_columns
439
439
  if c.address not in [y.address for y in cte.join_derived_concepts]
440
+ and c.address not in [y.address for y in cte.hidden_concepts]
440
441
  ] + [
441
442
  f"{self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
442
443
  for c in cte.join_derived_concepts
@@ -444,7 +445,9 @@ class BaseDialect:
444
445
  else:
445
446
  # otherwse, assume we are unnesting directly in the select
446
447
  select_columns = [
447
- self.render_concept_sql(c, cte) for c in cte.output_columns
448
+ self.render_concept_sql(c, cte)
449
+ for c in cte.output_columns
450
+ if c.address not in [y.address for y in cte.hidden_concepts]
448
451
  ]
449
452
  return CompiledCTE(
450
453
  name=cte.name,
trilogy/dialect/config.py CHANGED
@@ -100,22 +100,7 @@ class PrestoConfig(DialectConfig):
100
100
  return f"presto://{self.username}:{self.password}@{self.host}:{self.port}/{self.catalog}"
101
101
 
102
102
 
103
- class TrinoConfig(DialectConfig):
104
- def __init__(
105
- self,
106
- host: str,
107
- port: int,
108
- username: str,
109
- password: str,
110
- catalog: str,
111
- schema: str | None = None,
112
- ):
113
- self.host = host
114
- self.port = port
115
- self.username = username
116
- self.password = password
117
- self.catalog = catalog
118
- self.schema = schema
103
+ class TrinoConfig(PrestoConfig):
119
104
 
120
105
  def connection_string(self) -> str:
121
106
  if self.schema:
trilogy/dialect/presto.py CHANGED
@@ -27,6 +27,9 @@ FUNCTION_MAP = {
27
27
  FunctionType.QUARTER: lambda x: f"EXTRACT(QUARTER from {x[0]})",
28
28
  # math
29
29
  FunctionType.DIVIDE: lambda x: f"SAFE_DIVIDE({x[0]},{x[1]})",
30
+ FunctionType.DATE_ADD: lambda x: f"DATE_ADD('{x[1]}', {x[2]}, {x[0]})",
31
+ FunctionType.CURRENT_DATE: lambda x: "CURRENT_DATE",
32
+ FunctionType.CURRENT_DATETIME: lambda x: "CURRENT_TIMESTAMP",
30
33
  }
31
34
 
32
35
  FUNCTION_GRAIN_MATCH_MAP = {
@@ -42,11 +45,10 @@ CREATE OR REPLACE TABLE {{ output.address }} AS
42
45
  {% endif %}{%- if ctes %}
43
46
  WITH {% for cte in ctes %}
44
47
  {{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
45
-
46
- SELECT
47
48
  {%- if full_select -%}
48
49
  {{full_select}}
49
- {%- else -%}
50
+ {%- else %}
51
+ SELECT
50
52
  {%- for select in select_columns %}
51
53
  {{ select }}{% if not loop.last %},{% endif %}{% endfor %}
52
54
  {% if base %}FROM
trilogy/docs/__init__.py DELETED
File without changes