pytrilogy 0.0.3.64__py3-none-any.whl → 0.0.3.66__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.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.64
3
+ Version: 0.0.3.66
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.64.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=qOtWjDJ0j0xbQBLf8qO2rhFCUnbYN4Ds9lNcOIOte1k,303
1
+ pytrilogy-0.0.3.66.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=aRLUllOSoHKDIW__yBx1aitL_E96Qtj4AGz-mFHB16g,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=lv_aJWP6dn6e2aF4BAE72jbnNtceFddfqtiDSsvzno0,1692
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -17,19 +17,19 @@ trilogy/core/environment_helpers.py,sha256=VvPIiFemqaLLpIpLIqprfu63K7muZ1YzNg7UZ
17
17
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
18
18
  trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,673
19
19
  trilogy/core/functions.py,sha256=poVfAwet1xdxTkC7WL38UmGRDpUVO9iSMNWSagl9_r4,29302
20
- trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
20
+ trilogy/core/graph_models.py,sha256=wIT-oBchHWE46GLDkgN5K7EzhOBEo8LfaeWV5G5cYcE,3302
21
21
  trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
22
22
  trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
23
- trilogy/core/query_processor.py,sha256=QiE_w5HgheT4GLZFnaLssJ4plf4voK0TeTd6N3jhR6A,20188
23
+ trilogy/core/query_processor.py,sha256=kBvQex0Xi2-u8DvAbAo_LXFTMKzMkFvwL-SCrIXU41k,20310
24
24
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
25
25
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- trilogy/core/models/author.py,sha256=8XbIsQr6cQrgo9uzee5qRoYiMdEG7yKF4FiiWImW7U0,77490
27
- trilogy/core/models/build.py,sha256=80v9rxwl41O8_7uIJoHK4tnTUfgR6u8EPrwHg4ySqO4,63323
26
+ trilogy/core/models/author.py,sha256=44VyEInWied287lJp99T14Ahq8RSQK4q3Y-AzAByMTY,77818
27
+ trilogy/core/models/build.py,sha256=yR3GtkIgLrT88B_wu9geqHWxnEI4joUWQSYW7_9Gjk0,63489
28
28
  trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
29
29
  trilogy/core/models/core.py,sha256=EMAuWTngoNVGCdfNrAY7_k6g528iodNQLwPRVip-8DA,10980
30
30
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
31
- trilogy/core/models/environment.py,sha256=tM8SwH9r1ZSc-F0Enod7r1NuqzGpQfLcPxnrtK5Hqpk,27398
32
- trilogy/core/models/execute.py,sha256=hOilC-lka4W-C8Pakb0Vd1-T0oskeWdC8Ls0bm8_388,43109
31
+ trilogy/core/models/environment.py,sha256=7bkxUob5pNgvK7Om-qvlJgsDiCh5iSPlMHI7tN_OZhU,27717
32
+ trilogy/core/models/execute.py,sha256=94CZVY_EdW675n8SihDnhaGTV0dq7BAAsl2Anf1mPmk,41815
33
33
  trilogy/core/optimizations/__init__.py,sha256=YH2-mGXZnVDnBcWVi8vTbrdw7Qs5TivG4h38rH3js_I,290
34
34
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
35
35
  trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
@@ -49,17 +49,17 @@ trilogy/core/processing/node_generators/filter_node.py,sha256=0hdfiS2I-Jvr6P-il3
49
49
  trilogy/core/processing/node_generators/group_node.py,sha256=nIfiMrJQEksUfqAeeA3X5PS1343y4lmPTipYuCa-rvs,6141
50
50
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
51
51
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
52
- trilogy/core/processing/node_generators/node_merge_node.py,sha256=-suq5RSkI9SZLH7p69lLL5nMaUDVfsRvpIZ9wiuoRps,16647
52
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=-sVhRHB3NqNIOk_e7LLVhW17Rjcf82hxaHjEvpF5Q-w,16858
53
53
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
54
54
  trilogy/core/processing/node_generators/rowset_node.py,sha256=2BiSsegbRF9csJ_Xl8P_CxIm4dAAb7dF29u6v_Odr-A,6709
55
- trilogy/core/processing/node_generators/select_merge_node.py,sha256=Usa58nRrp-nww9g2QtihKa_5B_duY3nMCjXnqT-bIY0,21557
55
+ trilogy/core/processing/node_generators/select_merge_node.py,sha256=3GDGi1tNIfuKO_FMrNCfp-G1c3lxdRuuufcmomYLt4s,21446
56
56
  trilogy/core/processing/node_generators/select_node.py,sha256=3dvw0d53eUtCRCUPN6J48I3qBEX1Wha7saQ_ndPu6_I,1777
57
57
  trilogy/core/processing/node_generators/synonym_node.py,sha256=CN2swdGPEP_Irx4GykHp4gyLCK0dWd2vX7PYJUGxw7w,3548
58
58
  trilogy/core/processing/node_generators/union_node.py,sha256=VNo6Oey4p8etU9xrOh2oTT2lIOTvY6PULUPRvVa2uxU,2877
59
59
  trilogy/core/processing/node_generators/unnest_node.py,sha256=ueOQtoTf2iJHO09RzWHDFQ5iKZq2fVhGf2KAF2U2kU8,2677
60
60
  trilogy/core/processing/node_generators/window_node.py,sha256=GP3Hvkbb0TDA6ef7W7bmvQEHVH-NRIfBT_0W4fcH3g4,6529
61
61
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=GMW07bb6hXurhF0hZLYoMAKSIS65tat5hwBjvqqPeSA,6516
62
+ trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=Dw0pjdhuJX0p-18CjelLrIJoevAPKzZOTN9uSLNPOF4,6553
63
63
  trilogy/core/processing/nodes/__init__.py,sha256=zTge1EzwzEydlcMliIFO_TT7h7lS8l37lyZuQDir1h0,5487
64
64
  trilogy/core/processing/nodes/base_node.py,sha256=p6yljFNLQsXz277c5wTATMNqsKUbsdP_3e7--tezBMw,17691
65
65
  trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
@@ -76,7 +76,7 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
76
76
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
77
77
  trilogy/core/statements/execute.py,sha256=rqfuoMuXPcH7L7TmE1dSiZ_K_A1ohB8whVMfGimZBOk,1294
78
78
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- trilogy/dialect/base.py,sha256=5lmOd6MOkM0H-Yss9l6uyEJ4JTainPiEv1xVL9sEAh8,43102
79
+ trilogy/dialect/base.py,sha256=fM5tPJA5yi6tTHUTWpHldh4RApJUmrosDorbrRQibe4,43613
80
80
  trilogy/dialect/bigquery.py,sha256=6ghCqy-k7UioIJc1EEQ7gRo_PHaO8Vm7yYbiQ-kgpzs,3629
81
81
  trilogy/dialect/common.py,sha256=hhzuMTFW9QQIP7TKLT9BlJy6lw2R03a68jKQ-7t4-2c,6070
82
82
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
@@ -89,11 +89,11 @@ trilogy/dialect/snowflake.py,sha256=LQIcHuyuGZXbxrv6sH17aLXLzw7yFVuRoE9M4doNk5k,
89
89
  trilogy/dialect/sql_server.py,sha256=z2Vg7Qvw83rbGiEFIvHHLqVWJTWiz2xs76kpQj4HdTU,3131
90
90
  trilogy/hooks/__init__.py,sha256=T3SF3phuUDPLXKGRVE_Lf9mzuwoXWyaLolncR_1kY30,144
91
91
  trilogy/hooks/base_hook.py,sha256=I_l-NBMNC7hKTDx1JgHZPVOOCvLQ36m2oIGaR5EUMXY,1180
92
- trilogy/hooks/graph_hook.py,sha256=4jeC3ot4u_FyqiYiHr3Eit2ktD0zPk7_pcG8JgFoqMc,4844
92
+ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4756
93
93
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
94
94
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
- trilogy/parsing/common.py,sha256=yuKN3fQEtftRMZlJb0ESUX4TLOVFcAE0vw2CfImYG1A,29980
96
+ trilogy/parsing/common.py,sha256=_-KWPP3NvNAHiB4B6jyGSnYKqlLs2XqXeTZiX4MCtWU,30861
97
97
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
98
98
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
99
99
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -104,14 +104,14 @@ trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
105
105
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
106
  trilogy/std/date.preql,sha256=HWZm4t4HWyxr5geWRsY05RnHBVDMci8z8YA2cu0-OOw,188
107
- trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
107
+ trilogy/std/display.preql,sha256=nm7lox87Xf6lBvXCVCS6x2HskguMKzndEBucJ5pktzk,175
108
108
  trilogy/std/geography.preql,sha256=qLnHmDU5EnvjTbfqZF-NEclSYM5_e9rZra7QjV01rZ4,582
109
109
  trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
110
110
  trilogy/std/net.preql,sha256=-bMV6dyofskl4Kvows-iQ4JCxjVUwsZOeWCy8JO5Ftw,135
111
111
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
112
112
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
113
- pytrilogy-0.0.3.64.dist-info/METADATA,sha256=-m9RjEszSqiTRWWSld1SMzy7JKn6e9SPxLUUrjsgqs8,9095
114
- pytrilogy-0.0.3.64.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- pytrilogy-0.0.3.64.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
- pytrilogy-0.0.3.64.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
- pytrilogy-0.0.3.64.dist-info/RECORD,,
113
+ pytrilogy-0.0.3.66.dist-info/METADATA,sha256=cQJUWN4bTjEYsCxAcAOKmkzEkkmpxB7mBmMQPx0O4Cw,9095
114
+ pytrilogy-0.0.3.66.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
+ pytrilogy-0.0.3.66.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
+ pytrilogy-0.0.3.66.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
+ pytrilogy-0.0.3.66.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.64"
7
+ __version__ = "0.0.3.66"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -1,6 +1,50 @@
1
1
  import networkx as nx
2
2
 
3
- from trilogy.core.models.build import BuildConcept, BuildDatasource
3
+ from trilogy.core.models.build import BuildConcept, BuildDatasource, BuildWhereClause
4
+
5
+
6
+ def get_graph_exact_match(
7
+ g: nx.DiGraph, conditions: BuildWhereClause | None
8
+ ) -> set[str]:
9
+ datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
10
+ nx.get_node_attributes(g, "datasource")
11
+ )
12
+ exact: set[str] = set()
13
+ for node in g.nodes:
14
+ if node in datasources:
15
+ ds = datasources[node]
16
+ if isinstance(ds, list):
17
+ exact.add(node)
18
+ continue
19
+
20
+ if not conditions and not ds.non_partial_for:
21
+ exact.add(node)
22
+ continue
23
+ elif conditions:
24
+ if not ds.non_partial_for:
25
+ continue
26
+ if ds.non_partial_for and conditions == ds.non_partial_for:
27
+ exact.add(node)
28
+ continue
29
+ else:
30
+ continue
31
+
32
+ return exact
33
+
34
+
35
+ def prune_sources_for_conditions(
36
+ g: nx.DiGraph,
37
+ conditions: BuildWhereClause | None,
38
+ ):
39
+
40
+ complete = get_graph_exact_match(g, conditions)
41
+ to_remove = []
42
+ for node in g.nodes:
43
+ if node.startswith("ds~") and node not in complete:
44
+ to_remove.append(node)
45
+
46
+ for node in to_remove:
47
+ g.remove_node(node)
4
48
 
5
49
 
6
50
  def concept_to_node(input: BuildConcept) -> str:
@@ -271,6 +271,20 @@ class Conditional(Mergeable, ConceptArgs, Namespaced, DataTyped, BaseModel):
271
271
  right: Expr
272
272
  operator: BooleanOperator
273
273
 
274
+ @field_validator("left", mode="before")
275
+ @classmethod
276
+ def left_validator(cls, v, info: ValidationInfo):
277
+ if isinstance(v, Concept):
278
+ return v.reference
279
+ return v
280
+
281
+ @field_validator("right", mode="before")
282
+ @classmethod
283
+ def right_validator(cls, v, info: ValidationInfo):
284
+ if isinstance(v, Concept):
285
+ return v.reference
286
+ return v
287
+
274
288
  def __add__(self, other) -> "Conditional":
275
289
  if other is None:
276
290
  return self
@@ -346,7 +360,6 @@ class Conditional(Mergeable, ConceptArgs, Namespaced, DataTyped, BaseModel):
346
360
 
347
361
  @property
348
362
  def concept_arguments(self) -> Sequence[ConceptRef]:
349
- """Return concepts directly referenced in where clause"""
350
363
  output = []
351
364
  output += get_concept_arguments(self.left)
352
365
  output += get_concept_arguments(self.right)
@@ -575,11 +588,11 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
575
588
  date,
576
589
  Function,
577
590
  ConceptRef,
578
- "Conditional",
591
+ Conditional,
579
592
  DataType,
580
- "Comparison",
593
+ Comparison,
581
594
  FunctionCallWrapper,
582
- "Parenthetical",
595
+ Parenthetical,
583
596
  MagicConstants,
584
597
  WindowItem,
585
598
  AggregateWrapper,
@@ -1631,7 +1631,10 @@ class Factory:
1631
1631
 
1632
1632
  @build.register
1633
1633
  def _(self, base: CaseElse) -> BuildCaseElse:
1634
- return BuildCaseElse.model_construct(expr=self.build(base.expr))
1634
+ expr: Concept | FuncArgs = base.expr
1635
+ if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
1636
+ expr, _ = self.instantiate_concept(expr)
1637
+ return BuildCaseElse.model_construct(expr=self.build(expr))
1635
1638
 
1636
1639
  @build.register
1637
1640
  def _(self, base: Concept) -> BuildConcept:
@@ -603,7 +603,7 @@ class Environment(BaseModel):
603
603
  # too hacky for maintainability
604
604
  if current_derivation not in (Derivation.ROOT, Derivation.CONSTANT):
605
605
  logger.info(
606
- f"A datasource has been added which will persist derived concept {new_persisted_concept.address}"
606
+ f"A datasource has been added which will persist derived concept {new_persisted_concept.address} with derivation {current_derivation}"
607
607
  )
608
608
  persisted = f"{PERSISTED_CONCEPT_PREFIX}_" + new_persisted_concept.name
609
609
  # override the current concept source to reflect that it's now coming from a datasource
@@ -622,17 +622,21 @@ class Environment(BaseModel):
622
622
  meta=meta,
623
623
  force=True,
624
624
  )
625
+ base = {
626
+ "lineage": None,
627
+ "metadata": new_persisted_concept.metadata.model_copy(
628
+ update={"concept_source": ConceptSource.PERSIST_STATEMENT}
629
+ ),
630
+ "derivation": Derivation.ROOT,
631
+ "purpose": new_persisted_concept.purpose,
632
+ }
633
+ # purpose is used in derivation calculation
634
+ # which should be fixed, but we'll do in a followup
635
+ # so override here
636
+ if new_persisted_concept.purpose == Purpose.CONSTANT:
637
+ base["purpose"] = Purpose.KEY
625
638
  new_persisted_concept = new_persisted_concept.model_copy(
626
- deep=True,
627
- update={
628
- "lineage": None,
629
- "metadata": new_persisted_concept.metadata.model_copy(
630
- update={
631
- "concept_source": ConceptSource.PERSIST_STATEMENT
632
- }
633
- ),
634
- "derivation": Derivation.ROOT,
635
- },
639
+ deep=True, update=base
636
640
  )
637
641
  self.add_concept(
638
642
  new_persisted_concept,
@@ -56,6 +56,12 @@ LOGGER_PREFIX = "[MODELS_EXECUTE]"
56
56
  DATASOURCE_TYPES = (BuildDatasource, BuildDatasource)
57
57
 
58
58
 
59
+ class InlinedCTE(BaseModel):
60
+ original_alias: str
61
+ new_alias: str
62
+ new_base: str
63
+
64
+
59
65
  class CTE(BaseModel):
60
66
  name: str
61
67
  source: "QueryDatasource"
@@ -78,6 +84,7 @@ class CTE(BaseModel):
78
84
  limit: Optional[int] = None
79
85
  base_name_override: Optional[str] = None
80
86
  base_alias_override: Optional[str] = None
87
+ inlined_ctes: dict[str, InlinedCTE] = Field(default_factory=dict)
81
88
 
82
89
  @field_validator("join_derived_concepts")
83
90
  def validate_join_derived_concepts(cls, v):
@@ -104,62 +111,6 @@ class CTE(BaseModel):
104
111
  def validate_output_columns(cls, v):
105
112
  return unique(v, "address")
106
113
 
107
- def inline_constant(self, concept: BuildConcept):
108
- if not concept.derivation == Derivation.CONSTANT:
109
- return False
110
- if not isinstance(concept.lineage, BuildFunction):
111
- return False
112
- if not concept.lineage.operator == FunctionType.CONSTANT:
113
- return False
114
- # remove the constant
115
- removed: set = set()
116
- if concept.address in self.source_map:
117
- removed = removed.union(self.source_map[concept.address])
118
- del self.source_map[concept.address]
119
-
120
- if self.condition:
121
- self.condition = self.condition.inline_constant(concept)
122
- # if we've entirely removed the need to join to someplace to get the concept
123
- # drop the join as well.
124
- for removed_cte in removed:
125
- still_required = any(
126
- [
127
- removed_cte in x
128
- for x in self.source_map.values()
129
- or self.existence_source_map.values()
130
- ]
131
- )
132
- if not still_required:
133
- self.joins = [
134
- join
135
- for join in self.joins
136
- if not isinstance(join, Join)
137
- or (
138
- isinstance(join, Join)
139
- and (
140
- join.right_cte.name != removed_cte
141
- and any(
142
- [
143
- x.cte.name != removed_cte
144
- for x in (join.joinkey_pairs or [])
145
- ]
146
- )
147
- )
148
- )
149
- ]
150
- for join in self.joins:
151
- if isinstance(join, UnnestJoin) and concept in join.concepts:
152
- join.rendering_required = False
153
-
154
- self.parent_ctes = [
155
- x for x in self.parent_ctes if x.name != removed_cte
156
- ]
157
- if removed_cte == self.base_name_override:
158
- candidates = [x.name for x in self.parent_ctes]
159
- self.base_name_override = candidates[0] if candidates else None
160
- self.base_alias_override = candidates[0] if candidates else None
161
- return True
162
-
163
114
  @property
164
115
  def comment(self) -> str:
165
116
  base = f"Target: {str(self.grain)}. Group: {self.group_to_grain}"
@@ -240,7 +191,18 @@ class CTE(BaseModel):
240
191
  ]
241
192
  elif v == parent.safe_identifier:
242
193
  self.source_map[k] = [ds_being_inlined.safe_identifier]
243
-
194
+ for k, v in self.existence_source_map.items():
195
+ if isinstance(v, list):
196
+ self.existence_source_map[k] = [
197
+ (
198
+ ds_being_inlined.safe_identifier
199
+ if x == parent.safe_identifier
200
+ else x
201
+ )
202
+ for x in v
203
+ ]
204
+ elif v == parent.safe_identifier:
205
+ self.existence_source_map[k] = [ds_being_inlined.safe_identifier]
244
206
  # zip in any required values for lookups
245
207
  for k in ds_being_inlined.output_lcl.addresses:
246
208
  if k in self.source_map and self.source_map[k]:
@@ -251,6 +213,11 @@ class CTE(BaseModel):
251
213
  ]
252
214
  if force_group:
253
215
  self.group_to_grain = True
216
+ self.inlined_ctes[ds_being_inlined.safe_identifier] = InlinedCTE(
217
+ original_alias=parent.name,
218
+ new_alias=ds_being_inlined.safe_identifier,
219
+ new_base=ds_being_inlined.safe_location,
220
+ )
254
221
  return True
255
222
 
256
223
  def __add__(self, other: "CTE" | "UnionCTE"):
@@ -303,6 +270,10 @@ class CTE(BaseModel):
303
270
  **self.existence_source_map,
304
271
  **other.existence_source_map,
305
272
  }
273
+ self.inlined_ctes = {
274
+ **self.inlined_ctes,
275
+ **other.inlined_ctes,
276
+ }
306
277
 
307
278
  return self
308
279
 
@@ -672,7 +643,7 @@ class QueryDatasource(BaseModel):
672
643
  and CONFIG.validate_missing
673
644
  ):
674
645
  raise SyntaxError(
675
- f"On query datasource from {values} missing source map entry (map: {v}) for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
646
+ f"Missing source map entry for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have map: {v}"
676
647
  )
677
648
  return v
678
649
 
@@ -1057,6 +1028,7 @@ class UnionCTE(BaseModel):
1057
1028
  hidden_concepts: set[str] = Field(default_factory=set)
1058
1029
  partial_concepts: list[BuildConcept] = Field(default_factory=list)
1059
1030
  existence_source_map: Dict[str, list[str]] = Field(default_factory=dict)
1031
+ inlined_ctes: Dict[str, InlinedCTE] = Field(default_factory=dict)
1060
1032
 
1061
1033
  @computed_field # type: ignore
1062
1034
  @property
@@ -6,7 +6,7 @@ from networkx.algorithms import approximation as ax
6
6
  from trilogy.constants import logger
7
7
  from trilogy.core.enums import Derivation
8
8
  from trilogy.core.exceptions import AmbiguousRelationshipResolutionException
9
- from trilogy.core.graph_models import concept_to_node
9
+ from trilogy.core.graph_models import concept_to_node, prune_sources_for_conditions
10
10
  from trilogy.core.models.build import BuildConcept, BuildConditional, BuildWhereClause
11
11
  from trilogy.core.models.build_environment import BuildEnvironment
12
12
  from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
@@ -222,10 +222,12 @@ def resolve_weak_components(
222
222
  environment_graph: nx.DiGraph,
223
223
  filter_downstream: bool = True,
224
224
  accept_partial: bool = False,
225
+ search_conditions: BuildWhereClause | None = None,
225
226
  ) -> list[list[BuildConcept]] | None:
226
227
  break_flag = False
227
228
  found = []
228
229
  search_graph = environment_graph.copy()
230
+ prune_sources_for_conditions(search_graph, conditions=search_conditions)
229
231
  reduced_concept_sets: list[set[str]] = []
230
232
 
231
233
  # loop through, removing new nodes we find
@@ -406,6 +408,7 @@ def gen_merge_node(
406
408
  g,
407
409
  filter_downstream=filter_downstream,
408
410
  accept_partial=accept_partial,
411
+ search_conditions=search_conditions,
409
412
  )
410
413
  if not weak_resolve:
411
414
  logger.info(
@@ -169,7 +169,9 @@ def is_fully_covered(
169
169
  return current_end >= end
170
170
 
171
171
 
172
- def get_union_sources(datasources: list[BuildDatasource], concepts: list[BuildConcept]):
172
+ def get_union_sources(
173
+ datasources: list[BuildDatasource], concepts: list[BuildConcept]
174
+ ) -> List[list[BuildDatasource]]:
173
175
  candidates: list[BuildDatasource] = []
174
176
  for x in datasources:
175
177
  if all([c.address in x.output_concepts for c in concepts]):
@@ -5,7 +5,11 @@ import networkx as nx
5
5
 
6
6
  from trilogy.constants import logger
7
7
  from trilogy.core.enums import Derivation
8
- from trilogy.core.graph_models import concept_to_node
8
+ from trilogy.core.graph_models import (
9
+ concept_to_node,
10
+ get_graph_exact_match,
11
+ prune_sources_for_conditions,
12
+ )
9
13
  from trilogy.core.models.build import (
10
14
  BuildConcept,
11
15
  BuildDatasource,
@@ -57,28 +61,6 @@ def get_graph_partial_nodes(
57
61
  return partial
58
62
 
59
63
 
60
- def get_graph_exact_match(
61
- g: nx.DiGraph, conditions: BuildWhereClause | None
62
- ) -> set[str]:
63
- datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
64
- nx.get_node_attributes(g, "datasource")
65
- )
66
- exact: set[str] = set()
67
- for node in g.nodes:
68
- if node in datasources:
69
- ds = datasources[node]
70
- if not isinstance(ds, list):
71
- if not ds.non_partial_for:
72
- continue
73
- if ds.non_partial_for and conditions == ds.non_partial_for:
74
- exact.add(node)
75
- continue
76
- else:
77
- continue
78
-
79
- return exact
80
-
81
-
82
64
  def get_graph_grains(g: nx.DiGraph) -> dict[str, list[str]]:
83
65
  datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
84
66
  nx.get_node_attributes(g, "datasource")
@@ -98,28 +80,31 @@ def get_graph_grains(g: nx.DiGraph) -> dict[str, list[str]]:
98
80
 
99
81
 
100
82
  def subgraph_is_complete(
101
- nodes: list[str], targets: set[str], mapping: dict[str, str]
83
+ nodes: list[str], targets: set[str], mapping: dict[str, str], g: nx.DiGraph
102
84
  ) -> bool:
103
85
  mapped = set([mapping.get(n, n) for n in nodes])
104
- return all([t in mapped for t in targets])
105
-
106
-
107
- def prune_sources_for_conditions(
108
- g: nx.DiGraph,
109
- depth: int,
110
- conditions: BuildWhereClause | None,
111
- ):
112
-
113
- complete = get_graph_exact_match(g, conditions)
114
- to_remove = []
115
- for node in g.nodes:
116
- if node.startswith("ds~") and node not in complete:
117
- to_remove.append(node)
118
- logger.debug(
119
- f"{padding(depth)}{LOGGER_PREFIX} removing datasource {node} as it is not a match for conditions {conditions}"
120
- )
121
- for node in to_remove:
122
- g.remove_node(node)
86
+ passed = all([t in mapped for t in targets])
87
+ if not passed:
88
+ logger.info(
89
+ f"Subgraph {nodes} is not complete, missing targets {targets} - mapped {mapped}"
90
+ )
91
+ return False
92
+ # check if all concepts have a datasource edge
93
+ has_ds_edge = {
94
+ mapping.get(n, n): any(x.startswith("ds~") for x in nx.neighbors(g, n))
95
+ for n in nodes
96
+ if n.startswith("c~")
97
+ }
98
+ has_ds_edge = {k: False for k in targets}
99
+ # check at least one instance of concept has a datasource edge
100
+ for n in nodes:
101
+ if n.startswith("c~"):
102
+ neighbors = nx.neighbors(g, n)
103
+ for neighbor in neighbors:
104
+ if neighbor.startswith("ds~"):
105
+ has_ds_edge[mapping.get(n, n)] = True
106
+ break
107
+ return all(has_ds_edge.values()) and passed
123
108
 
124
109
 
125
110
  def create_pruned_concept_graph(
@@ -133,8 +118,6 @@ def create_pruned_concept_graph(
133
118
  orig_g = g
134
119
 
135
120
  g = g.copy()
136
- if conditions:
137
- prune_sources_for_conditions(g, depth, conditions)
138
121
  union_options = get_union_sources(datasources, all_concepts)
139
122
  for ds_list in union_options:
140
123
  node_address = "ds~" + "-".join([x.name for x in ds_list])
@@ -144,7 +127,8 @@ def create_pruned_concept_graph(
144
127
  g.add_node(node_address, datasource=ds_list)
145
128
  for c in common:
146
129
  g.add_edge(node_address, concept_to_node(c))
147
-
130
+ g.add_edge(concept_to_node(c), node_address)
131
+ prune_sources_for_conditions(g, conditions)
148
132
  target_addresses = set([c.address for c in all_concepts])
149
133
  concepts: dict[str, BuildConcept] = nx.get_node_attributes(orig_g, "concept")
150
134
  datasource_map: dict[str, BuildDatasource | list[BuildDatasource]] = (
@@ -156,8 +140,7 @@ def create_pruned_concept_graph(
156
140
  # filter out synonyms
157
141
  if (x := concepts.get(n, None)) and x.address in target_addresses
158
142
  }
159
- # from trilogy.hooks.graph_hook import GraphHook
160
- # GraphHook().query_graph_built(g)
143
+
161
144
  relevant_concepts: list[str] = list(relevant_concepts_pre.keys())
162
145
  relevent_datasets: list[str] = []
163
146
  if not accept_partial:
@@ -179,6 +162,7 @@ def create_pruned_concept_graph(
179
162
  to_remove.append(edge)
180
163
  for edge in to_remove:
181
164
  g.remove_edge(*edge)
165
+
182
166
  for n in g.nodes():
183
167
  if not n.startswith("ds~"):
184
168
  continue
@@ -211,13 +195,13 @@ def create_pruned_concept_graph(
211
195
  if n not in relevent_datasets and n not in relevant_concepts
212
196
  ]
213
197
  )
214
-
198
+ # from trilogy.hooks.graph_hook import GraphHook
199
+ # GraphHook().query_graph_built(g)
215
200
  subgraphs = list(nx.connected_components(g.to_undirected()))
216
-
217
201
  subgraphs = [
218
202
  s
219
203
  for s in subgraphs
220
- if subgraph_is_complete(s, target_addresses, relevant_concepts_pre)
204
+ if subgraph_is_complete(s, target_addresses, relevant_concepts_pre, g)
221
205
  ]
222
206
 
223
207
  if not subgraphs:
@@ -524,8 +508,12 @@ def gen_select_merge_node(
524
508
  constants = [c for c in all_concepts if c.derivation == Derivation.CONSTANT]
525
509
  if not non_constant and constants:
526
510
  logger.info(
527
- f"{padding(depth)}{LOGGER_PREFIX} only constant inputs to discovery, returning constant node directly"
511
+ f"{padding(depth)}{LOGGER_PREFIX} only constant inputs to discovery ({constants}), returning constant node directly"
528
512
  )
513
+ for x in constants:
514
+ logger.info(
515
+ f"{padding(depth)}{LOGGER_PREFIX} {x} {x.lineage} {x.derivation}"
516
+ )
529
517
  if conditions:
530
518
  if not all(
531
519
  [x.derivation == Derivation.CONSTANT for x in conditions.row_arguments]
@@ -395,6 +395,9 @@ def get_query_node(
395
395
  if not statement.output_components:
396
396
  raise ValueError(f"Statement has no output components {statement}")
397
397
  history = history or History(base_environment=environment)
398
+ print(
399
+ f"{LOGGER_PREFIX} building query node for {statement.output_components} grain {statement.grain}"
400
+ )
398
401
  build_statement: BuildSelectLineage | BuildMultiSelectLineage = Factory(
399
402
  environment=environment,
400
403
  ).build(statement)
trilogy/dialect/base.py CHANGED
@@ -580,7 +580,12 @@ class BaseDialect:
580
580
  target = INVALID_REFERENCE_STRING(
581
581
  f"Missing source CTE for {e.right.address}"
582
582
  )
583
+ assert cte, "CTE must be provided for inlined CTEs"
584
+ if target in cte.inlined_ctes:
585
+ info = cte.inlined_ctes[target]
586
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {info.new_base} as {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
583
587
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
588
+
584
589
  elif isinstance(
585
590
  e.right,
586
591
  (ListWrapper, TupleWrapper, BuildParenthetical, list),
@@ -85,7 +85,6 @@ class GraphHook(BaseHook):
85
85
  plt.show()
86
86
 
87
87
  def _draw_labels_with_manual_spacing(self, graph, pos):
88
- """Fallback method for manual label spacing when adjustText is not available"""
89
88
  import numpy as np
90
89
 
91
90
  pos_labels = {}
trilogy/parsing/common.py CHANGED
@@ -23,9 +23,13 @@ from trilogy.core.models.author import (
23
23
  AggregateWrapper,
24
24
  AlignClause,
25
25
  AlignItem,
26
+ CaseElse,
27
+ CaseWhen,
28
+ Comparison,
26
29
  Concept,
27
30
  ConceptArgs,
28
31
  ConceptRef,
32
+ Conditional,
29
33
  FilterItem,
30
34
  Function,
31
35
  FunctionCallWrapper,
@@ -38,6 +42,7 @@ from trilogy.core.models.author import (
38
42
  Parenthetical,
39
43
  RowsetItem,
40
44
  RowsetLineage,
45
+ SubselectComparison,
41
46
  TraitDataType,
42
47
  UndefinedConcept,
43
48
  WhereClause,
@@ -198,6 +203,7 @@ def atom_is_relevant(
198
203
  others: list[Concept | ConceptRef],
199
204
  environment: Environment | None = None,
200
205
  ):
206
+
201
207
  if isinstance(atom, (ConceptRef, Concept)):
202
208
  # when we are looking at atoms, if there is a concept that is in others
203
209
  # return directly
@@ -210,9 +216,10 @@ def atom_is_relevant(
210
216
  elif isinstance(atom, AggregateWrapper):
211
217
  return any(atom_is_relevant(x, others, environment) for x in atom.by)
212
218
 
213
- if isinstance(atom, Function):
219
+ elif isinstance(atom, Function):
214
220
  relevant = False
215
221
  for arg in atom.arguments:
222
+
216
223
  relevant = relevant or atom_is_relevant(arg, others, environment)
217
224
  return relevant
218
225
  elif isinstance(atom, FunctionCallWrapper):
@@ -220,8 +227,27 @@ def atom_is_relevant(
220
227
  [atom_is_relevant(atom.content, others, environment)]
221
228
  + [atom_is_relevant(x, others, environment) for x in atom.args]
222
229
  )
230
+ elif isinstance(atom, CaseWhen):
231
+ rval = atom_is_relevant(atom.expr, others, environment) or atom_is_relevant(
232
+ atom.comparison, others, environment
233
+ )
234
+ return rval
235
+ elif isinstance(atom, CaseElse):
236
+
237
+ rval = atom_is_relevant(atom.expr, others, environment)
238
+ return rval
239
+ elif isinstance(atom, SubselectComparison):
240
+ return atom_is_relevant(atom.left, others, environment)
241
+ elif isinstance(atom, Comparison):
242
+ return atom_is_relevant(atom.left, others, environment) or atom_is_relevant(
243
+ atom.right, others, environment
244
+ )
245
+ elif isinstance(atom, Conditional):
246
+ return atom_is_relevant(atom.left, others, environment) or atom_is_relevant(
247
+ atom.right, others, environment
248
+ )
223
249
  elif isinstance(atom, ConceptArgs):
224
- # use atom is relevant here to trigger the early exit behavior for concpets in set
250
+ # use atom is relevant here to trigger the early exit behavior for concepts in set
225
251
  return any(
226
252
  [atom_is_relevant(x, others, environment) for x in atom.concept_arguments]
227
253
  )
@@ -233,7 +259,6 @@ def concept_is_relevant(
233
259
  others: list[Concept | ConceptRef],
234
260
  environment: Environment | None = None,
235
261
  ) -> bool:
236
-
237
262
  if isinstance(concept, UndefinedConcept):
238
263
  return False
239
264
  if concept.datatype == DataType.UNKNOWN:
trilogy/std/display.preql CHANGED
@@ -1,3 +1,6 @@
1
1
 
2
2
 
3
- type percent float; # Percentage value
3
+ type percent float; # Percentage value
4
+
5
+ def calc_percent(a, b, digits=-1) -> case when digits =-1 then (a/b):: float::percent
6
+ else round((a/b):: float::percent, digits) end;