pytrilogy 0.0.2.15__py3-none-any.whl → 0.0.2.18__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.

Files changed (44) hide show
  1. {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/METADATA +12 -8
  2. pytrilogy-0.0.2.18.dist-info/RECORD +83 -0
  3. trilogy/__init__.py +1 -1
  4. trilogy/constants.py +1 -1
  5. trilogy/core/enums.py +1 -0
  6. trilogy/core/functions.py +11 -0
  7. trilogy/core/models.py +105 -59
  8. trilogy/core/optimization.py +15 -9
  9. trilogy/core/processing/concept_strategies_v3.py +372 -145
  10. trilogy/core/processing/node_generators/basic_node.py +27 -55
  11. trilogy/core/processing/node_generators/common.py +6 -7
  12. trilogy/core/processing/node_generators/filter_node.py +28 -31
  13. trilogy/core/processing/node_generators/group_node.py +14 -2
  14. trilogy/core/processing/node_generators/group_to_node.py +3 -1
  15. trilogy/core/processing/node_generators/multiselect_node.py +3 -0
  16. trilogy/core/processing/node_generators/node_merge_node.py +14 -9
  17. trilogy/core/processing/node_generators/rowset_node.py +12 -12
  18. trilogy/core/processing/node_generators/select_merge_node.py +302 -0
  19. trilogy/core/processing/node_generators/select_node.py +7 -511
  20. trilogy/core/processing/node_generators/unnest_node.py +4 -3
  21. trilogy/core/processing/node_generators/window_node.py +12 -37
  22. trilogy/core/processing/nodes/__init__.py +0 -2
  23. trilogy/core/processing/nodes/base_node.py +69 -20
  24. trilogy/core/processing/nodes/filter_node.py +3 -0
  25. trilogy/core/processing/nodes/group_node.py +18 -17
  26. trilogy/core/processing/nodes/merge_node.py +4 -10
  27. trilogy/core/processing/nodes/select_node_v2.py +28 -14
  28. trilogy/core/processing/nodes/window_node.py +1 -2
  29. trilogy/core/processing/utility.py +51 -3
  30. trilogy/core/query_processor.py +17 -73
  31. trilogy/dialect/base.py +8 -3
  32. trilogy/dialect/common.py +65 -10
  33. trilogy/dialect/duckdb.py +4 -1
  34. trilogy/dialect/sql_server.py +3 -3
  35. trilogy/executor.py +5 -0
  36. trilogy/hooks/query_debugger.py +5 -3
  37. trilogy/parsing/parse_engine.py +67 -39
  38. trilogy/parsing/render.py +2 -0
  39. trilogy/parsing/trilogy.lark +6 -3
  40. pytrilogy-0.0.2.15.dist-info/RECORD +0 -82
  41. {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/LICENSE.md +0 -0
  42. {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/WHEEL +0 -0
  43. {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/entry_points.txt +0 -0
  44. {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.15
3
+ Version: 0.0.2.18
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -42,15 +42,15 @@ Installation: `pip install pytrilogy`
42
42
 
43
43
  `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
44
44
 
45
- You can read more about the project [here](https://trilogydata.dev/) and try out an interactive demo on the page an interactive demo [here](https://trilogydata.dev/demo).
45
+ You can read more about the project [here](https://trilogydata.dev/) and try out an interactive demo [here](https://trilogydata.dev/demo/).
46
46
 
47
47
  Trilogy:
48
48
  ```sql
49
+ WHERE
50
+ name like '%lvis%'
49
51
  SELECT
50
52
  name,
51
53
  count(name) as name_count
52
- WHERE
53
- name='Elvis'
54
54
  ORDER BY
55
55
  name_count desc
56
56
  LIMIT 10;
@@ -145,7 +145,7 @@ Run the following from the directory the file is in.
145
145
  trilogy run hello.trilogy duckdb
146
146
  ```
147
147
 
148
- ![UI Preview](./docs/hello_world.png)
148
+ ![UI Preview](./hello_world.png)
149
149
 
150
150
  ## Backends
151
151
 
@@ -214,7 +214,7 @@ for row in results:
214
214
 
215
215
  ## Basic Example - CLI
216
216
 
217
- Trilogy can be run through a CLI tool, appropriately named 'trilogy'.
217
+ Trilogy can be run through a CLI tool, also named 'trilogy'.
218
218
 
219
219
  After installing trilogy, you can run the trilogy CLI with two required positional arguments; the first the path to a file or a direct command,
220
220
  and second the dialect to run.
@@ -252,7 +252,7 @@ N/A, only supports default auth. In python you can pass in a custom client.
252
252
 
253
253
  ## More Examples
254
254
 
255
- [Interactive demo](https://trilogydata.dev/demo).
255
+ [Interactive demo](https://trilogydata.dev/demo/).
256
256
 
257
257
  Additional examples can be found in the [public model repository](https://github.com/trilogydata/trilogy-public-models).
258
258
 
@@ -267,11 +267,15 @@ Clone repository and install requirements.txt and requirements-test.txt.
267
267
  Please open an issue first to discuss what you would like to change, and then create a PR against that issue.
268
268
 
269
269
  ## Similar in space
270
+ Trilogy combines two aspects; a semantic layer and a query language. We've covered examples of both below:
271
+
272
+ Python "semantic layers" are tools for defining data access to a warehouse in a more abstract way.
273
+
274
+ - [metricsflow](https://github.com/dbt-labs/metricflow)
270
275
 
271
276
  "Better SQL" has been a popular space. We believe Trilogy takes a different approach then the following,
272
277
  but all are worth checking out. Please open PRs/comment for anything missed!
273
278
 
274
-
275
279
  - [malloy](https://github.com/malloydata/malloy)
276
280
  - [preql](https://github.com/erezsh/Preql)
277
281
  - [PREQL](https://github.com/PRQL/prql)
@@ -0,0 +1,83 @@
1
+ trilogy/__init__.py,sha256=NVclSieaZqXKRfCCzUhXoqSrNUdoiMn3ytKw0jCWj7A,291
2
+ trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ trilogy/constants.py,sha256=pZkOneh_65f9Ua6NICu1bHAFAbmQxmiXRXS7tsmCWbQ,1235
4
+ trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
+ trilogy/executor.py,sha256=An6YLpHQOt96E7ozRQhwZels2hMsDbh0WV767kKCGU0,11294
6
+ trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
7
+ trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
+ trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
11
+ trilogy/core/enums.py,sha256=A9VC0lbP5eo9sndm2TzA-nNJRRmvbjE918ZiEXtcQ_c,6043
12
+ trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
13
+ trilogy/core/environment_helpers.py,sha256=1miP4is4FEoci01KSAy2VZVYmlmT5TOCOALBekd2muQ,7211
14
+ trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
15
+ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
+ trilogy/core/functions.py,sha256=ShFTStIKbgI-3EZIU0xTumI78AC5QlvARwnBM53P2O0,10677
17
+ trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
18
+ trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
+ trilogy/core/models.py,sha256=nSCOXedYRFgYdZa45dputERCvgPRQpRvCyiGglyeM0g,150985
20
+ trilogy/core/optimization.py,sha256=od_60A9F8J8Nj24MHgrxl4vwRwmBFH13TMdoMQvgVKs,7717
21
+ trilogy/core/query_processor.py,sha256=kXuBsIaRHu1s7zB_rAnT_gRe4-VgRSrPE1TnVJXFLtc,16447
22
+ trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
23
+ trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
24
+ trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
25
+ trilogy/core/optimizations/inline_datasource.py,sha256=AATzQ6YrtW_1-aQFjQyTYqEYKBoMFhek7ADfBr4uUdQ,3634
26
+ trilogy/core/optimizations/predicate_pushdown.py,sha256=1l9WnFOSv79e341typG3tTdk0XGl1J_ToQih3LYoGIY,8435
27
+ trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ trilogy/core/processing/concept_strategies_v3.py,sha256=DO9gybVLku8GEkO3uNPaCeqhalnufsjYYbvDs-gkwNc,35295
29
+ trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
+ trilogy/core/processing/utility.py,sha256=plpaAmssWjcxPee8_J4hleazTzQIBL6mBLJ33FKfwOM,19421
31
+ trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=WQNgJ1MwrMS_BQ-b3XwGGB6eToDykelAVj_fesJuqe0,2069
33
+ trilogy/core/processing/node_generators/common.py,sha256=LwDgPlhWeuw0t07f3kX9IE5LXBdZhXfh-aY0XGk50ak,8946
34
+ trilogy/core/processing/node_generators/filter_node.py,sha256=Vz9Rb67e1dfZgnliekwwLeDPVkthMbdrnrKRdz7J1ik,7654
35
+ trilogy/core/processing/node_generators/group_node.py,sha256=Dn9vEY-WPFHNN-LtXfgWiHIXspzHDKfkKL5a2KE2gD0,4252
36
+ trilogy/core/processing/node_generators/group_to_node.py,sha256=R9i_wHipxjXJyfYEwfeTw2EPpuanXVA327XyfcP2tBg,2537
37
+ trilogy/core/processing/node_generators/multiselect_node.py,sha256=_KO9lqzHQoy4VAviO0ttQlmK0tjaqrJj4SJPhmoIYm8,6229
38
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=yRDfY8muZN7G2vsdYXF2X1iqbQ2zDUNGlxvSIyKVoWU,13512
39
+ trilogy/core/processing/node_generators/rowset_node.py,sha256=gU_ybfYXO9tZqHjUSABIioVpb8AWtITpegj3IGSf2GI,4587
40
+ trilogy/core/processing/node_generators/select_merge_node.py,sha256=ipSxw1Oqk-hVVGhPhZlvRbptC0Vpwh52hZ7z8oOj2yk,10065
41
+ trilogy/core/processing/node_generators/select_node.py,sha256=vUg3gXHGvagdbniIAE7DdqJcQ0V1VAfHtTrw3edYPso,1734
42
+ trilogy/core/processing/node_generators/unnest_node.py,sha256=cZ26CN338CBnd6asML1OBUtNcDzmNlFpY0Vnade4yrc,2256
43
+ trilogy/core/processing/node_generators/window_node.py,sha256=jy3FF8uN0VA7yyrBeR40B9CAqR_5qBP4PiS6Gr-f-7w,2590
44
+ trilogy/core/processing/nodes/__init__.py,sha256=qS5EJDRwwIrCEfS7ibCA2ESE0RPzsAIii1UWd_wNsHA,4760
45
+ trilogy/core/processing/nodes/base_node.py,sha256=sc3HrXkWk-xpsAQ7B7ltX1ZejYAkqFiv8Ei8Jg5VGkQ,15579
46
+ trilogy/core/processing/nodes/filter_node.py,sha256=GfZ9eghpFDI-s7iQP2UqTljCmn25LT_T5TAxDlh7PkQ,2343
47
+ trilogy/core/processing/nodes/group_node.py,sha256=PrBHaGq_f8RmokUw9lXLGJ5YbjdP77P7Ag0pgR6e2cU,7293
48
+ trilogy/core/processing/nodes/merge_node.py,sha256=wlqh0suFfZBJOgkn7vy0OiDs5jza3NCX7eHTEEb6mBQ,14799
49
+ trilogy/core/processing/nodes/select_node_v2.py,sha256=gS9OQgS2TSEK59BQ9R0i83pTHfGJUxv7AkAmT21sYxI,8067
50
+ trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
51
+ trilogy/core/processing/nodes/window_node.py,sha256=ro0QfMFi4ZmIn5Q4D0M_vJWfnHH_C0MN7XkVkx8Gygg,1214
52
+ trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ trilogy/dialect/base.py,sha256=QDeKbc7vgqfbR649-87cwsOz8G3VY63V19zH6I-WITo,33103
54
+ trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
55
+ trilogy/dialect/common.py,sha256=Hr0mxcNxjSvhpBM5Wvb_Q7aklAuYj5aBDrW433py0Zs,4403
56
+ trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
57
+ trilogy/dialect/duckdb.py,sha256=_0a5HBU8zRNtZj7YED3ju4fHXRYG9jNeKwnlZwUDvwI,3419
58
+ trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
59
+ trilogy/dialect/postgres.py,sha256=ev1RJZsC8BB3vJSxJ4q-TTYqZ4Hk1NXUtuRkLrQEBX0,3254
60
+ trilogy/dialect/presto.py,sha256=2Rs53UfPxKU0rJTcEbiS-Lxm-CDiqUGojh7yRpQgyRE,3416
61
+ trilogy/dialect/snowflake.py,sha256=_Bf4XO7-nImMv9XCSsTfVM3g2f_KHdO17VTa9J-HgSM,2989
62
+ trilogy/dialect/sql_server.py,sha256=owUZbMFrooYIMj1DSLstPWxPO7K7WAUEWNvDKM-DMt0,3118
63
+ trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
+ trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
65
+ trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2467
66
+ trilogy/hooks/query_debugger.py,sha256=Pe-Kw1JGngeLqQOMQb0E3-24jXEavqnPCQ-KOfTfjP8,4357
67
+ trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
+ trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
+ trilogy/parsing/common.py,sha256=-4LM71ocidA8DI2RngqFEOmhzBrIt8VdBTO4x2BpD8E,9502
70
+ trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
71
+ trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
72
+ trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
+ trilogy/parsing/parse_engine.py,sha256=r0B7tXAIfTMJ42ZJwP70ulQBrOoM1AAiXFbCQ2rOpQs,64720
74
+ trilogy/parsing/render.py,sha256=8yxerPAi4AhlhPBlAfbYbOM3F9rz6HzpWVEWPtK2VEg,12321
75
+ trilogy/parsing/trilogy.lark,sha256=0JAvQBACFNL-X61I0tB_0QPZgsguZgerfHBv903oKh0,11623
76
+ trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
+ trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
78
+ pytrilogy-0.0.2.18.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.18.dist-info/METADATA,sha256=erqF5E59Qz6Xr2ZKiBKejkmQUFMGpDxK9NerqOoT6K0,8132
80
+ pytrilogy-0.0.2.18.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
81
+ pytrilogy-0.0.2.18.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.18.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.18.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.2.15"
7
+ __version__ = "0.0.2.18"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -24,7 +24,7 @@ class Optimizations:
24
24
  predicate_pushdown: bool = True
25
25
  datasource_inlining: bool = True
26
26
  constant_inlining: bool = True
27
- constant_inline_cutoff: int = 3
27
+ constant_inline_cutoff: int = 10
28
28
  direct_return: bool = True
29
29
 
30
30
 
trilogy/core/enums.py CHANGED
@@ -119,6 +119,7 @@ class FunctionType(Enum):
119
119
  CONSTANT = "constant"
120
120
  COALESCE = "coalesce"
121
121
  IS_NULL = "isnull"
122
+ BOOL = "bool"
122
123
 
123
124
  # COMPLEX
124
125
  INDEX_ACCESS = "index_access"
trilogy/core/functions.py CHANGED
@@ -340,6 +340,17 @@ def IsNull(args: list[Concept]) -> Function:
340
340
  )
341
341
 
342
342
 
343
+ def Bool(args: list[Concept]) -> Function:
344
+ return Function(
345
+ operator=FunctionType.BOOL,
346
+ arguments=args,
347
+ output_datatype=DataType.BOOL,
348
+ output_purpose=function_args_to_output_purpose(args),
349
+ arg_count=1,
350
+ # output_grain=Grain(components=arguments),
351
+ )
352
+
353
+
343
354
  def StrPos(args: list[Concept]) -> Function:
344
355
  return Function(
345
356
  operator=FunctionType.STRPOS,
trilogy/core/models.py CHANGED
@@ -432,6 +432,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
432
432
  grain: "Grain" = Field(default=None, validate_default=True)
433
433
  modifiers: Optional[List[Modifier]] = Field(default_factory=list)
434
434
  pseudonyms: Dict[str, Concept] = Field(default_factory=dict)
435
+ _address_cache: str | None = None
435
436
 
436
437
  def __hash__(self):
437
438
  return hash(str(self))
@@ -558,9 +559,19 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
558
559
  grain = ",".join([str(c.address) for c in self.grain.components])
559
560
  return f"{self.namespace}.{self.name}<{grain}>"
560
561
 
561
- @cached_property
562
+ @property
562
563
  def address(self) -> str:
563
- return f"{self.namespace}.{self.name}"
564
+ if not self._address_cache:
565
+ self._address_cache = f"{self.namespace}.{self.name}"
566
+ return self._address_cache
567
+
568
+ @address.setter
569
+ def address(self, address: str) -> None:
570
+ self._address_cache = address
571
+
572
+ def set_name(self, name: str):
573
+ self.name = name
574
+ self.address = f"{self.namespace}.{self.name}"
564
575
 
565
576
  @property
566
577
  def output(self) -> "Concept":
@@ -905,7 +916,13 @@ class Grain(Mergeable, BaseModel):
905
916
 
906
917
  @cached_property
907
918
  def set(self):
908
- return set([c.address for c in self.components_copy])
919
+ base = []
920
+ for x in self.components_copy:
921
+ if x.derivation == PurposeLineage.ROWSET:
922
+ base.append(x.lineage.content.address)
923
+ else:
924
+ base.append(x.address)
925
+ return set(base)
909
926
 
910
927
  def __eq__(self, other: object):
911
928
  if isinstance(other, list):
@@ -1555,6 +1572,10 @@ class OrderBy(Mergeable, Namespaced, BaseModel):
1555
1572
  items=[x.with_merge(source, target, modifiers) for x in self.items]
1556
1573
  )
1557
1574
 
1575
+ @property
1576
+ def concept_arguments(self):
1577
+ return [x.expr for x in self.items]
1578
+
1558
1579
 
1559
1580
  class RawSQLStatement(BaseModel):
1560
1581
  text: str
@@ -1567,6 +1588,13 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1567
1588
  limit: Optional[int] = None
1568
1589
  meta: Optional[Metadata] = Field(default_factory=lambda: Metadata())
1569
1590
 
1591
+ def refresh_bindings(self, environment: Environment):
1592
+ for item in self.selection:
1593
+ if isinstance(item.content, Concept):
1594
+ item.content = environment.concepts[item.content.address].with_grain(
1595
+ self.grain
1596
+ )
1597
+
1570
1598
  def __str__(self):
1571
1599
  from trilogy.parsing.render import render_query
1572
1600
 
@@ -1605,6 +1633,14 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1605
1633
  limit=self.limit,
1606
1634
  )
1607
1635
 
1636
+ @property
1637
+ def locally_derived(self) -> set[str]:
1638
+ locally_derived: set[str] = set()
1639
+ for item in self.selection:
1640
+ if isinstance(item.content, ConceptTransform):
1641
+ locally_derived.add(item.content.output.address)
1642
+ return locally_derived
1643
+
1608
1644
  @property
1609
1645
  def input_components(self) -> List[Concept]:
1610
1646
  output = set()
@@ -1655,11 +1691,22 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1655
1691
  address: Address,
1656
1692
  grain: Grain | None = None,
1657
1693
  ) -> Datasource:
1694
+ if self.where_clause or self.having_clause:
1695
+ modifiers = [Modifier.PARTIAL]
1696
+ else:
1697
+ modifiers = []
1658
1698
  columns = [
1659
1699
  # TODO: replace hardcoded replacement here
1660
- ColumnAssignment(alias=c.address.replace(".", "_"), concept=c)
1700
+ # if the concept is a locally derived concept, it cannot ever be partial
1701
+ # but if it's a concept pulled in from upstream and we have a where clause, it should be partial
1702
+ ColumnAssignment(
1703
+ alias=c.address.replace(".", "_"),
1704
+ concept=c,
1705
+ modifiers=modifiers if c.address not in self.locally_derived else [],
1706
+ )
1661
1707
  for c in self.output_components
1662
1708
  ]
1709
+
1663
1710
  new_datasource = Datasource(
1664
1711
  identifier=identifier,
1665
1712
  address=address,
@@ -1786,6 +1833,10 @@ class MultiSelectStatement(SelectTypeMixin, Mergeable, Namespaced, BaseModel):
1786
1833
  limit: Optional[int] = None
1787
1834
  meta: Optional[Metadata] = Field(default_factory=lambda: Metadata())
1788
1835
 
1836
+ def refresh_bindings(self, environment: Environment):
1837
+ for select in self.selects:
1838
+ select.refresh_bindings(environment)
1839
+
1789
1840
  def __repr__(self):
1790
1841
  return "MultiSelect<" + " MERGE ".join([str(s) for s in self.selects]) + ">"
1791
1842
 
@@ -2255,14 +2306,11 @@ class BaseJoin(BaseModel):
2255
2306
  def __str__(self):
2256
2307
  if self.concept_pairs:
2257
2308
  return (
2258
- f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
2259
- f" {self.right_datasource.identifier} on"
2309
+ f"{self.join_type.value} on"
2260
2310
  f" {','.join([str(k.left)+'='+str(k.right) for k in self.concept_pairs])}"
2261
2311
  )
2262
2312
  return (
2263
- f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
2264
- f" {self.right_datasource.identifier} on"
2265
- f" {','.join([str(k) for k in self.concepts])}"
2313
+ f"{self.join_type.value} on" f" {','.join([str(k) for k in self.concepts])}"
2266
2314
  )
2267
2315
 
2268
2316
 
@@ -2395,10 +2443,6 @@ class QueryDatasource(BaseModel):
2395
2443
  raise SyntaxError(
2396
2444
  "Can only merge two query datasources with identical grain"
2397
2445
  )
2398
- if not self.source_type == other.source_type:
2399
- raise SyntaxError(
2400
- "Can only merge two query datasources with identical source type"
2401
- )
2402
2446
  if not self.group_required == other.group_required:
2403
2447
  raise SyntaxError(
2404
2448
  "can only merge two datasources if the group required flag is the same"
@@ -2437,9 +2481,7 @@ class QueryDatasource(BaseModel):
2437
2481
  final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2438
2482
  self_hidden = self.hidden_concepts or []
2439
2483
  other_hidden = other.hidden_concepts or []
2440
- hidden = [
2441
- x for x in self_hidden if x.address in [y.address for y in other_hidden]
2442
- ]
2484
+ hidden = [x for x in self_hidden if x.address in other_hidden]
2443
2485
  qds = QueryDatasource(
2444
2486
  input_concepts=unique(
2445
2487
  self.input_concepts + other.input_concepts, "address"
@@ -2609,7 +2651,7 @@ class CTE(BaseModel):
2609
2651
 
2610
2652
  @property
2611
2653
  def comment(self) -> str:
2612
- base = f"Target: {str(self.grain)}."
2654
+ base = f"Target: {str(self.grain)}. Group: {self.group_to_grain}"
2613
2655
  base += f" Source: {self.source.source_type}."
2614
2656
  if self.parent_ctes:
2615
2657
  base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
@@ -2621,6 +2663,8 @@ class CTE(BaseModel):
2621
2663
  )
2622
2664
  base += f"\n-- Source Map: {self.source_map}."
2623
2665
  base += f"\n-- Output: {', '.join([str(x) for x in self.output_columns])}."
2666
+ if self.source.input_concepts:
2667
+ base += f"\n-- Inputs: {', '.join([str(x) for x in self.source.input_concepts])}."
2624
2668
  if self.hidden_concepts:
2625
2669
  base += f"\n-- Hidden: {', '.join([str(x) for x in self.hidden_concepts])}."
2626
2670
  if self.nullable_concepts:
@@ -2660,9 +2704,9 @@ class CTE(BaseModel):
2660
2704
  if isinstance(join, InstantiatedUnnestJoin):
2661
2705
  continue
2662
2706
  if join.left_cte.name == parent.name:
2663
- join.left_cte = ds_being_inlined
2707
+ join.inline_cte(parent)
2664
2708
  if join.right_cte.name == parent.name:
2665
- join.right_cte = ds_being_inlined
2709
+ join.inline_cte(parent)
2666
2710
  for k, v in self.source_map.items():
2667
2711
  if isinstance(v, list):
2668
2712
  self.source_map[k] = [
@@ -2697,7 +2741,7 @@ class CTE(BaseModel):
2697
2741
  raise ValueError(error)
2698
2742
  mutually_hidden = []
2699
2743
  for concept in self.hidden_concepts:
2700
- if concept in other.hidden_concepts:
2744
+ if concept.address in other.hidden_concepts:
2701
2745
  mutually_hidden.append(concept)
2702
2746
  self.partial_concepts = unique(
2703
2747
  self.partial_concepts + other.partial_concepts, "address"
@@ -2797,37 +2841,23 @@ class CTE(BaseModel):
2797
2841
 
2798
2842
  @property
2799
2843
  def group_concepts(self) -> List[Concept]:
2844
+ def check_is_not_in_group(c: Concept):
2845
+ if len(self.source_map.get(c.address, [])) > 0:
2846
+ return False
2847
+ if c.derivation == PurposeLineage.ROWSET:
2848
+ return False
2849
+ if c.derivation == PurposeLineage.CONSTANT:
2850
+ return False
2851
+ if c.purpose == Purpose.METRIC:
2852
+ return True
2853
+ elif c.derivation == PurposeLineage.BASIC and c.lineage:
2854
+ if all([check_is_not_in_group(x) for x in c.lineage.concept_arguments]):
2855
+ return True
2856
+ return False
2857
+
2800
2858
  return (
2801
2859
  unique(
2802
- self.grain.components
2803
- + [
2804
- c
2805
- for c in self.output_columns
2806
- if c.purpose in (Purpose.PROPERTY, Purpose.KEY)
2807
- and c.address not in [x.address for x in self.grain.components]
2808
- ]
2809
- + [
2810
- c
2811
- for c in self.output_columns
2812
- if c.purpose == Purpose.METRIC
2813
- and (
2814
- any(
2815
- [
2816
- c.with_grain(cte.grain) in cte.output_columns
2817
- for cte in self.parent_ctes
2818
- ]
2819
- )
2820
- # if we have this metric from a source
2821
- # it isn't derived here and must be grouped on
2822
- or len(self.source_map[c.address]) > 0
2823
- )
2824
- ]
2825
- + [
2826
- c
2827
- for c in self.output_columns
2828
- if c.purpose == Purpose.CONSTANT
2829
- and self.source_map[c.address] != []
2830
- ],
2860
+ [c for c in self.output_columns if not check_is_not_in_group(c)],
2831
2861
  "address",
2832
2862
  )
2833
2863
  if self.group_to_grain
@@ -2885,34 +2915,38 @@ class JoinKey(BaseModel):
2885
2915
 
2886
2916
 
2887
2917
  class Join(BaseModel):
2888
- left_cte: CTE | Datasource
2889
- right_cte: CTE | Datasource
2918
+ left_cte: CTE
2919
+ right_cte: CTE
2890
2920
  jointype: JoinType
2891
2921
  joinkeys: List[JoinKey]
2892
2922
  joinkey_pairs: List[ConceptPair] | None = None
2923
+ inlined_ctes: set[str] = Field(default_factory=set)
2924
+
2925
+ def inline_cte(self, cte: CTE):
2926
+ self.inlined_ctes.add(cte.name)
2893
2927
 
2894
2928
  @property
2895
2929
  def left_name(self) -> str:
2896
- if isinstance(self.left_cte, Datasource):
2897
- return self.left_cte.identifier
2930
+ if self.left_cte.name in self.inlined_ctes:
2931
+ return self.left_cte.source.datasources[0].identifier
2898
2932
  return self.left_cte.name
2899
2933
 
2900
2934
  @property
2901
2935
  def right_name(self) -> str:
2902
- if isinstance(self.right_cte, Datasource):
2903
- return self.right_cte.identifier
2936
+ if self.right_cte.name in self.inlined_ctes:
2937
+ return self.right_cte.source.datasources[0].identifier
2904
2938
  return self.right_cte.name
2905
2939
 
2906
2940
  @property
2907
2941
  def left_ref(self) -> str:
2908
- if isinstance(self.left_cte, Datasource):
2909
- return f"{self.left_cte.safe_location} as {self.left_cte.identifier}"
2942
+ if self.left_cte.name in self.inlined_ctes:
2943
+ return f"{self.left_cte.source.datasources[0].safe_location} as {self.left_cte.source.datasources[0].identifier}"
2910
2944
  return self.left_cte.name
2911
2945
 
2912
2946
  @property
2913
2947
  def right_ref(self) -> str:
2914
- if isinstance(self.right_cte, Datasource):
2915
- return f"{self.right_cte.safe_location} as {self.right_cte.identifier}"
2948
+ if self.right_cte.name in self.inlined_ctes:
2949
+ return f"{self.right_cte.source.datasources[0].safe_location} as {self.right_cte.source.datasources[0].identifier}"
2916
2950
  return self.right_cte.name
2917
2951
 
2918
2952
  @property
@@ -3386,6 +3420,18 @@ class Environment(BaseModel):
3386
3420
  ):
3387
3421
 
3388
3422
  self.datasources[datasource.env_label] = datasource
3423
+ for column in datasource.columns:
3424
+ current_concept = column.concept
3425
+ current_derivation = current_concept.derivation
3426
+ if current_derivation not in (PurposeLineage.ROOT, PurposeLineage.CONSTANT):
3427
+ new_concept = current_concept.model_copy(deep=True)
3428
+ new_concept.set_name("_pre_persist_" + current_concept.name)
3429
+ # remove the associated lineage
3430
+ current_concept.lineage = None
3431
+ self.add_concept(new_concept, meta=meta, force=True)
3432
+ self.add_concept(current_concept, meta=meta, force=True)
3433
+ self.merge_concept(new_concept, current_concept, [])
3434
+
3389
3435
  self.gen_concept_list_caches()
3390
3436
  return datasource
3391
3437
 
@@ -125,23 +125,29 @@ def is_direct_return_eligible(cte: CTE) -> CTE | None:
125
125
  for c in cte.source.output_concepts + cte.source.hidden_concepts
126
126
  if c not in cte.source.input_concepts
127
127
  ]
128
- conditions = (
129
- set(x.address for x in direct_parent.condition.concept_arguments)
130
- if direct_parent.condition
131
- else set()
132
- )
128
+
129
+ parent_derived_concepts = [
130
+ c
131
+ for c in direct_parent.source.output_concepts
132
+ + direct_parent.source.hidden_concepts
133
+ if c not in direct_parent.source.input_concepts
134
+ ]
135
+ condition_arguments = cte.condition.row_arguments if cte.condition else []
133
136
  for x in derived_concepts:
134
137
  if x.derivation == PurposeLineage.WINDOW:
135
138
  return None
136
139
  if x.derivation == PurposeLineage.UNNEST:
137
140
  return None
138
141
  if x.derivation == PurposeLineage.AGGREGATE:
139
- if x.address in conditions:
140
- return None
141
- # handling top level nodes that require unpacking
142
- for x in cte.output_columns:
142
+ return None
143
+ for x in parent_derived_concepts:
144
+ if x.address not in condition_arguments:
145
+ continue
143
146
  if x.derivation == PurposeLineage.UNNEST:
144
147
  return None
148
+ if x.derivation == PurposeLineage.WINDOW:
149
+ return None
150
+
145
151
  logger.info(
146
152
  f"[Optimization][EarlyReturn] Removing redundant output CTE with derived_concepts {[x.address for x in derived_concepts]}"
147
153
  )