pytrilogy 0.0.2.12__py3-none-any.whl → 0.0.2.13__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.12
3
+ Version: 0.0.2.13
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,14 +1,14 @@
1
- trilogy/__init__.py,sha256=qXNp3R3OFRd_QUIgMqKJ4RGQoBEMVcm6s2DHSehlVWU,291
1
+ trilogy/__init__.py,sha256=U1mVKIIGVmjHu6kwRMyNhb3buZHWWzbk-c9Hu7FiBGQ,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- trilogy/constants.py,sha256=HRQq4i3cpSEJCywt61QKEzRO1jd4tEPZNSBuxUA_7yg,922
3
+ trilogy/constants.py,sha256=rZJh3fAx3ljxf_QZNECR-devR6QXkYc9mpLCxIWNqB0,960
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
- trilogy/executor.py,sha256=5cRbU4Rj7p1pNV76rfp1pz704Hx_0q8_O8HFURjgXxQ,11016
5
+ trilogy/executor.py,sha256=PZr7IF8wS1Oi2WJGE-B3lp70Y8ue2uuauODw02chjdQ,11175
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
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
11
- trilogy/core/enums.py,sha256=WwQPOLfSdL27qhqXw4JTkMyUNvj57T3Xz9M3JUZzhZ8,5940
11
+ trilogy/core/enums.py,sha256=BRYqy-NgIacCYTJo0B11m5XQWSHq5pfxhoLd8pzA3ho,6025
12
12
  trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
13
13
  trilogy/core/environment_helpers.py,sha256=1miP4is4FEoci01KSAy2VZVYmlmT5TOCOALBekd2muQ,7211
14
14
  trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
@@ -16,9 +16,9 @@ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,56
16
16
  trilogy/core/functions.py,sha256=ARJAyBjeS415-54k3G_bx807rkPZonEulMaLRxSP7vU,10371
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=kJcSNv6JX79KBEDTZlIyc1nAsn34fWUQLWZN3y7oTVs,143710
19
+ trilogy/core/models.py,sha256=9-1kYfq4NqAthe50Jgs0WjBnZtEWJw0ReCBfFhVxD0Y,144799
20
20
  trilogy/core/optimization.py,sha256=7E-Ol51u6ZAxF56F_bzLxgRO-Hu6Yl1ZbPopZJB2tqk,7533
21
- trilogy/core/query_processor.py,sha256=Y8C03J9PSyXQoARiMmFomYhnP13L61XjRKNOD7nIops,19520
21
+ trilogy/core/query_processor.py,sha256=JJFBVBmT5QNsJ9rSDKNJBINLm7YW7i5fjAp98H0Wcd8,19281
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
@@ -29,8 +29,8 @@ trilogy/core/processing/concept_strategies_v3.py,sha256=ae6FmwiKNiEbOU2GhnzggFMh
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
30
  trilogy/core/processing/utility.py,sha256=QKaZL5yJzGJBWCirgB1cAKgcDOibhyk7ETvHveb3GOE,14604
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
- trilogy/core/processing/node_generators/basic_node.py,sha256=EfCCYleCXVWeoCOUih1VtfUXewg1oyG7EdUMRQOyyMk,3135
33
- trilogy/core/processing/node_generators/common.py,sha256=lDBRq9X6dQ_xSwXxLLNDq2pW8D-XwAY-ylTJLMugkLw,9525
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=IHj5jEloUe5yojGRLAzt35FcfHqGviWQdS8ETyvr39Q,3292
33
+ trilogy/core/processing/node_generators/common.py,sha256=WY41zjxSCG13n3mdUHmcE0mUQ0gtWxz4nugBgxHONd8,9327
34
34
  trilogy/core/processing/node_generators/filter_node.py,sha256=Ij2WqyOsu-TFxhAcL50PLMGpghsSWXJnWEJ8yTqOwrY,8228
35
35
  trilogy/core/processing/node_generators/group_node.py,sha256=Du-9uFXD0M-aHq2MV7v5R3QCrAL0JZBFMW-YQwgb6Bw,3135
36
36
  trilogy/core/processing/node_generators/group_to_node.py,sha256=nzITnhaALIT7FMonyo16nNo-kSrLfefa9sZBYecrvkU,2887
@@ -41,10 +41,10 @@ trilogy/core/processing/node_generators/select_node.py,sha256=E8bKOAUpwLwZy1iiaF
41
41
  trilogy/core/processing/node_generators/unnest_node.py,sha256=aZeixbOzMtXi7BPahKr9bOkIhTciyD9Klsj0kZ56F6s,2189
42
42
  trilogy/core/processing/node_generators/window_node.py,sha256=lFfmEjX_mLB7MuOM6CuKNnks1CabokGImpwhbQzjnkE,3283
43
43
  trilogy/core/processing/nodes/__init__.py,sha256=jyduHk96j5fpju72sc8swOiBjR3Md866kt8JZGkp3ZU,4866
44
- trilogy/core/processing/nodes/base_node.py,sha256=szquAzrIkCTXlVhAVSHt6HSJ7rw3b8lfjeO5eFIcEU8,13067
44
+ trilogy/core/processing/nodes/base_node.py,sha256=P4VyOhbgPgTLaXEIftbVVmmEPviScJJhi9v6hSxjC7M,13155
45
45
  trilogy/core/processing/nodes/filter_node.py,sha256=DBOSGFfkiILrZa1BlLv2uxUSkgWtSIKiZplqyKXPjg8,2132
46
46
  trilogy/core/processing/nodes/group_node.py,sha256=wE6tgyCUL74v76O8jACDm4oYMov4dAlwzLa5xMYReAA,6294
47
- trilogy/core/processing/nodes/merge_node.py,sha256=34eEH-denk9kkzD8FcZvxgDSMUB9K5e4lSeNpbqSt7I,14456
47
+ trilogy/core/processing/nodes/merge_node.py,sha256=KOTdYli_T3c5RwMaK_73W5UNtKPA0F8hjDJF9jQx2fs,14491
48
48
  trilogy/core/processing/nodes/select_node_v2.py,sha256=QuXNcwgjTRYamOoIooGrp4ie6INcqA9whtC5LZWjD8s,7180
49
49
  trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
50
50
  trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
@@ -65,18 +65,18 @@ trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2
65
65
  trilogy/hooks/query_debugger.py,sha256=NDChfkPmmW-KINa4TaQmDe_adGiwsKFdGLDSYpbodeU,4282
66
66
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
67
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- trilogy/parsing/common.py,sha256=rLF7SFj_qtLh92ox-cHrtVSyjgl1aTaa7qZJdR1RDuA,8182
68
+ trilogy/parsing/common.py,sha256=fa3R0xHXQkU8BUN8quPe7qCzez6qaRbnctkYaLgHLxY,8863
69
69
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
70
70
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
71
71
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
72
- trilogy/parsing/parse_engine.py,sha256=upL2pmq34vwNqzOtmb0EB22tbEZ4TTiK46J5qPq-_yw,62841
72
+ trilogy/parsing/parse_engine.py,sha256=yCHd6RVRijNctZJZ3iRJG2263UZL2n8EoFA_-Qfr88E,62966
73
73
  trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
74
- trilogy/parsing/trilogy.lark,sha256=QNJnExOdvJyKTrQA4ffh-SGIz7rYd93kf2Ccs0m3cn4,11498
74
+ trilogy/parsing/trilogy.lark,sha256=00j0D77gpTBsyet9WGN4Ir9Nc-YvNwXFr2toRiASb_M,11525
75
75
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
77
- pytrilogy-0.0.2.12.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
- pytrilogy-0.0.2.12.dist-info/METADATA,sha256=i7Cd69-1p3XsssoHNy1IqbvtZaNeaKpQErvJp940JMw,7907
79
- pytrilogy-0.0.2.12.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
80
- pytrilogy-0.0.2.12.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
- pytrilogy-0.0.2.12.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
- pytrilogy-0.0.2.12.dist-info/RECORD,,
77
+ pytrilogy-0.0.2.13.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
+ pytrilogy-0.0.2.13.dist-info/METADATA,sha256=AJXWyDVSN9EhYPYJlrkXhMVULqg3HyscbZGDhgVd-FA,7907
79
+ pytrilogy-0.0.2.13.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
80
+ pytrilogy-0.0.2.13.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
+ pytrilogy-0.0.2.13.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
+ pytrilogy-0.0.2.13.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.1.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.12"
7
+ __version__ = "0.0.2.13"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -28,6 +28,11 @@ class Optimizations:
28
28
  direct_return: bool = True
29
29
 
30
30
 
31
+ @dataclass
32
+ class Comments:
33
+ pass
34
+
35
+
31
36
  # TODO: support loading from environments
32
37
  @dataclass
33
38
  class Config:
trilogy/core/enums.py CHANGED
@@ -62,6 +62,8 @@ class Modifier(Enum):
62
62
  strval = str(value)
63
63
  if strval == "~":
64
64
  return Modifier.PARTIAL
65
+ elif strval == "?":
66
+ return Modifier.NULLABLE
65
67
  return super()._missing_(value=strval.capitalize())
66
68
 
67
69
 
@@ -273,6 +275,7 @@ class SourceType(Enum):
273
275
  CONSTANT = "constant"
274
276
  ROWSET = "rowset"
275
277
  MERGE = "merge"
278
+ BASIC = "basic"
276
279
 
277
280
 
278
281
  class ShowCategory(Enum):
trilogy/core/models.py CHANGED
@@ -2264,6 +2264,7 @@ class QueryDatasource(BaseModel):
2264
2264
  @field_validator("joins")
2265
2265
  @classmethod
2266
2266
  def validate_joins(cls, v):
2267
+ unique_pairs = set()
2267
2268
  for join in v:
2268
2269
  if not isinstance(join, BaseJoin):
2269
2270
  continue
@@ -2271,7 +2272,16 @@ class QueryDatasource(BaseModel):
2271
2272
  raise SyntaxError(
2272
2273
  f"Cannot join a datasource to itself, joining {join.left_datasource}"
2273
2274
  )
2274
-
2275
+ pairing = "".join(
2276
+ sorted(
2277
+ [join.left_datasource.identifier, join.right_datasource.identifier]
2278
+ )
2279
+ )
2280
+ if pairing in unique_pairs:
2281
+ raise SyntaxError(
2282
+ f"Duplicate join {join.left_datasource.identifier} and {join.right_datasource.identifier}"
2283
+ )
2284
+ unique_pairs.add(pairing)
2275
2285
  return v
2276
2286
 
2277
2287
  @field_validator("input_concepts")
@@ -2386,6 +2396,11 @@ class QueryDatasource(BaseModel):
2386
2396
  final_source_map[key] = other.source_map[key]
2387
2397
  for k, v in final_source_map.items():
2388
2398
  final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2399
+ self_hidden = self.hidden_concepts or []
2400
+ other_hidden = other.hidden_concepts or []
2401
+ hidden = [
2402
+ x for x in self_hidden if x.address in [y.address for y in other_hidden]
2403
+ ]
2389
2404
  qds = QueryDatasource(
2390
2405
  input_concepts=unique(
2391
2406
  self.input_concepts + other.input_concepts, "address"
@@ -2409,9 +2424,7 @@ class QueryDatasource(BaseModel):
2409
2424
  ),
2410
2425
  join_derived_concepts=self.join_derived_concepts,
2411
2426
  force_group=self.force_group,
2412
- hidden_concepts=unique(
2413
- self.hidden_concepts + other.hidden_concepts, "address"
2414
- ),
2427
+ hidden_concepts=hidden,
2415
2428
  )
2416
2429
 
2417
2430
  return qds
@@ -2557,6 +2570,7 @@ class CTE(BaseModel):
2557
2570
  @property
2558
2571
  def comment(self) -> str:
2559
2572
  base = f"Target: {str(self.grain)}."
2573
+ base += f" Source: {self.source.source_type}."
2560
2574
  if self.parent_ctes:
2561
2575
  base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
2562
2576
  if self.joins:
@@ -2565,6 +2579,11 @@ class CTE(BaseModel):
2565
2579
  base += (
2566
2580
  f"\n-- Partials: {', '.join([str(x) for x in self.partial_concepts])}."
2567
2581
  )
2582
+ base += f"\n-- Source Map: {self.source_map}."
2583
+ base += f"\n-- Output: {', '.join([str(x) for x in self.output_columns])}."
2584
+ if self.hidden_concepts:
2585
+ base += f"\n-- Hidden: {', '.join([str(x) for x in self.hidden_concepts])}."
2586
+
2568
2587
  return base
2569
2588
 
2570
2589
  def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
@@ -2632,6 +2651,10 @@ class CTE(BaseModel):
2632
2651
  f" {self.name} {other.name} conditions {self.condition} {other.condition}"
2633
2652
  )
2634
2653
  raise ValueError(error)
2654
+ mutually_hidden = []
2655
+ for concept in self.hidden_concepts:
2656
+ if concept in other.hidden_concepts:
2657
+ mutually_hidden.append(concept)
2635
2658
  self.partial_concepts = unique(
2636
2659
  self.partial_concepts + other.partial_concepts, "address"
2637
2660
  )
@@ -2654,9 +2677,7 @@ class CTE(BaseModel):
2654
2677
  self.source.output_concepts = unique(
2655
2678
  self.source.output_concepts + other.source.output_concepts, "address"
2656
2679
  )
2657
- self.hidden_concepts = unique(
2658
- self.hidden_concepts + other.hidden_concepts, "address"
2659
- )
2680
+ self.hidden_concepts = mutually_hidden
2660
2681
  self.existence_source_map = {
2661
2682
  **self.existence_source_map,
2662
2683
  **other.existence_source_map,
@@ -3012,6 +3033,9 @@ class EnvironmentDatasourceDict(dict):
3012
3033
  def values(self) -> ValuesView[Datasource]: # type: ignore
3013
3034
  return super().values()
3014
3035
 
3036
+ def items(self) -> ItemsView[str, Datasource]: # type: ignore
3037
+ return super().items()
3038
+
3015
3039
 
3016
3040
  class EnvironmentConceptDict(dict):
3017
3041
  def __init__(self, *args, **kwargs) -> None:
@@ -10,6 +10,7 @@ from trilogy.core.processing.node_generators.common import (
10
10
  )
11
11
  from trilogy.utility import unique
12
12
  from trilogy.constants import logger
13
+ from trilogy.core.enums import SourceType
13
14
  from itertools import combinations
14
15
 
15
16
  LOGGER_PREFIX = "[GEN_BASIC_NODE]"
@@ -35,7 +36,11 @@ def gen_basic_node(
35
36
  attempts: List[tuple[list[Concept], list[Concept]]] = [
36
37
  (parent_concepts, [concept] + local_optional_redundant)
37
38
  ]
38
- equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
39
+ equivalent_optional = [
40
+ x
41
+ for x in local_optional
42
+ if x.lineage == concept.lineage and x.address != concept.address
43
+ ]
39
44
  non_equivalent_optional = [
40
45
  x for x in local_optional if x not in equivalent_optional
41
46
  ]
@@ -61,8 +66,10 @@ def gen_basic_node(
61
66
  depth=depth + 1,
62
67
  history=history,
63
68
  )
69
+
64
70
  if not parent_node:
65
71
  continue
72
+ parent_node.source_type = SourceType.BASIC
66
73
  parents: List[StrategyNode] = [parent_node]
67
74
  for x in basic_output:
68
75
  sources = [p for p in parents if x in p.output_concepts]
@@ -1,4 +1,4 @@
1
- from typing import List, Tuple
1
+ from typing import List, Tuple, Callable
2
2
 
3
3
 
4
4
  from trilogy.core.enums import PurposeLineage, Purpose
@@ -96,6 +96,7 @@ def gen_property_enrichment_node(
96
96
  g,
97
97
  depth: int,
98
98
  source_concepts,
99
+ log_lambda: Callable,
99
100
  history: History | None = None,
100
101
  conditions: WhereClause | None = None,
101
102
  ):
@@ -106,8 +107,8 @@ def gen_property_enrichment_node(
106
107
  keys = "-".join([y.address for y in x.keys])
107
108
  required_keys[keys].add(x.address)
108
109
  final_nodes = []
109
- node_joins = []
110
110
  for _k, vs in required_keys.items():
111
+ log_lambda(f"Generating enrichment node for {_k} with {vs}")
111
112
  ks = _k.split("-")
112
113
  enrich_node: StrategyNode = source_concepts(
113
114
  mandatory_list=[environment.concepts[k] for k in ks]
@@ -119,17 +120,6 @@ def gen_property_enrichment_node(
119
120
  conditions=conditions,
120
121
  )
121
122
  final_nodes.append(enrich_node)
122
- node_joins.append(
123
- NodeJoin(
124
- left_node=enrich_node,
125
- right_node=base_node,
126
- concepts=concept_to_relevant_joins(
127
- [environment.concepts[k] for k in ks]
128
- ),
129
- filter_to_mutual=False,
130
- join_type=JoinType.LEFT_OUTER,
131
- )
132
- )
133
123
  return MergeNode(
134
124
  input_concepts=unique(
135
125
  base_node.output_concepts
@@ -146,9 +136,8 @@ def gen_property_enrichment_node(
146
136
  g=g,
147
137
  parents=[
148
138
  base_node,
149
- enrich_node,
150
- ],
151
- node_joins=node_joins,
139
+ ]
140
+ + final_nodes,
152
141
  )
153
142
 
154
143
 
@@ -197,6 +186,7 @@ def gen_enrichment_node(
197
186
  source_concepts,
198
187
  history=history,
199
188
  conditions=conditions,
189
+ log_lambda=log_lambda,
200
190
  )
201
191
 
202
192
  enrich_node: StrategyNode = source_concepts( # this fetches the parent + join keys
@@ -216,14 +206,14 @@ def gen_enrichment_node(
216
206
  log_lambda(
217
207
  f"{str(type(base_node).__name__)} returning merge node with group node + enrichment node"
218
208
  )
219
-
209
+ non_hidden = [
210
+ x
211
+ for x in base_node.output_concepts
212
+ if x.address not in [y.address for y in base_node.hidden_concepts]
213
+ ]
220
214
  return MergeNode(
221
- input_concepts=unique(
222
- join_keys + extra_required + base_node.output_concepts, "address"
223
- ),
224
- output_concepts=unique(
225
- join_keys + extra_required + base_node.output_concepts, "address"
226
- ),
215
+ input_concepts=unique(join_keys + extra_required + non_hidden, "address"),
216
+ output_concepts=unique(join_keys + extra_required + non_hidden, "address"),
227
217
  environment=environment,
228
218
  g=g,
229
219
  parents=[enrich_node, base_node],
@@ -61,11 +61,9 @@ def resolve_concept_map(
61
61
  for concept in input.output_concepts:
62
62
  if concept.address not in input.non_partial_concept_addresses:
63
63
  continue
64
-
65
- if (
66
- isinstance(input, QueryDatasource)
67
- and concept.address in input.hidden_concepts
68
- ):
64
+ if isinstance(input, QueryDatasource) and concept.address in [
65
+ x.address for x in input.hidden_concepts
66
+ ]:
69
67
  continue
70
68
  if concept.address in full_addresses:
71
69
 
@@ -82,10 +80,9 @@ def resolve_concept_map(
82
80
  for concept in input.output_concepts:
83
81
  if concept.address not in [t.address for t in inherited_inputs]:
84
82
  continue
85
- if (
86
- isinstance(input, QueryDatasource)
87
- and concept.address in input.hidden_concepts
88
- ):
83
+ if isinstance(input, QueryDatasource) and concept.address in [
84
+ x.address for x in input.hidden_concepts
85
+ ]:
89
86
  continue
90
87
  if len(concept_map.get(concept.address, [])) == 0:
91
88
  concept_map[concept.address].add(input)
@@ -94,6 +91,7 @@ def resolve_concept_map(
94
91
  if target.address not in inherited:
95
92
  # an empty source means it is defined in this CTE
96
93
  concept_map[target.address] = set()
94
+
97
95
  return concept_map
98
96
 
99
97
 
@@ -226,7 +224,10 @@ class StrategyNode:
226
224
  def remove_output_concepts(self, concepts: List[Concept]):
227
225
  for x in concepts:
228
226
  self.hidden_concepts.append(x)
229
- self.output_concepts = [x for x in self.output_concepts if x not in concepts]
227
+ addresses = [x.address for x in concepts]
228
+ self.output_concepts = [
229
+ x for x in self.output_concepts if x.address not in addresses
230
+ ]
230
231
  self.rebuild_cache()
231
232
 
232
233
  @property
@@ -257,6 +258,7 @@ class StrategyNode:
257
258
  targets=self.output_concepts,
258
259
  inherited_inputs=self.input_concepts + self.existence_concepts,
259
260
  )
261
+
260
262
  return QueryDatasource(
261
263
  input_concepts=self.input_concepts,
262
264
  output_concepts=self.output_concepts,
@@ -371,7 +371,7 @@ class MergeNode(StrategyNode):
371
371
  conditions=self.conditions,
372
372
  hidden_concepts=list(self.hidden_concepts),
373
373
  virtual_output_concepts=list(self.virtual_output_concepts),
374
- node_joins=self.node_joins,
374
+ node_joins=list(self.node_joins) if self.node_joins else None,
375
375
  join_concepts=list(self.join_concepts) if self.join_concepts else None,
376
376
  force_join_type=self.force_join_type,
377
377
  existence_concepts=list(self.existence_concepts),
@@ -35,7 +35,6 @@ from trilogy.core.ergonomics import CTE_NAMES
35
35
  from trilogy.core.optimization import optimize_ctes
36
36
  from math import ceil
37
37
  from collections import defaultdict
38
- from random import shuffle
39
38
 
40
39
  LOGGER_PREFIX = "[QUERY BUILD]"
41
40
 
@@ -128,8 +127,6 @@ def generate_source_map(
128
127
  if qdk in output_address:
129
128
  source_map[qdk].append(cte.name)
130
129
  # now do a pass that accepts partials
131
- # TODO: move this into a second loop by first creationg all sub sources
132
- # then loop through this
133
130
  for cte in matches:
134
131
  if qdk not in source_map:
135
132
  source_map[qdk] = [cte.name]
@@ -180,7 +177,6 @@ def generate_cte_name(full_name: str, name_map: dict[str, str]) -> str:
180
177
  int = ceil(idx / len(CTE_NAMES))
181
178
  suffix = f"_{int}"
182
179
  valid = [x for x in CTE_NAMES if x + suffix not in name_map.values()]
183
- shuffle(valid)
184
180
  lookup = valid[0]
185
181
  new_name = f"{lookup}{suffix}"
186
182
  name_map[full_name] = new_name
@@ -334,12 +330,13 @@ def append_existence_check(
334
330
  for subselect in where.existence_arguments:
335
331
  if not subselect:
336
332
  continue
337
- logger.info(
338
- f"{LOGGER_PREFIX} fetching existance clause inputs {[str(c) for c in subselect]}"
339
- )
333
+
340
334
  eds = source_query_concepts(
341
335
  [*subselect], environment=environment, g=graph, history=history
342
336
  )
337
+ logger.info(
338
+ f"{LOGGER_PREFIX} fetching existence clause inputs {[str(c) for c in subselect]}"
339
+ )
343
340
  node.add_parents([eds])
344
341
  node.add_existence_concepts([*subselect])
345
342
 
@@ -384,9 +381,7 @@ def get_query_node(
384
381
  if nest_where and statement.where_clause:
385
382
  if not all_aggregate:
386
383
  ods.conditions = statement.where_clause.conditional
387
- ods.output_concepts = statement.output_components
388
- # ods.hidden_concepts = where_delta
389
- ods.rebuild_cache()
384
+ ods.set_output_concepts(statement.output_components)
390
385
  append_existence_check(ods, environment, graph, history)
391
386
  ds = GroupNode(
392
387
  output_concepts=statement.output_components,
trilogy/executor.py CHANGED
@@ -300,10 +300,16 @@ class Executor(object):
300
300
  self.environment.add_datasource(x.datasource)
301
301
  yield x
302
302
 
303
- def execute_raw_sql(self, command: str) -> CursorResult:
303
+ def execute_raw_sql(
304
+ self, command: str, variables: dict | None = None
305
+ ) -> CursorResult:
304
306
  """Run a command against the raw underlying
305
307
  execution engine"""
306
- return self.connection.execute(text(command))
308
+ if variables:
309
+ return self.connection.execute(text(command), variables)
310
+ return self.connection.execute(
311
+ text(command),
312
+ )
307
313
 
308
314
  def execute_text(self, command: str) -> List[CursorResult]:
309
315
  """Run a preql text command"""
trilogy/parsing/common.py CHANGED
@@ -50,7 +50,7 @@ def process_function_args(
50
50
  id_hash = string_to_hash(str(arg))
51
51
  concept = function_to_concept(
52
52
  arg,
53
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
53
+ name=f"{VIRTUAL_CONCEPT_PREFIX}_{arg.operator.value}_{id_hash}",
54
54
  namespace=environment.namespace,
55
55
  )
56
56
  # to satisfy mypy, concept will always have metadata
@@ -255,20 +255,34 @@ def arbitrary_to_concept(
255
255
  | str
256
256
  ),
257
257
  namespace: str,
258
- name: str,
258
+ name: str | None = None,
259
259
  metadata: Metadata | None = None,
260
260
  purpose: Purpose | None = None,
261
261
  ) -> Concept:
262
262
 
263
263
  if isinstance(parent, AggregateWrapper):
264
+ if not name:
265
+ name = (
266
+ f"_agg_{parent.function.operator.value}_{string_to_hash(str(parent))}"
267
+ )
264
268
  return agg_wrapper_to_concept(parent, namespace, name, metadata, purpose)
265
269
  elif isinstance(parent, WindowItem):
270
+ if not name:
271
+ name = f"_window_{parent.type.value}_{string_to_hash(str(parent))}"
266
272
  return window_item_to_concept(parent, name, namespace, purpose, metadata)
267
273
  elif isinstance(parent, FilterItem):
274
+ if not name:
275
+ name = f"_filter_{parent.content.name}_{string_to_hash(str(parent))}"
268
276
  return filter_item_to_concept(parent, name, namespace, purpose, metadata)
269
277
  elif isinstance(parent, Function):
278
+ if not name:
279
+ name = f"_func_{parent.operator.value}_{string_to_hash(str(parent))}"
270
280
  return function_to_concept(parent, name, namespace)
271
281
  elif isinstance(parent, ListWrapper):
282
+ if not name:
283
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
272
284
  return constant_to_concept(parent, name, namespace, purpose, metadata)
273
285
  else:
286
+ if not name:
287
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
274
288
  return constant_to_concept(parent, name, namespace, purpose, metadata)
@@ -16,7 +16,6 @@ from trilogy.core.internal import INTERNAL_NAMESPACE, ALL_ROWS_CONCEPT
16
16
  from trilogy.constants import (
17
17
  DEFAULT_NAMESPACE,
18
18
  NULL_VALUE,
19
- VIRTUAL_CONCEPT_PREFIX,
20
19
  MagicConstants,
21
20
  )
22
21
  from trilogy.core.enums import (
@@ -109,7 +108,6 @@ from trilogy.core.models import (
109
108
  HavingClause,
110
109
  )
111
110
  from trilogy.parsing.exceptions import ParseError
112
- from trilogy.utility import string_to_hash
113
111
  from trilogy.parsing.common import (
114
112
  agg_wrapper_to_concept,
115
113
  window_item_to_concept,
@@ -739,8 +737,8 @@ class ParseToObjects(Transformer):
739
737
  x = arbitrary_to_concept(
740
738
  x,
741
739
  namespace=namespace,
742
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(x))}",
743
740
  )
741
+ self.environment.add_concept(x)
744
742
  return x
745
743
 
746
744
  return [
@@ -781,6 +779,11 @@ class ParseToObjects(Transformer):
781
779
  def rawsql_statement(self, meta: Meta, args) -> RawSQLStatement:
782
780
  return RawSQLStatement(meta=Metadata(line_number=meta.line), text=args[0])
783
781
 
782
+ def resolve_import_address(self, address) -> str:
783
+ with open(address, "r", encoding="utf-8") as f:
784
+ text = f.read()
785
+ return text
786
+
784
787
  def import_statement(self, args: list[str]) -> ImportStatement:
785
788
  alias = args[-1]
786
789
  path = args[0].split(".")
@@ -790,8 +793,7 @@ class ParseToObjects(Transformer):
790
793
  nparser = self.parsed[target]
791
794
  else:
792
795
  try:
793
- with open(target, "r", encoding="utf-8") as f:
794
- text = f.read()
796
+ text = self.resolve_import_address(target)
795
797
  nparser = ParseToObjects(
796
798
  visit_tokens=True,
797
799
  text=text,
@@ -1093,7 +1095,6 @@ class ParseToObjects(Transformer):
1093
1095
  left = arbitrary_to_concept(
1094
1096
  args[0],
1095
1097
  namespace=self.environment.namespace,
1096
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(args[0]))}",
1097
1098
  )
1098
1099
  self.environment.add_concept(left)
1099
1100
  else:
@@ -1102,7 +1103,6 @@ class ParseToObjects(Transformer):
1102
1103
  right = arbitrary_to_concept(
1103
1104
  args[2],
1104
1105
  namespace=self.environment.namespace,
1105
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(args[2]))}",
1106
1106
  )
1107
1107
  self.environment.add_concept(right)
1108
1108
  else:
@@ -1137,7 +1137,6 @@ class ParseToObjects(Transformer):
1137
1137
  right = arbitrary_to_concept(
1138
1138
  right,
1139
1139
  namespace=self.environment.namespace,
1140
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(right))}",
1141
1140
  )
1142
1141
  self.environment.add_concept(right, meta=meta)
1143
1142
  return SubselectComparison(
@@ -1186,8 +1185,9 @@ class ParseToObjects(Transformer):
1186
1185
  def window_item_order(self, args):
1187
1186
  return WindowItemOrder(contents=args[0])
1188
1187
 
1189
- def window_item(self, args) -> WindowItem:
1190
- type = args[0]
1188
+ @v_args(meta=True)
1189
+ def window_item(self, meta, args) -> WindowItem:
1190
+ type: WindowType = args[0]
1191
1191
  order_by = []
1192
1192
  over = []
1193
1193
  index = None
@@ -1203,6 +1203,14 @@ class ParseToObjects(Transformer):
1203
1203
  concept = self.environment.concepts[item]
1204
1204
  elif isinstance(item, Concept):
1205
1205
  concept = item
1206
+ elif isinstance(item, WindowType):
1207
+ type = item
1208
+ else:
1209
+ concept = arbitrary_to_concept(
1210
+ item,
1211
+ namespace=self.environment.namespace,
1212
+ )
1213
+ self.environment.add_concept(concept, meta=meta)
1206
1214
  assert concept
1207
1215
  return WindowItem(
1208
1216
  type=type, content=concept, over=over, order_by=order_by, index=index
@@ -90,9 +90,9 @@
90
90
 
91
91
 
92
92
  // rank/lag/lead
93
- WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i) /[\s]+/
93
+ WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i | "avg"i | "max"i | "min"i ) /[\s]+/
94
94
 
95
- window_item: WINDOW_TYPE int_lit? concept_lit window_item_over? window_item_order?
95
+ window_item: WINDOW_TYPE int_lit? expr window_item_over? window_item_order?
96
96
 
97
97
  window_item_over: ("OVER"i over_list)
98
98
 
@@ -296,7 +296,7 @@
296
296
 
297
297
  MODIFIER: "Optional"i | "Partial"i | "Nullable"i
298
298
 
299
- SHORTHAND_MODIFIER: "~"
299
+ SHORTHAND_MODIFIER: "~" | "?"
300
300
 
301
301
  struct_type: "struct"i "<" ((data_type | IDENTIFIER) ",")* (data_type | IDENTIFIER) ","? ">"
302
302