pytrilogy 0.0.3.46__py3-none-any.whl → 0.0.3.48__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.46
3
+ Version: 0.0.3.48
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -53,7 +53,7 @@ Installation: `pip install pytrilogy`
53
53
 
54
54
  You can read more about the project [here](https://trilogydata.dev/) and try out an interactive demo [here](https://trilogydata.dev/demo/).
55
55
 
56
- Trilogy:
56
+ Trilogy looks like SQL:
57
57
  ```sql
58
58
  WHERE
59
59
  name like '%lvis%'
@@ -65,7 +65,7 @@ ORDER BY
65
65
  LIMIT 10;
66
66
  ```
67
67
  ## Goals
68
- vs SQL, the goals are:
68
+ And aims to:
69
69
 
70
70
  Preserve:
71
71
  - Correctness
@@ -85,6 +85,7 @@ Maintain:
85
85
  Save the following code in a file named `hello.preql`
86
86
 
87
87
  ```python
88
+ # semantic model is abstract from data
88
89
  key sentence_id int;
89
90
  property sentence_id.word_one string; # comments after a definition
90
91
  property sentence_id.word_two string; # are syntactic sugar for adding
@@ -92,7 +93,8 @@ property sentence_id.word_three string; # a description to it
92
93
 
93
94
  # comments in other places are just comments
94
95
 
95
- # define our datasources as queries in duckdb
96
+ # define our datasource to bind the model to data
97
+ # testing using query fixtures is a common pattern
96
98
  datasource word_one(
97
99
  sentence: sentence_id,
98
100
  word:word_one
@@ -126,25 +128,20 @@ union all
126
128
  select 2 as sentence, '!'
127
129
  ''';
128
130
 
131
+ def concat_with_space(x,y) -> x || ' ' || y;
132
+
129
133
  # an actual select statement
130
134
  # joins are automatically resolved between the 3 sources
131
135
  with sentences as
132
- select sentence_id, word_one || ' ' || word_two || word_three as text;
136
+ select sentence_id, @concat_with_space(word_one, word_two) || word_three as text;
133
137
 
134
- SELECT
135
- --sentences.sentence_id,
136
- sentences.text
137
138
  WHERE
138
- sentences.sentence_id = 1
139
- ;
140
-
139
+ sentences.sentence_id in (1,2)
141
140
  SELECT
142
- --sentences.sentence_id,
143
141
  sentences.text
144
- WHERE
145
- sentences.sentence_id = 2
146
142
  ;
147
- # semicolon termination for all statements
143
+
144
+
148
145
 
149
146
  ```
150
147
 
@@ -1,9 +1,9 @@
1
- pytrilogy-0.0.3.46.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=YyUmUiX_R4JnFfmRIfKopul0PlCDdGcFxMRUxiSPkp0,303
1
+ pytrilogy-0.0.3.48.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=X7jtGsMd3bHz73UVPQxZzoipqeTD3gI4UEQZtAq3OUs,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
6
- trilogy/executor.py,sha256=_xihzIaUEbE5lzwHECsvQ75Dm5fdRPBdMCVz6gNBpV4,16091
6
+ trilogy/executor.py,sha256=GwNhP9UW4565dxnpHbw-VWNE2lX8uroQJQtSpC_j2pI,16298
7
7
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
8
8
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
@@ -13,25 +13,24 @@ trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
14
14
  trilogy/core/enums.py,sha256=JwbWyAHOC2xRTZe2SeEvlIGPvmC1KjcJ4uh1Po5USzQ,7380
15
15
  trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
16
- trilogy/core/environment_helpers.py,sha256=UWtF5ZQqFyzHdrjUBEd7c2ZfASBhBWFoa9WkUHBbyHI,9700
16
+ trilogy/core/environment_helpers.py,sha256=VvPIiFemqaLLpIpLIqprfu63K7muZ1YzNg7UZIUph8w,8267
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=4fEOGgXWDvgrJtCg_5m2Y9iWnHfLbvLQ82RkIMl_1K0,27722
20
20
  trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
21
21
  trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
22
- trilogy/core/optimization.py,sha256=aihzx4-2-mSjx5td1TDTYGvc7e9Zvy-_xEyhPqLS-Ig,8314
23
- trilogy/core/query_processor.py,sha256=Vl-u0F0rbqI2liv82yJgiZCB255Kx_KiuzZVHL6aeTM,19459
22
+ trilogy/core/optimization.py,sha256=O7ag0IVQlJyWdAXBi_hHeU3Df5DRyd75Vlz6pks2J10,8197
23
+ trilogy/core/query_processor.py,sha256=NNzYPKN5HzivQFXugSbJC_MaupkwOYii7A_vnXuBIK4,20063
24
24
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- trilogy/core/models/author.py,sha256=b5GQc79w-gFQfZhgBdHeRCJAtCYr4j_da6k3Dkx4YAA,76863
26
- trilogy/core/models/build.py,sha256=EsI7BLmFXdxj1an3NnKR_Qm79tcjlFKjmLjmt3_v2eA,61829
25
+ trilogy/core/models/author.py,sha256=NhTKuk1eYAuYBbpvaFUxr-LntIoVarFQlNuNJwZmMmw,76990
26
+ trilogy/core/models/build.py,sha256=MPiHgyfOumZ8zF3iB61pzrAeDAlGV2F9R0Dw7mTTyqQ,62708
27
27
  trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
28
28
  trilogy/core/models/core.py,sha256=wx6hJcFECMG-Ij972ADNkr-3nFXkYESr82ObPiC46_U,10875
29
29
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
30
30
  trilogy/core/models/environment.py,sha256=AVSrvjNcNX535GhCPtYhCRY2Lp_Hj0tdY3VVt_kZb9Q,27260
31
- trilogy/core/models/execute.py,sha256=mQm5Gydo2Ph0W7w9wm5dQEarS04PC-IKAgNVsdqOZsQ,34524
32
- trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
31
+ trilogy/core/models/execute.py,sha256=m_GodtQkhuPo5kyBNlfC9c_jgprV7M64kE6x_12_ExQ,34616
32
+ trilogy/core/optimizations/__init__.py,sha256=YH2-mGXZnVDnBcWVi8vTbrdw7Qs5TivG4h38rH3js_I,290
33
33
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
34
- trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52doLU9sIg3pmU2_S08,1332
35
34
  trilogy/core/optimizations/inline_datasource.py,sha256=AHuTGh2x0GQ8usOe0NiFncfTFQ_KogdgDl4uucmhIbI,4241
36
35
  trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
37
36
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -42,7 +41,7 @@ trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW
42
41
  trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
43
42
  trilogy/core/processing/node_generators/common.py,sha256=nVeH_AdO58ygtNSO0wNgMR7_h2D0dFSGM_rh1fJd4Yc,9468
44
43
  trilogy/core/processing/node_generators/filter_node.py,sha256=JymSKzA-9oQAZ3ZtJRK9c3w5FXs8MjJBGWU9TYUqx4E,9099
45
- trilogy/core/processing/node_generators/group_node.py,sha256=kO-ersxIL04rZwX5-vFIFQQnp357PFo_7ZKXoGq3wyc,5989
44
+ trilogy/core/processing/node_generators/group_node.py,sha256=ISv2lLnr5m5nMpiXYJbgBqfUPQqeypjCAcaool9Kvnk,6109
46
45
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
47
46
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
48
47
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=sv55oynfqgpHEpo1OEtVDri-5fywzPhDlR85qaWikvY,16195
@@ -55,7 +54,7 @@ trilogy/core/processing/node_generators/unnest_node.py,sha256=cOEKnMRzXUW3bwmiOl
55
54
  trilogy/core/processing/node_generators/window_node.py,sha256=RUHgpYovQObFod1xRIMWtDzMcxwlm4-1Fdrf_Cuw5W4,6346
56
55
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
56
  trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=GMW07bb6hXurhF0hZLYoMAKSIS65tat5hwBjvqqPeSA,6516
58
- trilogy/core/processing/nodes/__init__.py,sha256=MfookoG73Z9p7kYFDh033uZJpP4exqkwdKtheDRepAw,5893
57
+ trilogy/core/processing/nodes/__init__.py,sha256=xPFF7x3TFs1Z4IcfthCykZgrksb-UhN-pc_oIigfFSo,6014
59
58
  trilogy/core/processing/nodes/base_node.py,sha256=FHrY8GsTKPuMJklOjILbhGqCt5s1nmlj62Z-molARDA,16835
60
59
  trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
61
60
  trilogy/core/processing/nodes/group_node.py,sha256=MUvcOg9U5J6TnWBel8eht9PdI9BfAKjUxmfjP_ZXx9o,10484
@@ -65,14 +64,14 @@ trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ
65
64
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
66
65
  trilogy/core/processing/nodes/window_node.py,sha256=JXJ0iVRlSEM2IBr1TANym2RaUf_p5E_l2sNykRzXWDo,1710
67
66
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- trilogy/core/statements/author.py,sha256=rYDf9rCQ4YKEO9br1OWmOZd-51AiaDkaWYegvteJa8M,14728
67
+ trilogy/core/statements/author.py,sha256=2q0yvP_0AbExdXnASlLG7OaDcM7sBaRco6YALnrQwzg,15255
69
68
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
69
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
71
70
  trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
72
71
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- trilogy/dialect/base.py,sha256=vp6_9fUkblAWVpCXGBIcoAx6N7vof9M7s9t6-b_waUY,41409
72
+ trilogy/dialect/base.py,sha256=RkfNoNSo46p-WCafAWC5tXqJ_FMZEXANLyZSqX7_Pxw,42082
74
73
  trilogy/dialect/bigquery.py,sha256=7LcgPLDkeNBk6YTfaE-RBBi7SjWFV-jjuvZM1VMIXqk,3350
75
- trilogy/dialect/common.py,sha256=XjHkP8Dqezjkd2JU5xoAlMRS_6HNyXQCF4CykLK3C8o,5011
74
+ trilogy/dialect/common.py,sha256=JQ8ONloalaWEXsTTWUhZcYyzMRaZ9HdUw7cN6QWtY5c,5295
76
75
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
77
76
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
78
77
  trilogy/dialect/duckdb.py,sha256=XTBK4RhE1_wF2_IA_7c2W5ih0uxZx0wZ1mfJ3YFIuso,3768
@@ -87,13 +86,13 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
87
86
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
88
87
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
88
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- trilogy/parsing/common.py,sha256=U9RNi1GyPTQaitZGwXy1QftdC5PWYArP7V8t-v3H8Po,27157
89
+ trilogy/parsing/common.py,sha256=u7V8uc2mdBtszVujk-hzllfDAqM3j5pKd8B9UEj-uNc,29223
91
90
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
92
- trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
91
+ trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
93
92
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
94
- trilogy/parsing/parse_engine.py,sha256=9SO2q8m5MlZo_Eho-_r6hmTSm5VH38k47C2iHTtYwjU,68224
93
+ trilogy/parsing/parse_engine.py,sha256=K3TwjCiiZtG3UrICF9Alik56_KPusVNWfqE-oUaKfho,68664
95
94
  trilogy/parsing/render.py,sha256=hI4y-xjXrEXvHslY2l2TQ8ic0zAOpN41ADH37J2_FZY,19047
96
- trilogy/parsing/trilogy.lark,sha256=zbDAIG7gpsImxBtteD8E2pKwcJCGpM-rEQDRqpgzoSQ,13717
95
+ trilogy/parsing/trilogy.lark,sha256=q15J3P71yA_4lsWjC1vb7eDTemkJGLPKYvf5Hn9IBIk,13584
97
96
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
97
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
99
98
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -102,8 +101,8 @@ trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
102
101
  trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
103
102
  trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
104
103
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
105
- pytrilogy-0.0.3.46.dist-info/METADATA,sha256=1cBYEU3n78eaoNyqLsOS-eP_WylqceEDeFoktS7JzO0,9100
106
- pytrilogy-0.0.3.46.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
107
- pytrilogy-0.0.3.46.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
108
- pytrilogy-0.0.3.46.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
109
- pytrilogy-0.0.3.46.dist-info/RECORD,,
104
+ pytrilogy-0.0.3.48.dist-info/METADATA,sha256=xuGEKV1ZdJtS-uES50q5bj5x-_60E4Pb6VL5G_SKzNQ,9095
105
+ pytrilogy-0.0.3.48.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
106
+ pytrilogy-0.0.3.48.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
107
+ pytrilogy-0.0.3.48.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
108
+ pytrilogy-0.0.3.48.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.4.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.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.46"
7
+ __version__ = "0.0.3.48"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -6,23 +6,8 @@ from trilogy.core.models.core import DataType, StructType, arg_to_datatype
6
6
  from trilogy.core.models.environment import Environment
7
7
  from trilogy.parsing.common import Meta
8
8
 
9
- FUNCTION_DESCRIPTION_MAPS = {
10
- FunctionType.DATE: "The date part of a timestamp/date. Integer, 0-31 depending on month.",
11
- FunctionType.MONTH: "The month part of a timestamp/date. Integer, 1-12.",
12
- FunctionType.YEAR: "The year part of a timestamp/date. Integer.",
13
- FunctionType.QUARTER: "The quarter part of a timestamp/date. Integer, 1-4.",
14
- FunctionType.DAY_OF_WEEK: "The day of the week part of a timestamp/date. Integer, 0-6.",
15
- FunctionType.HOUR: "The hour part of a timestamp. Integer, 0-23.",
16
- FunctionType.MINUTE: "The minute part of a timestamp. Integer, 0-59.",
17
- FunctionType.SECOND: "The second part of a timestamp. Integer, 0-59.",
18
- }
19
-
20
9
 
21
10
  def generate_date_concepts(concept: Concept, environment: Environment):
22
- if concept.metadata and concept.metadata.description:
23
- base_description = concept.metadata.description
24
- else:
25
- base_description = f"a {concept.address}"
26
11
  if concept.metadata and concept.metadata.line_number:
27
12
  base_line_number = concept.metadata.line_number
28
13
  else:
@@ -67,7 +52,6 @@ def generate_date_concepts(concept: Concept, environment: Environment):
67
52
  [concept.address],
68
53
  ),
69
54
  metadata=Metadata(
70
- description=f"Auto-derived from {base_description}. {FUNCTION_DESCRIPTION_MAPS.get(ftype, ftype.value)}",
71
55
  line_number=base_line_number,
72
56
  concept_source=ConceptSource.AUTO_DERIVED,
73
57
  ),
@@ -95,7 +79,7 @@ def generate_date_concepts(concept: Concept, environment: Environment):
95
79
  [concept.address],
96
80
  ),
97
81
  metadata=Metadata(
98
- description=f"Auto-derived from {base_description}. The date truncated to the {grain.value}.",
82
+ # description=f"Auto-derived from {base_description}. The date truncated to the {grain.value}.",
99
83
  line_number=base_line_number,
100
84
  concept_source=ConceptSource.AUTO_DERIVED,
101
85
  ),
@@ -105,10 +89,6 @@ def generate_date_concepts(concept: Concept, environment: Environment):
105
89
 
106
90
 
107
91
  def generate_datetime_concepts(concept: Concept, environment: Environment):
108
- if concept.metadata and concept.metadata.description:
109
- base_description = concept.metadata.description
110
- else:
111
- base_description = concept.address
112
92
  if concept.metadata and concept.metadata.line_number:
113
93
  base_line_number = concept.metadata.line_number
114
94
  else:
@@ -146,7 +126,6 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
146
126
  [concept.address],
147
127
  ),
148
128
  metadata=Metadata(
149
- description=f"Auto-derived from {base_description}. {FUNCTION_DESCRIPTION_MAPS.get(ftype, ftype.value)}",
150
129
  line_number=base_line_number,
151
130
  concept_source=ConceptSource.AUTO_DERIVED,
152
131
  ),
@@ -157,10 +136,6 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
157
136
 
158
137
 
159
138
  def generate_key_concepts(concept: Concept, environment: Environment):
160
- if concept.metadata and concept.metadata.description:
161
- base_description = concept.metadata.description
162
- else:
163
- base_description = f"a {concept.datatype.value}"
164
139
  if concept.metadata and concept.metadata.line_number:
165
140
  base_line_number = concept.metadata.line_number
166
141
  else:
@@ -186,7 +161,7 @@ def generate_key_concepts(concept: Concept, environment: Environment):
186
161
  namespace=concept.namespace,
187
162
  keys=set(),
188
163
  metadata=Metadata(
189
- description=f"Auto-derived integer. The {ftype.value} of {concept.address}, {base_description}",
164
+ # description=f"Auto-derived integer. The {ftype.value} of {concept.address}, {base_description}",
190
165
  line_number=base_line_number,
191
166
  concept_source=ConceptSource.AUTO_DERIVED,
192
167
  ),
@@ -445,13 +445,16 @@ class Grain(Namespaced, BaseModel):
445
445
  concepts: Iterable[Concept | ConceptRef | str],
446
446
  environment: Environment | None = None,
447
447
  where_clause: WhereClause | None = None,
448
+ local_concepts: dict[str, Concept] | None = None,
448
449
  ) -> Grain:
449
450
  from trilogy.parsing.common import concepts_to_grain_concepts
450
451
 
451
452
  x = Grain.model_construct(
452
453
  components={
453
454
  c.address
454
- for c in concepts_to_grain_concepts(concepts, environment=environment)
455
+ for c in concepts_to_grain_concepts(
456
+ concepts, environment=environment, local_concepts=local_concepts
457
+ )
455
458
  },
456
459
  where_clause=where_clause,
457
460
  )
@@ -126,9 +126,14 @@ def concept_is_relevant(
126
126
 
127
127
  return False
128
128
  if concept.purpose in (Purpose.PROPERTY, Purpose.METRIC) and concept.keys:
129
- if any([c in others for c in concept.keys]):
130
-
129
+ if all([c in others for c in concept.keys]):
131
130
  return False
131
+ if (
132
+ concept.purpose == Purpose.KEY
133
+ and concept.keys
134
+ and all([c in others for c in concept.keys])
135
+ ):
136
+ return False
132
137
  if concept.purpose in (Purpose.METRIC,):
133
138
  if all([c in others for c in concept.grain.components]):
134
139
  return False
@@ -242,6 +247,17 @@ def get_concept_arguments(expr) -> List["BuildConcept"]:
242
247
  return output
243
248
 
244
249
 
250
+ class BuildParamaterizedConceptReference(BaseModel):
251
+ concept: BuildConcept
252
+
253
+ def __str__(self):
254
+ return f":{self.concept.address}"
255
+
256
+ @property
257
+ def safe_address(self) -> str:
258
+ return self.concept.safe_address
259
+
260
+
245
261
  class BuildGrain(BaseModel):
246
262
  components: set[str] = Field(default_factory=set)
247
263
  where_clause: Optional[BuildWhereClause] = None
@@ -1458,11 +1474,35 @@ class Factory:
1458
1474
  {} if local_concepts is None else local_concepts
1459
1475
  )
1460
1476
 
1477
+ def instantiate_concept(
1478
+ self,
1479
+ arg: (
1480
+ AggregateWrapper
1481
+ | FunctionCallWrapper
1482
+ | WindowItem
1483
+ | FilterItem
1484
+ | Function
1485
+ | ListWrapper[Any]
1486
+ | MapWrapper[Any, Any]
1487
+ | int
1488
+ | float
1489
+ | str
1490
+ ),
1491
+ ) -> tuple[Concept, BuildConcept]:
1492
+ from trilogy.parsing.common import arbitrary_to_concept
1493
+
1494
+ new = arbitrary_to_concept(
1495
+ arg,
1496
+ environment=self.environment,
1497
+ )
1498
+ built = self.build(new)
1499
+ self.local_concepts[new.address] = built
1500
+ return new, built
1501
+
1461
1502
  @singledispatchmethod
1462
1503
  def build(self, base):
1463
1504
  raise NotImplementedError("Cannot build {}".format(type(base)))
1464
1505
 
1465
- @build.register
1466
1506
  @build.register
1467
1507
  def _(
1468
1508
  self,
@@ -1496,31 +1536,6 @@ class Factory:
1496
1536
  ):
1497
1537
  return base
1498
1538
 
1499
- def instantiate_concept(
1500
- self,
1501
- arg: (
1502
- AggregateWrapper
1503
- | FunctionCallWrapper
1504
- | WindowItem
1505
- | FilterItem
1506
- | Function
1507
- | ListWrapper[Any]
1508
- | MapWrapper[Any, Any]
1509
- | int
1510
- | float
1511
- | str
1512
- ),
1513
- ) -> tuple[Concept, BuildConcept]:
1514
- from trilogy.parsing.common import arbitrary_to_concept
1515
-
1516
- new = arbitrary_to_concept(
1517
- arg,
1518
- environment=self.environment,
1519
- )
1520
- built = self.build(new)
1521
- self.local_concepts[new.address] = built
1522
- return new, built
1523
-
1524
1539
  @build.register
1525
1540
  def _(self, base: None) -> None:
1526
1541
  return base
@@ -1574,7 +1589,7 @@ class Factory:
1574
1589
 
1575
1590
  new = BuildFunction.model_construct(
1576
1591
  operator=base.operator,
1577
- arguments=[self.build(c) for c in raw_args],
1592
+ arguments=[self.handle_constant(self.build(c)) for c in raw_args],
1578
1593
  output_datatype=base.output_datatype,
1579
1594
  output_purpose=base.output_purpose,
1580
1595
  valid_inputs=base.valid_inputs,
@@ -1611,6 +1626,8 @@ class Factory:
1611
1626
 
1612
1627
  @build.register
1613
1628
  def _(self, base: Concept) -> BuildConcept:
1629
+
1630
+ # TODO: if we are using parameters, wrap it in a new model and use that in rendering
1614
1631
  if base.address in self.local_concepts:
1615
1632
  return self.local_concepts[base.address]
1616
1633
  new_lineage, final_grain, _ = base.get_select_grain_and_keys(
@@ -1626,6 +1643,7 @@ class Factory:
1626
1643
  derivation, final_grain, build_lineage
1627
1644
  )
1628
1645
  is_aggregate = Concept.calculate_is_aggregate(build_lineage)
1646
+
1629
1647
  rval = BuildConcept.model_construct(
1630
1648
  name=base.name,
1631
1649
  datatype=base.datatype,
@@ -1646,7 +1664,6 @@ class Factory:
1646
1664
 
1647
1665
  @build.register
1648
1666
  def _(self, base: AggregateWrapper) -> BuildAggregateWrapper:
1649
-
1650
1667
  if not base.by:
1651
1668
  by = [
1652
1669
  self.build(self.environment.concepts[c]) for c in self.grain.components
@@ -1734,8 +1751,8 @@ class Factory:
1734
1751
  @build.register
1735
1752
  def _(self, base: Conditional) -> BuildConditional:
1736
1753
  return BuildConditional.model_construct(
1737
- left=(self.build(base.left)),
1738
- right=(self.build(base.right)),
1754
+ left=self.handle_constant(self.build(base.left)),
1755
+ right=self.handle_constant(self.build(base.right)),
1739
1756
  operator=base.operator,
1740
1757
  )
1741
1758
 
@@ -1763,8 +1780,8 @@ class Factory:
1763
1780
  right_c, _ = self.instantiate_concept(right)
1764
1781
  right = right_c # type: ignore
1765
1782
  return BuildComparison.model_construct(
1766
- left=(self.build(left)),
1767
- right=(self.build(right)),
1783
+ left=self.handle_constant(self.build(left)),
1784
+ right=self.handle_constant(self.build(right)),
1768
1785
  operator=base.operator,
1769
1786
  )
1770
1787
 
@@ -2011,3 +2028,12 @@ class Factory:
2011
2028
  factory.build(base.non_partial_for) if base.non_partial_for else None
2012
2029
  ),
2013
2030
  )
2031
+
2032
+ def handle_constant(self, base):
2033
+ if (
2034
+ isinstance(base, BuildConcept)
2035
+ and isinstance(base.lineage, BuildFunction)
2036
+ and base.lineage.operator == FunctionType.CONSTANT
2037
+ ):
2038
+ return BuildParamaterizedConceptReference(concept=base)
2039
+ return base
@@ -24,6 +24,7 @@ from trilogy.core.models.build import (
24
24
  BuildFunction,
25
25
  BuildGrain,
26
26
  BuildOrderBy,
27
+ BuildParamaterizedConceptReference,
27
28
  BuildParenthetical,
28
29
  BuildRowsetItem,
29
30
  LooseBuildConceptList,
@@ -447,7 +448,7 @@ class CTEConceptPair(ConceptPair):
447
448
 
448
449
 
449
450
  class InstantiatedUnnestJoin(BaseModel):
450
- concept_to_unnest: BuildConcept
451
+ object_to_unnest: BuildConcept | BuildParamaterizedConceptReference | BuildFunction
451
452
  alias: str = "unnest"
452
453
 
453
454
 
@@ -5,7 +5,6 @@ from trilogy.core.models.build import (
5
5
  )
6
6
  from trilogy.core.models.execute import CTE, UnionCTE
7
7
  from trilogy.core.optimizations import (
8
- InlineConstant,
9
8
  InlineDatasource,
10
9
  OptimizationRule,
11
10
  PredicatePushdown,
@@ -206,8 +205,6 @@ def optimize_ctes(
206
205
  REGISTERED_RULES.append(PredicatePushdown())
207
206
  if CONFIG.optimizations.predicate_pushdown:
208
207
  REGISTERED_RULES.append(PredicatePushdownRemove())
209
- if CONFIG.optimizations.constant_inlining:
210
- REGISTERED_RULES.append(InlineConstant())
211
208
  for rule in REGISTERED_RULES:
212
209
  loops = 0
213
210
  complete = False
@@ -1,11 +1,9 @@
1
1
  from .base_optimization import OptimizationRule
2
- from .inline_constant import InlineConstant
3
2
  from .inline_datasource import InlineDatasource
4
3
  from .predicate_pushdown import PredicatePushdown, PredicatePushdownRemove
5
4
 
6
5
  __all__ = [
7
6
  "OptimizationRule",
8
- "InlineConstant",
9
7
  "InlineDatasource",
10
8
  "PredicatePushdown",
11
9
  "PredicatePushdownRemove",
@@ -1,6 +1,7 @@
1
1
  from typing import List
2
2
 
3
3
  from trilogy.constants import logger
4
+ from trilogy.core.internal import ALL_ROWS_CONCEPT
4
5
  from trilogy.core.models.build import (
5
6
  BuildAggregateWrapper,
6
7
  BuildConcept,
@@ -92,7 +93,9 @@ def gen_group_node(
92
93
  logger.info(
93
94
  f"{padding(depth)}{LOGGER_PREFIX} fetching group node parents {LooseBuildConceptList(concepts=parent_concepts)}"
94
95
  )
95
- parent_concepts = unique(parent_concepts, "address")
96
+ parent_concepts = unique(
97
+ [x for x in parent_concepts if not x.name == ALL_ROWS_CONCEPT], "address"
98
+ )
96
99
  parent = source_concepts(
97
100
  mandatory_list=parent_concepts,
98
101
  environment=environment,
@@ -1,6 +1,7 @@
1
1
  from pydantic import BaseModel, ConfigDict, Field
2
2
 
3
3
  from trilogy.core.exceptions import UnresolvableQueryException
4
+ from trilogy.core.models.author import Concept
4
5
  from trilogy.core.models.build import BuildConcept, BuildWhereClause
5
6
  from trilogy.core.models.build_environment import BuildEnvironment
6
7
  from trilogy.core.models.environment import Environment
@@ -17,6 +18,7 @@ from .window_node import WindowNode
17
18
 
18
19
  class History(BaseModel):
19
20
  base_environment: Environment
21
+ local_base_concepts: dict[str, Concept] = Field(default_factory=dict)
20
22
  history: dict[str, StrategyNode | None] = Field(default_factory=dict)
21
23
  select_history: dict[str, StrategyNode | None] = Field(default_factory=dict)
22
24
  started: dict[str, int] = Field(default_factory=dict)
@@ -12,7 +12,9 @@ from trilogy.core.models.build import (
12
12
  BuildConcept,
13
13
  BuildConditional,
14
14
  BuildDatasource,
15
+ BuildFunction,
15
16
  BuildMultiSelectLineage,
17
+ BuildParamaterizedConceptReference,
16
18
  BuildSelectLineage,
17
19
  Factory,
18
20
  )
@@ -55,8 +57,14 @@ def base_join_to_join(
55
57
  """This function converts joins at the datasource level
56
58
  to joins at the CTE level"""
57
59
  if isinstance(base_join, UnnestJoin):
60
+ object_to_unnest = base_join.parent.arguments[0]
61
+ if not isinstance(
62
+ object_to_unnest,
63
+ (BuildConcept | BuildParamaterizedConceptReference | BuildFunction),
64
+ ):
65
+ raise ValueError(f"Unnest join must be a concept; got {object_to_unnest}")
58
66
  return InstantiatedUnnestJoin(
59
- concept_to_unnest=base_join.parent.concept_arguments[0],
67
+ object_to_unnest=object_to_unnest,
60
68
  alias=base_join.alias,
61
69
  )
62
70
 
@@ -220,6 +228,8 @@ def resolve_cte_base_name_and_alias_v2(
220
228
  source_map: Dict[str, list[str]],
221
229
  raw_joins: List[Join | InstantiatedUnnestJoin],
222
230
  ) -> Tuple[str | None, str | None]:
231
+ if not source.datasources:
232
+ return None, None
223
233
  if (
224
234
  isinstance(source.datasources[0], BuildDatasource)
225
235
  and not source.datasources[0].name == CONSTANT_DATASET
@@ -301,18 +311,23 @@ def datasource_to_cte(
301
311
 
302
312
  else:
303
313
  # source is the first datasource of the query datasource
304
- source = query_datasource.datasources[0]
305
- # this is required to ensure that constant datasets
306
- # render properly on initial access; since they have
307
- # no actual source
308
- if source.name == CONSTANT_DATASET:
309
- source_map = {k: [] for k in query_datasource.source_map}
310
- existence_map = source_map
314
+ if query_datasource.datasources:
315
+
316
+ source = query_datasource.datasources[0]
317
+ # this is required to ensure that constant datasets
318
+ # render properly on initial access; since they have
319
+ # no actual source
320
+ if source.name == CONSTANT_DATASET:
321
+ source_map = {k: [] for k in query_datasource.source_map}
322
+ existence_map = source_map
323
+ else:
324
+ source_map = {
325
+ k: [] if not v else [source.safe_identifier]
326
+ for k, v in query_datasource.source_map.items()
327
+ }
328
+ existence_map = source_map
311
329
  else:
312
- source_map = {
313
- k: [] if not v else [source.safe_identifier]
314
- for k, v in query_datasource.source_map.items()
315
- }
330
+ source_map = {k: [] for k in query_datasource.source_map}
316
331
  existence_map = source_map
317
332
 
318
333
  human_id = generate_cte_name(query_datasource.identifier, name_map)
@@ -373,10 +388,9 @@ def get_query_node(
373
388
  ) -> StrategyNode:
374
389
  if not statement.output_components:
375
390
  raise ValueError(f"Statement has no output components {statement}")
376
-
377
391
  history = history or History(base_environment=environment)
378
392
  build_statement: BuildSelectLineage | BuildMultiSelectLineage = Factory(
379
- environment=environment
393
+ environment=environment,
380
394
  ).build(statement)
381
395
 
382
396
  # build_statement = statement
@@ -7,6 +7,7 @@ from pydantic.functional_validators import PlainValidator
7
7
 
8
8
  from trilogy.constants import CONFIG
9
9
  from trilogy.core.enums import (
10
+ ConceptSource,
10
11
  FunctionClass,
11
12
  IOType,
12
13
  Modifier,
@@ -134,7 +135,7 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
134
135
  meta=meta or Metadata(),
135
136
  )
136
137
 
137
- output.grain = output.calculate_grain(environment)
138
+ output.grain = output.calculate_grain(environment, output.local_concepts)
138
139
 
139
140
  for x in selection:
140
141
  if x.is_undefined and environment.concepts.fail_on_missing:
@@ -144,12 +145,13 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
144
145
  elif isinstance(x.content, ConceptTransform):
145
146
  if isinstance(x.content.output, UndefinedConcept):
146
147
  continue
147
- if (
148
- CONFIG.parsing.select_as_definition
149
- and not environment.frozen
150
- and x.concept.address not in environment.concepts
151
- ):
152
- environment.add_concept(x.content.output)
148
+ if CONFIG.parsing.select_as_definition and not environment.frozen:
149
+ if x.concept.address not in environment.concepts:
150
+ environment.add_concept(x.content.output)
151
+ elif x.concept.address in environment.concepts:
152
+ version = environment.concepts[x.concept.address]
153
+ if version.metadata.concept_source == ConceptSource.SELECT:
154
+ environment.add_concept(x.content.output, force=True)
153
155
  x.content.output = x.content.output.set_select_grain(
154
156
  output.grain, environment
155
157
  )
@@ -160,16 +162,26 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
160
162
  output.local_concepts[x.content.address] = environment.concepts[
161
163
  x.content.address
162
164
  ]
165
+
166
+ output.grain = output.calculate_grain(environment, output.local_concepts)
167
+
163
168
  output.validate_syntax(environment)
164
169
  return output
165
170
 
166
- def calculate_grain(self, environment: Environment | None = None) -> Grain:
171
+ def calculate_grain(
172
+ self,
173
+ environment: Environment | None = None,
174
+ local_concepts: dict[str, Concept] | None = None,
175
+ ) -> Grain:
167
176
  targets = []
168
177
  for x in self.selection:
169
178
  targets.append(x.concept)
170
179
 
171
180
  result = Grain.from_concepts(
172
- targets, where_clause=self.where_clause, environment=environment
181
+ targets,
182
+ where_clause=self.where_clause,
183
+ environment=environment,
184
+ local_concepts=local_concepts,
173
185
  )
174
186
  return result
175
187
 
trilogy/dialect/base.py CHANGED
@@ -3,7 +3,13 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
3
3
 
4
4
  from jinja2 import Template
5
5
 
6
- from trilogy.constants import CONFIG, MagicConstants, Rendering, logger
6
+ from trilogy.constants import (
7
+ CONFIG,
8
+ DEFAULT_NAMESPACE,
9
+ MagicConstants,
10
+ Rendering,
11
+ logger,
12
+ )
7
13
  from trilogy.core.enums import (
8
14
  DatePart,
9
15
  FunctionType,
@@ -22,6 +28,7 @@ from trilogy.core.models.build import (
22
28
  BuildFunction,
23
29
  BuildMultiSelectLineage,
24
30
  BuildOrderItem,
31
+ BuildParamaterizedConceptReference,
25
32
  BuildParenthetical,
26
33
  BuildRowsetItem,
27
34
  BuildSubselectComparison,
@@ -199,6 +206,7 @@ FUNCTION_MAP = {
199
206
  FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc({x[0]},{x[1]})",
200
207
  FunctionType.DATE_PART: lambda x: f"date_part({x[0]},{x[1]})",
201
208
  FunctionType.DATE_ADD: lambda x: f"date_add({x[0]},{x[1]}, {x[2]})",
209
+ FunctionType.DATE_SUB: lambda x: f"date_sub({x[0]},{x[1]}, {x[2]})",
202
210
  FunctionType.DATE_DIFF: lambda x: f"date_diff({x[0]},{x[1]}, {x[2]})",
203
211
  FunctionType.DATE: lambda x: f"date({x[0]})",
204
212
  FunctionType.DATETIME: lambda x: f"datetime({x[0]})",
@@ -512,6 +520,9 @@ class BaseDialect:
512
520
  BuildWindowItem,
513
521
  BuildFilterItem,
514
522
  BuildParenthetical,
523
+ BuildParamaterizedConceptReference,
524
+ BuildMultiSelectLineage,
525
+ BuildRowsetItem,
515
526
  str,
516
527
  int,
517
528
  list,
@@ -692,6 +703,14 @@ class BaseDialect:
692
703
  return self.render_expr(e.type, cte=cte, cte_map=cte_map)
693
704
  elif isinstance(e, ListType):
694
705
  return f"{self.COMPLEX_DATATYPE_MAP[DataType.LIST](self.render_expr(e.value_data_type, cte=cte, cte_map=cte_map))}"
706
+ elif isinstance(e, BuildParamaterizedConceptReference):
707
+ if self.rendering.parameters:
708
+ if e.concept.namespace == DEFAULT_NAMESPACE:
709
+ return f":{e.concept.name}"
710
+ return f":{e.concept.address}"
711
+ elif e.concept.lineage:
712
+ return self.render_expr(e.concept.lineage, cte=cte, cte_map=cte_map)
713
+ return f"{self.QUOTE_CHARACTER}{e.concept.address}{self.QUOTE_CHARACTER}"
695
714
  else:
696
715
  raise ValueError(f"Unable to render type {type(e)} {e}")
697
716
 
@@ -743,12 +762,12 @@ class BaseDialect:
743
762
  UnnestMode.CROSS_APPLY,
744
763
  ):
745
764
 
746
- source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_concept_sql, cte)}"
765
+ source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_expr, cte)}"
747
766
  elif (
748
767
  cte.join_derived_concepts
749
768
  and self.UNNEST_MODE == UnnestMode.SNOWFLAKE
750
769
  ):
751
- source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_concept_sql, cte)}"
770
+ source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_expr, cte)}"
752
771
  # direct - eg DUCK DB - can be directly selected inline
753
772
  elif (
754
773
  cte.join_derived_concepts and self.UNNEST_MODE == UnnestMode.DIRECT
@@ -802,7 +821,6 @@ class BaseDialect:
802
821
  render_join(
803
822
  join,
804
823
  self.QUOTE_CHARACTER,
805
- self.render_concept_sql,
806
824
  self.render_expr,
807
825
  cte,
808
826
  self.UNNEST_MODE,
trilogy/dialect/common.py CHANGED
@@ -1,7 +1,11 @@
1
1
  from typing import Callable
2
2
 
3
3
  from trilogy.core.enums import Modifier, UnnestMode
4
- from trilogy.core.models.build import BuildConcept, BuildFunction
4
+ from trilogy.core.models.build import (
5
+ BuildConcept,
6
+ BuildFunction,
7
+ BuildParamaterizedConceptReference,
8
+ )
5
9
  from trilogy.core.models.datasource import RawColumnExpr
6
10
  from trilogy.core.models.execute import (
7
11
  CTE,
@@ -19,21 +23,27 @@ def null_wrapper(lval: str, rval: str, modifiers: list[Modifier]) -> str:
19
23
  def render_unnest(
20
24
  unnest_mode: UnnestMode,
21
25
  quote_character: str,
22
- concept: BuildConcept,
23
- render_func: Callable[[BuildConcept, CTE, bool], str],
26
+ concept: BuildConcept | BuildParamaterizedConceptReference | BuildFunction,
27
+ render_func: Callable[
28
+ [BuildConcept | BuildParamaterizedConceptReference | BuildFunction, CTE], str
29
+ ],
24
30
  cte: CTE,
25
31
  ):
32
+ if not isinstance(concept, (BuildConcept, BuildParamaterizedConceptReference)):
33
+ address = "anon_function"
34
+ else:
35
+ address = concept.safe_address
26
36
  if unnest_mode == UnnestMode.CROSS_JOIN:
27
- return f"{render_func(concept, cte, False)} as {quote_character}{concept.safe_address}{quote_character}"
37
+ return f"{render_func(concept, cte)} as {quote_character}{address}{quote_character}"
28
38
  elif unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
29
- return f"{render_func(concept, cte, False)} as unnest_wrapper ({quote_character}{concept.safe_address}{quote_character})"
39
+ return f"{render_func(concept, cte)} as unnest_wrapper ({quote_character}{address}{quote_character})"
30
40
  elif unnest_mode == UnnestMode.SNOWFLAKE:
31
41
  # if we don't actually have a join, we're directly unnesting a concept, and we can skip the flatten
32
42
  if not cte.render_from_clause:
33
- return f"{render_func(concept, cte, False)} as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
43
+ return f"{render_func(concept, cte)} as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
34
44
  # otherwise, flatten the concept for the join
35
- return f"flatten({render_func(concept, cte, False)}) as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
36
- return f"{render_func(concept, cte, False)} as {quote_character}{concept.safe_address}{quote_character}"
45
+ return f"flatten({render_func(concept, cte)}) as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
46
+ return f"{render_func(concept, cte)} as {quote_character}{address}{quote_character}"
37
47
 
38
48
 
39
49
  def render_join_concept(
@@ -60,8 +70,9 @@ def render_join_concept(
60
70
  def render_join(
61
71
  join: Join | InstantiatedUnnestJoin,
62
72
  quote_character: str,
63
- render_func: Callable[[BuildConcept, CTE, bool], str],
64
- render_expr_func: Callable[[BuildConcept, CTE], str],
73
+ render_expr_func: Callable[
74
+ [BuildConcept | BuildParamaterizedConceptReference | BuildFunction, CTE], str
75
+ ],
65
76
  cte: CTE,
66
77
  unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
67
78
  ) -> str | None:
@@ -72,12 +83,12 @@ def render_join(
72
83
  if not cte:
73
84
  raise ValueError("must provide a cte to build an unnest joins")
74
85
  if unnest_mode == UnnestMode.CROSS_JOIN:
75
- return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
86
+ return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.object_to_unnest, render_expr_func, cte)}"
76
87
  if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
77
- return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
88
+ return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.object_to_unnest, render_expr_func, cte)}"
78
89
  if unnest_mode == UnnestMode.SNOWFLAKE:
79
- return f"LEFT JOIN LATERAL {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
80
- return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
90
+ return f"LEFT JOIN LATERAL {render_unnest(unnest_mode, quote_character, join.object_to_unnest, render_expr_func, cte)}"
91
+ return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.object_to_unnest, render_expr_func, cte)}"
81
92
  # left_name = join.left_name
82
93
  right_name = join.right_name
83
94
  if cte.quote_address.get(join.right_name, False):
trilogy/executor.py CHANGED
@@ -394,6 +394,9 @@ class Executor(object):
394
394
  if isinstance(rval, ListWrapper):
395
395
  return [x for x in rval]
396
396
  if isinstance(rval, MapWrapper):
397
+ # duckdb expects maps in this format as variables
398
+ if self.dialect == Dialects.DUCK_DB:
399
+ return {"key": [x for x in rval], "value": [rval[x] for x in rval]}
397
400
  return {k: v for k, v in rval.items()}
398
401
  # if isinstance(rval, ConceptRef):
399
402
  # return self._concept_to_value(self.environment.concepts[rval.address], local_concepts=local_concepts)
trilogy/parsing/common.py CHANGED
@@ -24,6 +24,7 @@ from trilogy.core.models.author import (
24
24
  AlignClause,
25
25
  AlignItem,
26
26
  Concept,
27
+ ConceptArgs,
27
28
  ConceptRef,
28
29
  FilterItem,
29
30
  Function,
@@ -191,16 +192,52 @@ def constant_to_concept(
191
192
  )
192
193
 
193
194
 
195
+ def atom_is_relevant(
196
+ atom,
197
+ others: list[Concept | ConceptRef],
198
+ environment: Environment | None = None,
199
+ ):
200
+ if isinstance(atom, (ConceptRef, Concept)):
201
+ # when we are looking at atoms, if there is a concept that is in others
202
+ # return directly
203
+ if atom.address in others:
204
+ return False
205
+ return concept_is_relevant(atom, others, environment)
206
+
207
+ if isinstance(atom, AggregateWrapper) and not atom.by:
208
+ return False
209
+ elif isinstance(atom, AggregateWrapper):
210
+ return any(atom_is_relevant(x, others, environment) for x in atom.by)
211
+
212
+ if isinstance(atom, Function):
213
+ relevant = False
214
+ for arg in atom.arguments:
215
+ relevant = relevant or atom_is_relevant(arg, others, environment)
216
+ return relevant
217
+ elif isinstance(atom, FunctionCallWrapper):
218
+ return any(
219
+ [atom_is_relevant(atom.content, others, environment)]
220
+ + [atom_is_relevant(x, others, environment) for x in atom.args]
221
+ )
222
+ elif isinstance(atom, ConceptArgs):
223
+ # use atom is relevant here to trigger the early exit behavior for concpets in set
224
+ return any(
225
+ [atom_is_relevant(x, others, environment) for x in atom.concept_arguments]
226
+ )
227
+ return False
228
+
229
+
194
230
  def concept_is_relevant(
195
231
  concept: Concept | ConceptRef,
196
232
  others: list[Concept | ConceptRef],
197
233
  environment: Environment | None = None,
198
234
  ) -> bool:
199
- if isinstance(concept, UndefinedConcept):
200
235
 
236
+ if isinstance(concept, UndefinedConcept):
201
237
  return False
202
238
  if concept.datatype == DataType.UNKNOWN:
203
239
  return False
240
+
204
241
  if isinstance(concept, ConceptRef):
205
242
  if environment:
206
243
  concept = environment.concepts[concept.address]
@@ -208,41 +245,56 @@ def concept_is_relevant(
208
245
  raise SyntaxError(
209
246
  "Require environment to determine relevance of ConceptRef"
210
247
  )
211
-
248
+ if concept.derivation == Derivation.CONSTANT:
249
+ return False
212
250
  if concept.is_aggregate and not (
213
251
  isinstance(concept.lineage, AggregateWrapper) and concept.lineage.by
214
252
  ):
215
253
 
216
254
  return False
217
255
  if concept.purpose in (Purpose.PROPERTY, Purpose.METRIC) and concept.keys:
218
- if any([c in others for c in concept.keys]):
219
-
256
+ if all([c in others for c in concept.keys]):
220
257
  return False
258
+ if (
259
+ concept.purpose == Purpose.KEY
260
+ and concept.keys
261
+ and all([c in others for c in concept.keys])
262
+ ):
263
+ return False
221
264
  if concept.purpose in (Purpose.METRIC,):
222
265
  if all([c in others for c in concept.grain.components]):
223
266
  return False
224
- if concept.derivation in (Derivation.BASIC,):
225
- return any(
226
- concept_is_relevant(c, others, environment)
227
- for c in concept.concept_arguments
228
- )
267
+ if concept.derivation in (Derivation.BASIC,) and isinstance(
268
+ concept.lineage, Function
269
+ ):
270
+ relevant = False
271
+ for arg in concept.lineage.arguments:
272
+ relevant = atom_is_relevant(arg, others, environment) or relevant
273
+ return relevant
229
274
  if concept.granularity == Granularity.SINGLE_ROW:
230
275
  return False
231
276
  return True
232
277
 
233
278
 
234
279
  def concepts_to_grain_concepts(
235
- concepts: Iterable[Concept | ConceptRef | str], environment: Environment | None
280
+ concepts: Iterable[Concept | ConceptRef | str],
281
+ environment: Environment | None,
282
+ local_concepts: dict[str, Concept] | None = None,
236
283
  ) -> list[Concept]:
237
284
  pconcepts: list[Concept] = []
238
285
  for c in concepts:
239
-
240
286
  if isinstance(c, Concept):
241
287
  pconcepts.append(c)
242
288
  elif isinstance(c, ConceptRef) and environment:
243
- pconcepts.append(environment.concepts[c.address])
289
+ if local_concepts and c.address in local_concepts:
290
+ pconcepts.append(local_concepts[c.address])
291
+ else:
292
+ pconcepts.append(environment.concepts[c.address])
244
293
  elif isinstance(c, str) and environment:
245
- pconcepts.append(environment.concepts[c])
294
+ if local_concepts and c in local_concepts:
295
+ pconcepts.append(local_concepts[c])
296
+ else:
297
+ pconcepts.append(environment.concepts[c])
246
298
  else:
247
299
  raise ValueError(
248
300
  f"Unable to resolve input {c} without environment provided to concepts_to_grain call"
@@ -250,6 +302,7 @@ def concepts_to_grain_concepts(
250
302
 
251
303
  final: List[Concept] = []
252
304
  for sub in pconcepts:
305
+
253
306
  if not concept_is_relevant(sub, pconcepts, environment): # type: ignore
254
307
  continue
255
308
  final.append(sub)
@@ -366,7 +419,12 @@ def function_to_concept(
366
419
  is_metric = False
367
420
  ref_args, is_metric = get_relevant_parent_concepts(parent)
368
421
  concrete_args = [environment.concepts[c.address] for c in ref_args]
369
- pkeys += [x for x in concrete_args if not x.derivation == Derivation.CONSTANT]
422
+ pkeys += [
423
+ x
424
+ for x in concrete_args
425
+ if not x.derivation == Derivation.CONSTANT
426
+ and not (x.derivation == Derivation.AGGREGATE and not x.grain.components)
427
+ ]
370
428
  grain: Grain | None = Grain()
371
429
  for x in pkeys:
372
430
  grain += x.grain
@@ -376,7 +434,7 @@ def function_to_concept(
376
434
  modifiers = get_upstream_modifiers(pkeys, environment)
377
435
  key_grain: list[str] = []
378
436
  for x in pkeys:
379
- # metrics will group to keys, so do no do key traversal
437
+ # metrics will group to keys, so do not do key traversal
380
438
  if is_metric:
381
439
  key_grain.append(x.address)
382
440
  # otherwse, for row ops, assume keys are transitive
@@ -419,7 +477,6 @@ def function_to_concept(
419
477
  else:
420
478
  derivation = Derivation.BASIC
421
479
  granularity = Granularity.MULTI_ROW
422
-
423
480
  if grain is not None:
424
481
  r = Concept(
425
482
  name=name,
@@ -1,2 +1,8 @@
1
1
  class ParseError(Exception):
2
2
  pass
3
+
4
+
5
+ class NameShadowError(ParseError):
6
+ """
7
+ Raised when a name shadows another name in the same scope.
8
+ """
@@ -117,7 +117,6 @@ from trilogy.core.statements.author import (
117
117
  CopyStatement,
118
118
  FunctionDeclaration,
119
119
  ImportStatement,
120
- KeyMergeStatement,
121
120
  Limit,
122
121
  MergeStatementV2,
123
122
  MultiSelectStatement,
@@ -136,7 +135,7 @@ from trilogy.parsing.common import (
136
135
  process_function_args,
137
136
  rowset_to_concepts,
138
137
  )
139
- from trilogy.parsing.exceptions import ParseError
138
+ from trilogy.parsing.exceptions import NameShadowError, ParseError
140
139
 
141
140
  perf_logger = getLogger("trilogy.parse.performance")
142
141
 
@@ -814,6 +813,24 @@ class ParseToObjects(Transformer):
814
813
  )
815
814
  if self.parse_pass == ParsePass.VALIDATION:
816
815
  self.environment.add_datasource(datasource, meta=meta)
816
+ # if we have any foreign keys on the datasource, we can
817
+ # at this point optimize them to properties if they do not have other usage.
818
+ for column in columns:
819
+ # skip partial for now
820
+ if not grain:
821
+ continue
822
+ if column.concept.address in grain.components:
823
+ continue
824
+ target_c = self.environment.concepts[column.concept.address]
825
+ if target_c.purpose != Purpose.KEY:
826
+ continue
827
+
828
+ key_inputs = grain.components
829
+ keys = [self.environment.concepts[grain] for grain in key_inputs]
830
+ # target_c.purpose = Purpose.PROPERTY
831
+ target_c.keys = set([x.address for x in keys])
832
+ # target_c.grain = Grain(components={x.address for x in keys})
833
+
817
834
  return datasource
818
835
 
819
836
  @v_args(meta=True)
@@ -903,29 +920,6 @@ class ParseToObjects(Transformer):
903
920
  def over_list(self, args):
904
921
  return [x for x in args]
905
922
 
906
- @v_args(meta=True)
907
- def key_merge_statement(self, meta: Meta, args) -> KeyMergeStatement | None:
908
- key_inputs = args[:-1]
909
- target = args[-1]
910
- keys = [self.environment.concepts[grain] for grain in key_inputs]
911
- target_c = self.environment.concepts[target]
912
- new = KeyMergeStatement(
913
- keys=set([x.address for x in keys]),
914
- target=target_c.reference,
915
- )
916
- internal = Concept(
917
- name="_" + target_c.address.replace(".", "_"),
918
- namespace=self.environment.namespace,
919
- purpose=Purpose.PROPERTY,
920
- keys=set([x.address for x in keys]),
921
- datatype=target_c.datatype,
922
- grain=Grain(components={x.address for x in keys}),
923
- )
924
- self.environment.add_concept(internal)
925
- # always a full merge
926
- self.environment.merge_concept(target_c, internal, [])
927
- return new
928
-
929
923
  @v_args(meta=True)
930
924
  def merge_statement(self, meta: Meta, args) -> MergeStatementV2 | None:
931
925
  modifiers = []
@@ -1253,9 +1247,18 @@ class ParseToObjects(Transformer):
1253
1247
  ):
1254
1248
  intersection = base.locally_derived.intersection(pre_keys)
1255
1249
  if intersection:
1256
- raise ParseError(
1257
- f"Select statement {base} creates new derived concepts {list(intersection)} from transformations with identical name(s) to existing concept(s). Do you mean to drop the calculation and directly use the existing concept? If not, alias these concept(s) under new names."
1258
- )
1250
+ for x in intersection:
1251
+ if (
1252
+ base.local_concepts[x].derivation
1253
+ == self.environment.concepts[x].derivation
1254
+ ):
1255
+ raise NameShadowError(
1256
+ f"Select statement {base} derives concept {x} with identical derivation as named concept. Use the named concept directly."
1257
+ )
1258
+ else:
1259
+ raise NameShadowError(
1260
+ f"Select statement {base} creates new derived concepts {list(intersection)} with identical name(s) to existing concept(s). If these are identical, reference the concept directly. Otherwise alias your column as a new name."
1261
+ )
1259
1262
  return base
1260
1263
 
1261
1264
  @v_args(meta=True)
@@ -1792,7 +1795,7 @@ class ParseToObjects(Transformer):
1792
1795
  def fyear(self, meta, args):
1793
1796
  return self.function_factory.create_function(args, FunctionType.YEAR, meta)
1794
1797
 
1795
- def internal_fcast(self, meta, args):
1798
+ def internal_fcast(self, meta, args) -> Function:
1796
1799
  args = process_function_args(args, meta=meta, environment=self.environment)
1797
1800
  if isinstance(args[0], str):
1798
1801
  processed: date | datetime | int | float | bool | str
@@ -1854,6 +1857,8 @@ class ParseToObjects(Transformer):
1854
1857
 
1855
1858
  @v_args(meta=True)
1856
1859
  def fround(self, meta, args) -> Function:
1860
+ if len(args) == 1:
1861
+ args.append(0)
1857
1862
  return self.function_factory.create_function(args, FunctionType.ROUND, meta)
1858
1863
 
1859
1864
  @v_args(meta=True)
@@ -10,7 +10,6 @@
10
10
  | rowset_derivation_statement
11
11
  | import_statement
12
12
  | copy_statement
13
- | key_merge_statement
14
13
  | merge_statement
15
14
  | rawsql_statement
16
15
 
@@ -77,8 +76,6 @@
77
76
 
78
77
  align_clause: align_item ("AND"i align_item)* "AND"i?
79
78
 
80
- key_merge_statement: "merge"i "property"i "<" IDENTIFIER ("," IDENTIFIER )* ","? ">" "from"i IDENTIFIER
81
-
82
79
  merge_statement: "merge"i WILDCARD_IDENTIFIER "into"i SHORTHAND_MODIFIER? WILDCARD_IDENTIFIER
83
80
 
84
81
  // raw sql statement
@@ -223,7 +220,7 @@
223
220
  fdiv: ( "divide"i "(" expr "," expr ")")
224
221
  fmod: ( "mod"i "(" expr "," (int_lit | concept_lit ) ")")
225
222
  _ROUND.1: "round"i "("
226
- fround: _ROUND expr "," expr ")"
223
+ fround: _ROUND expr ("," expr)? ")"
227
224
  fabs: "abs"i "(" expr ")"
228
225
  _SQRT.1: "sqrt("
229
226
  fsqrt: _SQRT expr ")"
@@ -1,35 +0,0 @@
1
- from trilogy.core.enums import Derivation
2
- from trilogy.core.models.build import BuildConcept
3
- from trilogy.core.models.execute import (
4
- CTE,
5
- UnionCTE,
6
- )
7
- from trilogy.core.optimizations.base_optimization import OptimizationRule
8
-
9
-
10
- class InlineConstant(OptimizationRule):
11
- def optimize(
12
- self, cte: CTE | UnionCTE, inverse_map: dict[str, list[CTE | UnionCTE]]
13
- ) -> bool:
14
- if isinstance(cte, UnionCTE):
15
- return any(self.optimize(x, inverse_map) for x in cte.internal_ctes)
16
-
17
- to_inline: list[BuildConcept] = []
18
- for x in cte.source.input_concepts:
19
- if x.address not in cte.source_map:
20
- continue
21
- if x.derivation == Derivation.CONSTANT:
22
- self.log(f"Found constant {x.address} on {cte.name}")
23
- to_inline.append(x)
24
- if to_inline:
25
- inlined = False
26
- for c in to_inline:
27
- self.log(f"Attempting to inline constant {c.address} on {cte.name}")
28
- test = cte.inline_constant(c)
29
- if test:
30
- self.log(f"Successfully inlined constant {c.address} to {cte.name}")
31
- inlined = True
32
- else:
33
- self.log(f"Could not inline constant to {cte.name}")
34
- return inlined
35
- return False