pytrilogy 0.0.2.2__py3-none-any.whl → 0.0.2.4__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 (31) hide show
  1. {pytrilogy-0.0.2.2.dist-info → pytrilogy-0.0.2.4.dist-info}/METADATA +3 -3
  2. {pytrilogy-0.0.2.2.dist-info → pytrilogy-0.0.2.4.dist-info}/RECORD +31 -31
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/enums.py +2 -0
  5. trilogy/core/env_processor.py +5 -0
  6. trilogy/core/functions.py +39 -6
  7. trilogy/core/models.py +81 -9
  8. trilogy/core/optimization.py +46 -31
  9. trilogy/core/optimizations/predicate_pushdown.py +33 -8
  10. trilogy/core/processing/concept_strategies_v3.py +10 -0
  11. trilogy/core/processing/node_generators/basic_node.py +1 -1
  12. trilogy/core/processing/node_generators/common.py +3 -3
  13. trilogy/core/processing/node_generators/filter_node.py +20 -16
  14. trilogy/core/processing/node_generators/node_merge_node.py +46 -108
  15. trilogy/core/processing/nodes/group_node.py +28 -2
  16. trilogy/core/processing/utility.py +56 -32
  17. trilogy/core/query_processor.py +10 -3
  18. trilogy/dialect/base.py +62 -88
  19. trilogy/dialect/bigquery.py +3 -1
  20. trilogy/dialect/duckdb.py +5 -4
  21. trilogy/dialect/postgres.py +3 -1
  22. trilogy/dialect/presto.py +4 -1
  23. trilogy/dialect/snowflake.py +3 -1
  24. trilogy/dialect/sql_server.py +3 -1
  25. trilogy/parsing/common.py +5 -1
  26. trilogy/parsing/parse_engine.py +94 -6
  27. trilogy/parsing/trilogy.lark +11 -4
  28. {pytrilogy-0.0.2.2.dist-info → pytrilogy-0.0.2.4.dist-info}/LICENSE.md +0 -0
  29. {pytrilogy-0.0.2.2.dist-info → pytrilogy-0.0.2.4.dist-info}/WHEEL +0 -0
  30. {pytrilogy-0.0.2.2.dist-info → pytrilogy-0.0.2.4.dist-info}/entry_points.txt +0 -0
  31. {pytrilogy-0.0.2.2.dist-info → pytrilogy-0.0.2.4.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.2
3
+ Version: 0.0.2.4
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -247,7 +247,7 @@ N/A, only supports default auth. In python you can pass in a custom client.
247
247
 
248
248
 
249
249
  > [!TIP]
250
- > The CLI can also be used for formatting. PreQL has a default formatting style that should always be adhered to. `trilogy fmt <path to trilogy file>`
250
+ > The CLI can also be used for formatting. Trilogy has a default formatting style that should always be adhered to. `trilogy fmt <path to trilogy file>`
251
251
 
252
252
 
253
253
  ## More Examples
@@ -284,7 +284,7 @@ but all are worth checking out. Please open PRs/comment for anything missed!
284
284
 
285
285
  #### CONCEPT
286
286
 
287
- Types: `string | int | float | bool | date | datetime | time | timestamp | interval`;
287
+ Types: `string | int | float | bool | date | datetime | time | numeric(scale, precision) | timestamp | interval`;
288
288
 
289
289
  Key:
290
290
  `key <name> <type>;`
@@ -1,4 +1,4 @@
1
- trilogy/__init__.py,sha256=mSeU_j02wb5aKd5vDcHQJdZaoG1AB96bCjUn2tIVpwk,290
1
+ trilogy/__init__.py,sha256=sspl5Npt0npzVRC6wCwiYXa0nK0E7a_biwiL2T4kbjM,290
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=KIvi-cgU4R9urNgDdGiCsRkCrzjAfM4xGHhZb1SVy2w,881
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
@@ -8,34 +8,34 @@ trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
11
- trilogy/core/enums.py,sha256=DWKPuShM_DbyAenjNFOAxcIH2l0QC9WgIwqZKPQqk_o,5850
12
- trilogy/core/env_processor.py,sha256=Wpy-iiduBwHntTsQTYWBO1O0i3Ij9_VoL3d4IWDEoj4,2126
11
+ trilogy/core/enums.py,sha256=ozgw8G0s4CdbL6IUsX5nNT_ZGd4mMkoo8_EGLZXeQYY,5902
12
+ trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
13
13
  trilogy/core/environment_helpers.py,sha256=mzBDHhdF9ssZ_-LY8CcaM_ddfJavkpRYrFImUd3cjXI,5972
14
14
  trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
15
15
  trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
- trilogy/core/functions.py,sha256=xISGHMiUR9hFyoZe2l3VLTMO6UEtyGHQcMuovzlVRqw,9492
16
+ trilogy/core/functions.py,sha256=ARJAyBjeS415-54k3G_bx807rkPZonEulMaLRxSP7vU,10371
17
17
  trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=4iwlTfHzzmbDhm9vt7zgExMqY1xKyQL-fVG5Nrm-YTU,134722
20
- trilogy/core/optimization.py,sha256=iX8BCh2BIEPJ6cYlcn7piDKFfig47azphhyqkwkYJM4,4291
21
- trilogy/core/query_processor.py,sha256=PROehLEOMOuKy1EAUeIgdfGq1LrJsX0N7Z2qDED6S30,17580
19
+ trilogy/core/models.py,sha256=KVPUfupZVhQxJZCViMP7PELzvNR7WOwkjmqqgEM4oeM,137120
20
+ trilogy/core/optimization.py,sha256=A8S9C9H5RcQcFSQLYtEEBnm-r1CW_e9GEWlLK7q3MqA,4930
21
+ trilogy/core/query_processor.py,sha256=6JiX6YqsTgnsEkIxcmWFxJuM6kRJbXbPGRCQgi8g-B8,17941
22
22
  trilogy/core/optimizations/__init__.py,sha256=pxRzNzd2g8oRMy4f_ub5va6bNS2pd4hnyp9JBzTKc1E,300
23
23
  trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
24
24
  trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
25
25
  trilogy/core/optimizations/inline_datasource.py,sha256=KiwZ4fnRnSOVK8zzdwMAkafszo3fNn5LY8xgb0X1CbI,3194
26
- trilogy/core/optimizations/predicate_pushdown.py,sha256=mduJn4zzKmLVNUja60ZlY6LlNp389OwjEbq2Rj8K6co,5389
26
+ trilogy/core/optimizations/predicate_pushdown.py,sha256=iVZV_BVaL3I6wlELPOuJAiuF0heOM7bCUnNqyCxxRVw,6370
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- trilogy/core/processing/concept_strategies_v3.py,sha256=hmbck3a23N61HfMwC2z3lxA6Yms9TP7Lm3Aw7GiDcMA,23765
28
+ trilogy/core/processing/concept_strategies_v3.py,sha256=xCPfg1ySHP6C-u777kqnV8eqIK1X68hqwvRcw61q5X0,23968
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=hgfPfz5FlghN8edIVGTQ21eDUof5EMvnl3Vu64hjfqY,13289
30
+ trilogy/core/processing/utility.py,sha256=rhnkn3KgB1MW17mZYSEZ8XFEbC__B7PPmZgpDql4Myg,13614
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
- trilogy/core/processing/node_generators/basic_node.py,sha256=uD6noJOgrtmhjb-1aa-vD8sQ97s9Tya6wbso7V_AdyE,2819
33
- trilogy/core/processing/node_generators/common.py,sha256=liZDth7mvhkF_sUFXK7JitJsiaKD132w3ySLbF7l-nE,8956
34
- trilogy/core/processing/node_generators/filter_node.py,sha256=5B7UCK84A9lGgJ7EjCiC7YGWWM1xPyQGHkBwZgYypFM,4585
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=4242PNGTCm2tklqMIkqVu5Iv4m_IeTnOYXxDveuCDZM,2856
33
+ trilogy/core/processing/node_generators/common.py,sha256=uLrqbEsuYREswiTfuTQuxldO9SsS6ylZEQT_q9iB2uY,8948
34
+ trilogy/core/processing/node_generators/filter_node.py,sha256=kVsK9O9QWUgknui3ESHJJCnFY9dVMCHlGpHXn1QtXkU,4824
35
35
  trilogy/core/processing/node_generators/group_node.py,sha256=xWI1xNIXEOj6jlRGD9hcv2_vVNvY6lpzJl6pQ8HuFBE,2988
36
36
  trilogy/core/processing/node_generators/group_to_node.py,sha256=BzPdYwzoo8gRMH7BDffTTXq4z-mjfCEzvfB5I-P0_nw,2941
37
37
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=vP84dnLQy6dtypi6mUbt9sMAcmmrTgQ1Oz4GI6X1IEo,6421
38
- trilogy/core/processing/node_generators/node_merge_node.py,sha256=YivTVrTEUJuQbQAHFVB2lUD4mJYQ-kTVVpwY4oT9A-Y,14664
38
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=JDQEIs43BJCgz0yofztcjd7J20Ybk4W40IJG_6blQJI,12168
39
39
  trilogy/core/processing/node_generators/rowset_node.py,sha256=2ROd2c1_o4h5sbnDdaiL1o9UiiwyYw-B6OKyfHYNe0A,6188
40
40
  trilogy/core/processing/node_generators/select_node.py,sha256=XGcz74XsWv5POWhV2gGC_jbi7T9g7sY-VtP3sjlppFc,19057
41
41
  trilogy/core/processing/node_generators/unnest_node.py,sha256=6CH66eGwpadNX7TzUhWZ8aqIisOtQeHINbLV6X3QBUk,1779
@@ -43,40 +43,40 @@ trilogy/core/processing/node_generators/window_node.py,sha256=9nXUXUgQrNczU1gaOq
43
43
  trilogy/core/processing/nodes/__init__.py,sha256=-8ONG7Nqa6sNLpst5wNNyMigyaHRfUKGdE3xce8E3mM,3998
44
44
  trilogy/core/processing/nodes/base_node.py,sha256=q1G4FO3Q4OuboWL8EaJgJ4jcnnqM9QsothMIcF2ot1g,11161
45
45
  trilogy/core/processing/nodes/filter_node.py,sha256=ImUHYj5G5eHbOtki-ZMR31Op38wKOjR1xu_ETiE_D6I,1932
46
- trilogy/core/processing/nodes/group_node.py,sha256=5dqGlonXG5Ou7HRSsTStbM1KyxeDSZROz9qKTjOUMEc,5166
46
+ trilogy/core/processing/nodes/group_node.py,sha256=j3vtFQIn_t4EfCke8dGULlwVkG2KKPYfgGwo2rvgcww,6322
47
47
  trilogy/core/processing/nodes/merge_node.py,sha256=W7ryL9rceylCQGNgcuGY-dSiNoIz8qrbpJXKzg_Niwo,14056
48
48
  trilogy/core/processing/nodes/select_node_v2.py,sha256=COu-WPuyabGAc3HTkJB-_7eNzR4OgxIL4YDuZIRrST0,7179
49
49
  trilogy/core/processing/nodes/unnest_node.py,sha256=JFtm90IVM-46aCYkTNIaJah6v9ApAfonjVhcVM1HmDE,1903
50
50
  trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
51
51
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
- trilogy/dialect/base.py,sha256=EZCP3xU8-B4xpU1pFFew4CGtkNWignbyPll6OTENFLg,29832
53
- trilogy/dialect/bigquery.py,sha256=BAN2o0SOuLvzLYLV7Sc11woapS5L7Cc36aMBeaphW5k,2905
52
+ trilogy/dialect/base.py,sha256=aE8iJJhkGtZE04mYm_wQx3ebQKYbcRsKISUd9mbrZ2A,28782
53
+ trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
54
54
  trilogy/dialect/common.py,sha256=5jdOHWIj3Xv8F8y5mnyWHLjxD_we2fncM-ZnNetJP7U,2781
55
55
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
56
- trilogy/dialect/duckdb.py,sha256=JQPnR5F39iDQXfTccKhbq7xWYGzYc0dPpn5vo0Qd_Vk,3076
56
+ trilogy/dialect/duckdb.py,sha256=JEdhctDxZr-FEodot5V4De6JnrQDJT02zbjX48j6ay8,3125
57
57
  trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
58
- trilogy/dialect/postgres.py,sha256=r47xbCA7nfEYENofiVfLZ-SnReNfDmUmW4OSHVkkP4E,3206
59
- trilogy/dialect/presto.py,sha256=ouSVNbs7e3eEc20emLfxKyRjdINSibPGUFL4X9OwboA,3163
60
- trilogy/dialect/snowflake.py,sha256=N3HknYgN-fjD7BLX1Ucj-ss_ku2Ox8DgLsF3BIHutHo,2941
61
- trilogy/dialect/sql_server.py,sha256=HX68vNTrcDaTnOxe6Zbx_PBgrO42e2VuThxO6CYQ2cY,3026
58
+ trilogy/dialect/postgres.py,sha256=ev1RJZsC8BB3vJSxJ4q-TTYqZ4Hk1NXUtuRkLrQEBX0,3254
59
+ trilogy/dialect/presto.py,sha256=InvHDXsGLDG3E3ffl7hGZ-9y3vH8RO6UhNE0OSra01g,3269
60
+ trilogy/dialect/snowflake.py,sha256=_Bf4XO7-nImMv9XCSsTfVM3g2f_KHdO17VTa9J-HgSM,2989
61
+ trilogy/dialect/sql_server.py,sha256=OtXbm1v6NIGyXeC5i18ojUvLeqescc_Pbv3EASUBB94,3074
62
62
  trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
64
64
  trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2467
65
65
  trilogy/hooks/query_debugger.py,sha256=NDChfkPmmW-KINa4TaQmDe_adGiwsKFdGLDSYpbodeU,4282
66
66
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
67
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- trilogy/parsing/common.py,sha256=8MDRPccWymtVkHVQMzESjL5tOH79flWU5jo6Ys-C5UQ,5963
68
+ trilogy/parsing/common.py,sha256=aYmLtsEk8VJj5yYASzUcTf0i0lXVsZBbZJwphwokxK4,6092
69
69
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
70
70
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
71
71
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
72
- trilogy/parsing/parse_engine.py,sha256=rjTDs8AhJ6NjPCugfZCTdu7d_bUOSC9M8MAl9KmURNw,57763
72
+ trilogy/parsing/parse_engine.py,sha256=_Cpq7bq6QyuDcnmYX_fDLGNFNSQPj173cqYsh3V6jH8,60969
73
73
  trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
74
- trilogy/parsing/trilogy.lark,sha256=Zn5UHqiNoVKa76r1af9KND0TNdsLIeHpyG3hJjUOOTQ,11000
74
+ trilogy/parsing/trilogy.lark,sha256=uQlyb4RQ6Tpp8lRlXLK_Qb8sD1qzYVYErRI97v22Zak,11317
75
75
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
77
- pytrilogy-0.0.2.2.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
- pytrilogy-0.0.2.2.dist-info/METADATA,sha256=rjwj8b_CWxQc82n5HJfPTBYlhtVKNUwIhAkWLusfA7g,7876
79
- pytrilogy-0.0.2.2.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
80
- pytrilogy-0.0.2.2.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
- pytrilogy-0.0.2.2.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
- pytrilogy-0.0.2.2.dist-info/RECORD,,
77
+ pytrilogy-0.0.2.4.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
+ pytrilogy-0.0.2.4.dist-info/METADATA,sha256=R3D4ULv2Y2yJRO0sToJ5FlPPiqkU8H02X035M-vaG9s,7906
79
+ pytrilogy-0.0.2.4.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
80
+ pytrilogy-0.0.2.4.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
+ pytrilogy-0.0.2.4.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
+ pytrilogy-0.0.2.4.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.2"
7
+ __version__ = "0.0.2.4"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -119,7 +119,9 @@ class FunctionType(Enum):
119
119
 
120
120
  # COMPLEX
121
121
  INDEX_ACCESS = "index_access"
122
+ MAP_ACCESS = "map_access"
122
123
  ATTR_ACCESS = "attr_access"
124
+ STRUCT = "struct"
123
125
 
124
126
  # TEXT AND MAYBE MORE
125
127
  SPLIT = "split"
@@ -17,6 +17,11 @@ def add_concept(concept: Concept, g: ReferenceGraph):
17
17
  for _, pseudonym in concept.pseudonyms.items():
18
18
  pseudonym = pseudonym.with_default_grain()
19
19
  pseudonym_node = concept_to_node(pseudonym)
20
+ if (pseudonym_node, node_name) in g.edges and (
21
+ node_name,
22
+ pseudonym_node,
23
+ ) in g.edges:
24
+ continue
20
25
  if pseudonym_node.split("@")[0] == node_name.split("@")[0]:
21
26
  continue
22
27
  g.add_edge(pseudonym_node, node_name, pseudonym=True)
trilogy/core/functions.py CHANGED
@@ -202,15 +202,21 @@ def Split(args: list[Concept]) -> Function:
202
202
  )
203
203
 
204
204
 
205
+ def get_index_output_type(
206
+ arg: Concept,
207
+ ) -> DataType | StructType | MapType | ListType | NumericType:
208
+ if isinstance(arg.datatype, ListType):
209
+ return arg.datatype.value_data_type
210
+ elif isinstance(arg.datatype, MapType):
211
+ return arg.datatype.value_data_type
212
+ return arg.datatype
213
+
214
+
205
215
  def IndexAccess(args: list[Concept]):
206
216
  return Function(
207
217
  operator=FunctionType.INDEX_ACCESS,
208
218
  arguments=args,
209
- output_datatype=(
210
- args[0].datatype.value_data_type
211
- if isinstance(args[0].datatype, ListType)
212
- else args[0].datatype
213
- ),
219
+ output_datatype=get_index_output_type(args[0]),
214
220
  output_purpose=Purpose.PROPERTY,
215
221
  valid_inputs=[
216
222
  {
@@ -226,11 +232,38 @@ def IndexAccess(args: list[Concept]):
226
232
  )
227
233
 
228
234
 
235
+ def MapAccess(args: list[Concept]):
236
+ return Function(
237
+ operator=FunctionType.MAP_ACCESS,
238
+ arguments=args,
239
+ output_datatype=get_index_output_type(args[0]),
240
+ output_purpose=Purpose.PROPERTY,
241
+ valid_inputs=[
242
+ {
243
+ DataType.MAP,
244
+ },
245
+ {
246
+ DataType.INTEGER,
247
+ DataType.STRING,
248
+ },
249
+ ],
250
+ arg_count=2,
251
+ )
252
+
253
+
254
+ def get_attr_datatype(
255
+ arg: Concept, lookup
256
+ ) -> DataType | ListType | StructType | MapType | NumericType:
257
+ if isinstance(arg.datatype, StructType):
258
+ return arg_to_datatype(arg.datatype.fields_map[lookup])
259
+ return arg.datatype
260
+
261
+
229
262
  def AttrAccess(args: list[Concept]):
230
263
  return Function(
231
264
  operator=FunctionType.ATTR_ACCESS,
232
265
  arguments=args,
233
- output_datatype=args[0].field_map[args[1]].datatype, # type: ignore
266
+ output_datatype=get_attr_datatype(args[0], args[1]), # type: ignore
234
267
  output_purpose=Purpose.PROPERTY,
235
268
  valid_inputs=[
236
269
  {DataType.STRUCT},
trilogy/core/models.py CHANGED
@@ -67,7 +67,7 @@ from trilogy.core.enums import (
67
67
  )
68
68
  from trilogy.core.exceptions import UndefinedConceptException, InvalidSyntaxException
69
69
  from trilogy.utility import unique
70
- from collections import UserList
70
+ from collections import UserList, UserDict
71
71
  from functools import cached_property
72
72
  from abc import ABC
73
73
 
@@ -267,7 +267,7 @@ class ListType(BaseModel):
267
267
 
268
268
  class MapType(BaseModel):
269
269
  key_type: DataType
270
- content_type: ALL_TYPES
270
+ value_type: ALL_TYPES
271
271
 
272
272
  @property
273
273
  def data_type(self):
@@ -277,10 +277,26 @@ class MapType(BaseModel):
277
277
  def value(self):
278
278
  return self.data_type.value
279
279
 
280
+ @property
281
+ def value_data_type(
282
+ self,
283
+ ) -> DataType | StructType | MapType | ListType | NumericType:
284
+ if isinstance(self.value_type, Concept):
285
+ return self.value_type.datatype
286
+ return self.value_type
287
+
288
+ @property
289
+ def key_data_type(
290
+ self,
291
+ ) -> DataType | StructType | MapType | ListType | NumericType:
292
+ if isinstance(self.key_type, Concept):
293
+ return self.key_type.datatype
294
+ return self.key_type
295
+
280
296
 
281
297
  class StructType(BaseModel):
282
298
  fields: List[ALL_TYPES]
283
- fields_map: Dict[str, Concept] = Field(default_factory=dict)
299
+ fields_map: Dict[str, Concept | int | float | str] = Field(default_factory=dict)
284
300
 
285
301
  @property
286
302
  def data_type(self):
@@ -314,6 +330,34 @@ class ListWrapper(Generic[VT], UserList):
314
330
  return cls(v, type=arg_to_datatype(v[0]))
315
331
 
316
332
 
333
+ class MapWrapper(Generic[KT, VT], UserDict):
334
+ """Used to distinguish parsed map objects from other dicts"""
335
+
336
+ def __init__(self, *args, key_type: DataType, value_type: DataType, **kwargs):
337
+ super().__init__(*args, **kwargs)
338
+ self.key_type = key_type
339
+ self.value_type = value_type
340
+
341
+ @classmethod
342
+ def __get_pydantic_core_schema__(
343
+ cls, source_type: Any, handler: Callable[[Any], core_schema.CoreSchema]
344
+ ) -> core_schema.CoreSchema:
345
+ args = get_args(source_type)
346
+ if args:
347
+ schema = handler(Dict[args]) # type: ignore
348
+ else:
349
+ schema = handler(Dict)
350
+ return core_schema.no_info_after_validator_function(cls.validate, schema)
351
+
352
+ @classmethod
353
+ def validate(cls, v):
354
+ return cls(
355
+ v,
356
+ key_type=arg_to_datatype(list(v.keys()).pop()),
357
+ value_type=arg_to_datatype(list(v.values()).pop()),
358
+ )
359
+
360
+
317
361
  class Metadata(BaseModel):
318
362
  """Metadata container object.
319
363
  TODO: support arbitrary tags"""
@@ -375,6 +419,23 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
375
419
  def __hash__(self):
376
420
  return hash(str(self))
377
421
 
422
+ @property
423
+ def is_aggregate(self):
424
+ if (
425
+ self.lineage
426
+ and isinstance(self.lineage, Function)
427
+ and self.lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
428
+ ):
429
+ return True
430
+ if (
431
+ self.lineage
432
+ and isinstance(self.lineage, AggregateWrapper)
433
+ and self.lineage.function.operator
434
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
435
+ ):
436
+ return True
437
+ return False
438
+
378
439
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
379
440
  if self.address == source.address:
380
441
  new = target.with_grain(self.grain.with_merge(source, target, modifiers))
@@ -949,8 +1010,8 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
949
1010
  output_purpose: Purpose
950
1011
  valid_inputs: Optional[
951
1012
  Union[
952
- Set[DataType | ListType | StructType | NumericType],
953
- List[Set[DataType | ListType | StructType] | NumericType],
1013
+ Set[DataType | ListType | StructType | MapType | NumericType],
1014
+ List[Set[DataType | ListType | StructType | MapType | NumericType]],
954
1015
  ]
955
1016
  ] = None
956
1017
  arguments: Sequence[
@@ -961,17 +1022,17 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
961
1022
  int,
962
1023
  float,
963
1024
  str,
1025
+ MapWrapper[Any, Any],
964
1026
  DataType,
965
1027
  ListType,
1028
+ MapType,
966
1029
  NumericType,
967
1030
  DatePart,
968
1031
  "Parenthetical",
969
1032
  CaseWhen,
970
1033
  "CaseElse",
971
1034
  list,
972
- ListWrapper[int],
973
- ListWrapper[str],
974
- ListWrapper[float],
1035
+ ListWrapper[Any],
975
1036
  ]
976
1037
  ]
977
1038
 
@@ -2342,7 +2403,6 @@ class CTE(BaseModel):
2342
2403
  hidden_concepts: List[Concept] = Field(default_factory=list)
2343
2404
  order_by: Optional[OrderBy] = None
2344
2405
  limit: Optional[int] = None
2345
- requires_nesting: bool = True
2346
2406
  base_name_override: Optional[str] = None
2347
2407
  base_alias_override: Optional[str] = None
2348
2408
 
@@ -3141,6 +3201,7 @@ class Environment(BaseModel):
3141
3201
  v.pseudonyms[source.address] = source
3142
3202
  if v.address == source.address:
3143
3203
  replacements[k] = target
3204
+ v.pseudonyms[target.address] = target
3144
3205
  self.concepts.update(replacements)
3145
3206
 
3146
3207
  for k, ds in self.datasources.items():
@@ -4115,6 +4176,15 @@ def list_to_wrapper(args):
4115
4176
  return ListWrapper(args, type=types[0])
4116
4177
 
4117
4178
 
4179
+ def dict_to_map_wrapper(arg):
4180
+ key_types = [arg_to_datatype(arg) for arg in arg.keys()]
4181
+
4182
+ value_types = [arg_to_datatype(arg) for arg in arg.values()]
4183
+ assert len(set(key_types)) == 1
4184
+ assert len(set(key_types)) == 1
4185
+ return MapWrapper(arg, key_type=key_types[0], value_type=value_types[0])
4186
+
4187
+
4118
4188
  def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | NumericType:
4119
4189
  if isinstance(arg, Function):
4120
4190
  return arg.output_datatype
@@ -4143,5 +4213,7 @@ def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | Numeric
4143
4213
  elif isinstance(arg, list):
4144
4214
  wrapper = list_to_wrapper(arg)
4145
4215
  return ListType(type=wrapper.type)
4216
+ elif isinstance(arg, MapWrapper):
4217
+ return MapType(key_type=arg.key_type, value_type=arg.value_type)
4146
4218
  else:
4147
4219
  raise ValueError(f"Cannot parse arg datatype for arg of raw type {type(arg)}")
@@ -1,10 +1,10 @@
1
1
  from trilogy.core.models import (
2
2
  CTE,
3
3
  SelectStatement,
4
- PersistStatement,
5
4
  MultiSelectStatement,
5
+ Conditional,
6
6
  )
7
- from trilogy.core.enums import PurposeLineage
7
+ from trilogy.core.enums import PurposeLineage, BooleanOperator
8
8
  from trilogy.constants import logger, CONFIG
9
9
  from trilogy.core.optimizations import (
10
10
  OptimizationRule,
@@ -42,34 +42,45 @@ def gen_inverse_map(input: list[CTE]) -> dict[str, list[CTE]]:
42
42
  return inverse_map
43
43
 
44
44
 
45
- def is_direct_return_eligible(
46
- cte: CTE, select: SelectStatement | PersistStatement | MultiSelectStatement
47
- ) -> bool:
48
- if isinstance(select, (PersistStatement, MultiSelectStatement)):
49
- return False
45
+ def is_direct_return_eligible(cte: CTE) -> CTE | None:
46
+ # if isinstance(select, (PersistStatement, MultiSelectStatement)):
47
+ # return False
48
+ if len(cte.parent_ctes) != 1:
49
+ return None
50
+ direct_parent = cte.parent_ctes[0]
51
+
52
+ output_addresses = set([x.address for x in cte.output_columns])
53
+ parent_output_addresses = set([x.address for x in direct_parent.output_columns])
54
+ if not output_addresses.issubset(parent_output_addresses):
55
+ return None
56
+ if not direct_parent.grain == cte.grain:
57
+ return None
50
58
  derived_concepts = [
51
59
  c
52
60
  for c in cte.source.output_concepts + cte.source.hidden_concepts
53
61
  if c not in cte.source.input_concepts
54
62
  ]
55
- eligible = True
56
63
  conditions = (
57
- set(x.address for x in select.where_clause.concept_arguments)
58
- if select.where_clause
64
+ set(x.address for x in direct_parent.condition.concept_arguments)
65
+ if direct_parent.condition
59
66
  else set()
60
67
  )
61
68
  for x in derived_concepts:
62
69
  if x.derivation == PurposeLineage.WINDOW:
63
- return False
70
+ return None
64
71
  if x.derivation == PurposeLineage.UNNEST:
65
- return False
72
+ return None
66
73
  if x.derivation == PurposeLineage.AGGREGATE:
67
74
  if x.address in conditions:
68
- return False
75
+ return None
76
+ # handling top level nodes that require unpacking
77
+ for x in cte.output_columns:
78
+ if x.derivation == PurposeLineage.UNNEST:
79
+ return None
69
80
  logger.info(
70
- f"[Optimization][EarlyReturn] Upleveling output select to final CTE with derived_concepts {[x.address for x in derived_concepts]}"
81
+ f"[Optimization][EarlyReturn] Removing redundant output CTE with derived_concepts {[x.address for x in derived_concepts]}"
71
82
  )
72
- return eligible
83
+ return direct_parent
73
84
 
74
85
 
75
86
  def sort_select_output(cte: CTE, query: SelectStatement | MultiSelectStatement):
@@ -90,23 +101,27 @@ def optimize_ctes(
90
101
  input: list[CTE], root_cte: CTE, select: SelectStatement | MultiSelectStatement
91
102
  ) -> list[CTE]:
92
103
 
93
- if CONFIG.optimizations.direct_return and is_direct_return_eligible(
94
- root_cte, select
104
+ direct_parent: CTE | None = root_cte
105
+ while CONFIG.optimizations.direct_return and (
106
+ direct_parent := is_direct_return_eligible(root_cte)
95
107
  ):
96
- root_cte.order_by = select.order_by
97
- root_cte.limit = select.limit
98
- # if select.where_clause:
99
-
100
- # if root_cte.condition:
101
- # root_cte.condition = Conditional(
102
- # left=root_cte.condition,
103
- # operator=BooleanOperator.AND,
104
- # right=select.where_clause.conditional,
105
- # )
106
- # else:
107
- # root_cte.condition = select.where_clause.conditional
108
- root_cte.requires_nesting = False
109
- sort_select_output(root_cte, select)
108
+ direct_parent.order_by = root_cte.order_by
109
+ direct_parent.limit = root_cte.limit
110
+ direct_parent.hidden_concepts = (
111
+ root_cte.hidden_concepts + direct_parent.hidden_concepts
112
+ )
113
+ if root_cte.condition:
114
+ if direct_parent.condition:
115
+ direct_parent.condition = Conditional(
116
+ left=direct_parent.condition,
117
+ operator=BooleanOperator.AND,
118
+ right=root_cte.condition,
119
+ )
120
+ else:
121
+ direct_parent.condition = root_cte.condition
122
+ root_cte = direct_parent
123
+
124
+ sort_select_output(root_cte, select)
110
125
 
111
126
  REGISTERED_RULES: list["OptimizationRule"] = []
112
127
  if CONFIG.optimizations.constant_inlining:
@@ -3,18 +3,36 @@ from trilogy.core.models import (
3
3
  Conditional,
4
4
  BooleanOperator,
5
5
  Datasource,
6
+ SubselectComparison,
7
+ Comparison,
8
+ Parenthetical,
6
9
  )
7
10
  from trilogy.core.optimizations.base_optimization import OptimizationRule
11
+ from trilogy.core.processing.utility import is_scalar_condition
8
12
 
9
13
 
10
- def decompose_condition(conditional: Conditional):
11
- chunks = []
14
+ def decompose_condition(
15
+ conditional: Conditional,
16
+ ) -> list[SubselectComparison | Comparison | Conditional | Parenthetical]:
17
+ chunks: list[SubselectComparison | Comparison | Conditional | Parenthetical] = []
12
18
  if conditional.operator == BooleanOperator.AND:
13
- for val in [conditional.left, conditional.right]:
14
- if isinstance(val, Conditional):
15
- chunks.extend(decompose_condition(val))
16
- else:
17
- chunks.append(val)
19
+ if not (
20
+ isinstance(
21
+ conditional.left,
22
+ (SubselectComparison, Comparison, Conditional, Parenthetical),
23
+ )
24
+ and isinstance(
25
+ conditional.right,
26
+ (SubselectComparison, Comparison, Conditional, Parenthetical),
27
+ )
28
+ ):
29
+ chunks.append(conditional)
30
+ else:
31
+ for val in [conditional.left, conditional.right]:
32
+ if isinstance(val, Conditional):
33
+ chunks.extend(decompose_condition(val))
34
+ else:
35
+ chunks.append(val)
18
36
  else:
19
37
  chunks.append(conditional)
20
38
  return chunks
@@ -128,7 +146,14 @@ class PredicatePushdown(OptimizationRule):
128
146
  )
129
147
  optimized = False
130
148
  for candidate in candidates:
131
- self.debug(f"Checking candidate {candidate}")
149
+ if not is_scalar_condition(candidate):
150
+ self.debug(
151
+ f"Skipping {candidate} as not a basic [no aggregate, etc] condition"
152
+ )
153
+ continue
154
+ self.log(
155
+ f"Checking candidate {candidate}, {type(candidate)}, scalar: {is_scalar_condition(candidate)}"
156
+ )
132
157
  for parent_cte in cte.parent_ctes:
133
158
  local_pushdown = self._check_parent(
134
159
  parent_cte=parent_cte, candidate=candidate, inverse_map=inverse_map
@@ -336,8 +336,11 @@ def validate_concept(
336
336
  virtual_addresses: set[str],
337
337
  found_map: dict[str, set[Concept]],
338
338
  accept_partial: bool,
339
+ seen: set[str],
339
340
  ):
341
+
340
342
  found_map[str(node)].add(concept)
343
+ seen.add(concept.address)
341
344
  if concept not in node.partial_concepts:
342
345
 
343
346
  found_addresses.add(concept.address)
@@ -357,6 +360,8 @@ def validate_concept(
357
360
  for _, v in concept.pseudonyms.items():
358
361
  if v.address == concept.address:
359
362
  return
363
+ if v.address in seen:
364
+ return
360
365
  validate_concept(
361
366
  v,
362
367
  node,
@@ -366,6 +371,7 @@ def validate_concept(
366
371
  virtual_addresses,
367
372
  found_map,
368
373
  accept_partial,
374
+ seen=seen,
369
375
  )
370
376
 
371
377
 
@@ -379,8 +385,10 @@ def validate_stack(
379
385
  non_partial_addresses: set[str] = set()
380
386
  partial_addresses: set[str] = set()
381
387
  virtual_addresses: set[str] = set()
388
+ seen: set[str] = set()
382
389
  for node in stack:
383
390
  resolved = node.resolve()
391
+
384
392
  for concept in resolved.output_concepts:
385
393
  validate_concept(
386
394
  concept,
@@ -391,6 +399,7 @@ def validate_stack(
391
399
  virtual_addresses,
392
400
  found_map,
393
401
  accept_partial,
402
+ seen,
394
403
  )
395
404
  for concept in node.virtual_output_concepts:
396
405
  if concept.address in non_partial_addresses:
@@ -480,6 +489,7 @@ def _search_concepts(
480
489
 
481
490
  found: set[str] = set()
482
491
  skip: set[str] = set()
492
+ virtual: set[str] = set()
483
493
  stack: List[StrategyNode] = []
484
494
  complete = ValidationResult.INCOMPLETE
485
495
 
@@ -44,7 +44,7 @@ def gen_basic_node(
44
44
  list(optional_set) + [concept],
45
45
  )
46
46
  )
47
-
47
+ # check for the concept by itself
48
48
  for attempt, basic_output in reversed(attempts):
49
49
  partials = []
50
50
  attempt = unique(attempt, "address")