pytrilogy 0.0.3.115__py3-none-any.whl → 0.0.3.116__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.115
3
+ Version: 0.0.3.116
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Classifier: Programming Language :: Python
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,8 +1,8 @@
1
- pytrilogy-0.0.3.115.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=L11AizxIDnbCCjCvAE55VTsy0d4QmvAZfNp61uAPu00,304
3
- trilogy/constants.py,sha256=2ERigKZBmULg3Qr43nWKgdwqiCnT8XPQVeH-O-k66wc,2640
1
+ pytrilogy-0.0.3.116.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=4zCLL3ve6Z14PQ3LocAoJnyiTflxDn90gE8yUR-T9-w,304
3
+ trilogy/constants.py,sha256=_Tm7YGaAZuxH77X5ve0TajU0dQD7RcGV6ECrTHRL3qQ,2678
4
4
  trilogy/engine.py,sha256=v4TpNktM4zZ9OX7jZH2nde4dpX5uAH2U23ELfULTCSg,2280
5
- trilogy/executor.py,sha256=q3EsAjzgxNxPn-yTHD_FTFzm7bJ2mlf9CrJEjyt6-pE,17884
5
+ trilogy/executor.py,sha256=uKlCnPp4FHkgsa_dDcQJ4y-ObtvKat2KFx05c-z1mZo,17885
6
6
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
@@ -23,45 +23,45 @@ trilogy/ai/providers/utils.py,sha256=yttP6y2E_XzdytBCwhaKekfXfxM6gE6MRce4AtyLL60
23
23
  trilogy/authoring/__init__.py,sha256=TABMOETSMERrWuyDLR0nK4ISlqR0yaqeXrmuOdrSvAY,3060
24
24
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  trilogy/core/constants.py,sha256=nizWYDCJQ1bigQMtkNIEMNTcN0NoEAXiIHLzpelxQ24,201
26
- trilogy/core/enums.py,sha256=R_iNsK8j0uGJ5fzfFUwoe1vhIHTX28VJfDQd99WBQ4E,9064
26
+ trilogy/core/enums.py,sha256=SkuyRrqqS59LO08CWwbFaZ5kYw_w0WWWcigd9MSXdaA,9082
27
27
  trilogy/core/env_processor.py,sha256=H-rr2ALj31l5oh3FqeI47Qju6OOfiXBacXNJGNZ92zQ,4521
28
28
  trilogy/core/environment_helpers.py,sha256=TRlqVctqIRBxzfjRBmpQsAVoiCcsEKBhG1B6PUE0l1M,12743
29
29
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
30
30
  trilogy/core/exceptions.py,sha256=axkVXYJYQXCCwMHwlyDA232g4tCOwdCZUt7eHeUMDMg,2829
31
- trilogy/core/functions.py,sha256=hURJBM5dNcHfXuPHaHM_tz_ahsEktnmL4At0zebfjMk,34668
31
+ trilogy/core/functions.py,sha256=QidWH7dDjvLjXA0ujCkemUzYjkYmtMD2T78hnvk86K8,34876
32
32
  trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
33
33
  trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
34
- trilogy/core/optimization.py,sha256=Km0ITEx9n6Iv5ReX6tm4uXO5uniSv_ooahycNNiET3g,9212
34
+ trilogy/core/optimization.py,sha256=eKieOaWXUtoNTVQbThGA5tqrI06ZR6SUFOqGe4Jw0k4,9262
35
35
  trilogy/core/query_processor.py,sha256=rMrtLSQxVm7yeyh0nWjDNI9nnu4Xi0NgHvBJ14gvu4I,20384
36
36
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
37
37
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- trilogy/core/models/author.py,sha256=DFpIGlIruz0JYIOLJ0qZQrsdT4BvQX0augow1G8pD54,81841
39
- trilogy/core/models/build.py,sha256=LN8G1JRk8RDSFQwqS3CUszIIUgB5v9Nxy6EpxvAcJ1A,71514
38
+ trilogy/core/models/author.py,sha256=YnBwxanbn9uBAExz5POM0_RFw9GIHmjfLZRVzA10Gws,85862
39
+ trilogy/core/models/build.py,sha256=zVHD8jo8V8E0aGWtobzXG7hY3VjLChiW4s1QBx9sD5k,72938
40
40
  trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYycCrP4zF-rHU,5258
41
41
  trilogy/core/models/core.py,sha256=iT9WdZoiXeglmUHWn6bZyXCTBpkApTGPKtNm_Mhbu_g,12987
42
42
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
43
- trilogy/core/models/environment.py,sha256=do1Xvr9oyjDj0knAxgIqexSbt0HMrHbLJNyl9utkSvs,28760
43
+ trilogy/core/models/environment.py,sha256=-r6_imqkJ10iPTbWGrdfeF2Th480UKLsG8ihACyqO8w,28823
44
44
  trilogy/core/models/execute.py,sha256=3fgEdho2e7S0outq91cCzb9jFwz6L1hTbsTrJwGvIFs,42311
45
45
  trilogy/core/optimizations/__init__.py,sha256=yspWc25M5SgAuvXYoSt5J8atyPbDlOfsKjIo5yGD9s4,368
46
46
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
47
- trilogy/core/optimizations/hide_unused_concept.py,sha256=DbsP8NqQOxmPv9omDOoFNPUGObUkqsRRNrr5d1xDxx4,1962
47
+ trilogy/core/optimizations/hide_unused_concept.py,sha256=THp6byyh64dArSz6EuY7unik5WKgQQfWxNGToEqjqE0,1875
48
48
  trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
49
49
  trilogy/core/optimizations/predicate_pushdown.py,sha256=5ubatgq1IwWQ4L2FDt4--y168YLuGP-vwqH0m8IeTIw,9786
50
50
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- trilogy/core/processing/concept_strategies_v3.py,sha256=Y6AG10NGU_mUiTfXmxkss7QSO5iLpOdzGN6-7Hc5XiQ,23294
51
+ trilogy/core/processing/concept_strategies_v3.py,sha256=MBQeJGBDW2w3xxwano43-5MVBqfbIU9M3K7RFbnSKNA,23367
52
52
  trilogy/core/processing/discovery_node_factory.py,sha256=llnLxZo1NqBRIuuPz0GUohym6LZFhVkPT3xSiORi3k4,15446
53
53
  trilogy/core/processing/discovery_utility.py,sha256=mO0npZMRlQSzxt3l4m8garKBAOrXFkzt3eiiUyUSoIU,13528
54
54
  trilogy/core/processing/discovery_validation.py,sha256=eZ4HfHMpqZLI8MGG2jez8arS8THs6ceuVrQFIY6gXrU,5364
55
55
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
56
56
  trilogy/core/processing/utility.py,sha256=ESs6pKqVP2c9eMdfB2JNjw7D7YnoezVwbLFx1D6OUYA,26088
57
57
  trilogy/core/processing/node_generators/__init__.py,sha256=iVJ-crowPxYeut-hFjyEjfibKIDq7PfB4LEuDAUCjGY,943
58
- trilogy/core/processing/node_generators/basic_node.py,sha256=rv53il-W633by0plvbN5qaqznJfl60yPinLZGBAC_MI,5707
59
- trilogy/core/processing/node_generators/common.py,sha256=xF32Kf6B08dZgKs2SOow1HomptSiSC057GCUCHFlS5s,9464
58
+ trilogy/core/processing/node_generators/basic_node.py,sha256=IeR00MV6Ay1TDuebcbwNTp1WC9kT23_JIWYpeUqtn_M,5992
59
+ trilogy/core/processing/node_generators/common.py,sha256=oGUW4_RhSpfGu9PNUj0JsHB8iMeTz76FQHMcfVRHP30,9516
60
60
  trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
61
61
  trilogy/core/processing/node_generators/filter_node.py,sha256=EiP_tafx-X0gM-BIVRCy2rnp1_apt2cbhVfv8cg9dVg,11259
62
62
  trilogy/core/processing/node_generators/group_node.py,sha256=sIm1QYrF4EY6sk56A48B6MieCZqvaJLSQebih_aiKnQ,8567
63
63
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
64
- trilogy/core/processing/node_generators/multiselect_node.py,sha256=dHPDoSKU0FF6Ue_t_LkZxTd0Q-Sf-EpYdsMYdyUlFQc,7120
64
+ trilogy/core/processing/node_generators/multiselect_node.py,sha256=qrgx8ofExbigipFBXKufu279vKWBLSBP91TyJW6g1qE,7135
65
65
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=hNcZxnDLTZyYJWfojg769zH9HB9PfZfESmpN1lcHWXg,23172
66
66
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
67
67
  trilogy/core/processing/node_generators/rowset_node.py,sha256=MuVNIexXhqGONho_mewqMOwaYXNUnjjvyPvk_RDGNYE,5943
@@ -84,7 +84,7 @@ trilogy/core/processing/nodes/union_node.py,sha256=hLAXXVWqEgMWi7dlgSHfCF59fon64
84
84
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
85
85
  trilogy/core/processing/nodes/window_node.py,sha256=JXJ0iVRlSEM2IBr1TANym2RaUf_p5E_l2sNykRzXWDo,1710
86
86
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- trilogy/core/statements/author.py,sha256=zLd-1IZKF5UE7oWQYqifn3b3JcW2wCbonlELjJiZhfo,16436
87
+ trilogy/core/statements/author.py,sha256=uEYJ9Q39TdGP0cxQEv3Ue4pGp_dmZ-NiT6k2-rfZERQ,16525
88
88
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  trilogy/core/statements/common.py,sha256=VnVLULQg1TJLNUFzJaROT1tsf2ewk3IpuhvZaP36R6A,535
90
90
  trilogy/core/statements/execute.py,sha256=kiwJcVeMa4wZR-xLfM2oYOJ9DeyJkP8An38WFyJxktM,2413
@@ -95,7 +95,7 @@ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGx
95
95
  trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
96
96
  trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
97
97
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
- trilogy/dialect/base.py,sha256=Eo8n5fJiHOYkfDDDFgnrqMuIZTxRorWwVer_1-DRG4c,51178
98
+ trilogy/dialect/base.py,sha256=i_rdrO3TmKYZK4K8k6ggpvRmzaCl1oR65n89_yHNEPo,52445
99
99
  trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
100
100
  trilogy/dialect/common.py,sha256=I5Ku_TR5MwJTB3ZhcQenrtvXhH2RvTQ8wQe9w5lfkfA,5708
101
101
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
@@ -113,13 +113,13 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
113
113
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
114
114
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
115
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- trilogy/parsing/common.py,sha256=r9EmWzN2ozMKtWCY1HfjOREJnLKvaURT6h6edS5U4x8,32508
116
+ trilogy/parsing/common.py,sha256=GijDRpysULL6vQWpFcjgxVASuTWXUVUi5fILHvjzkbg,35534
117
117
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
118
118
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
119
119
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
120
- trilogy/parsing/parse_engine.py,sha256=lMo73QD37ih5kgvAOToMLCBbCkBzo_G7QMOaQaHprV8,84333
120
+ trilogy/parsing/parse_engine.py,sha256=iHBRUatv4hXSdDgAjllgTe1OtD8WE7V0Gu-ZTT2YugY,86301
121
121
  trilogy/parsing/render.py,sha256=k7MNp8EBTqVBSVqFlgTHSwIhfSKLyJfSeb2fSbt9dVA,24307
122
- trilogy/parsing/trilogy.lark,sha256=kPOIrwa__kdXvxS3uM5GzdlgIa2cQxNW_0DNRqsRBeg,16939
122
+ trilogy/parsing/trilogy.lark,sha256=6nQ4fLpiL0pNMAlmmIEwN8Am4DlIIYpBVajp4jHveA8,17185
123
123
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
124
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
125
125
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -132,8 +132,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
132
132
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
133
133
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
134
134
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
135
- pytrilogy-0.0.3.115.dist-info/METADATA,sha256=pB3OdY3t5s8GkX73rniQKQkMSy8JcXjzKZ0w1va3k5E,12911
136
- pytrilogy-0.0.3.115.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
- pytrilogy-0.0.3.115.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
- pytrilogy-0.0.3.115.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
- pytrilogy-0.0.3.115.dist-info/RECORD,,
135
+ pytrilogy-0.0.3.116.dist-info/METADATA,sha256=N3aeyGXHRF0etmB8YiAXUHOfidTmQgqDcxUm8enTumw,12911
136
+ pytrilogy-0.0.3.116.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
+ pytrilogy-0.0.3.116.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
+ pytrilogy-0.0.3.116.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
+ pytrilogy-0.0.3.116.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.115"
7
+ __version__ = "0.0.3.116"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -31,6 +31,7 @@ class Optimizations:
31
31
  constant_inlining: bool = True
32
32
  constant_inline_cutoff: int = 10
33
33
  direct_return: bool = True
34
+ hide_unused_concepts: bool = True
34
35
 
35
36
 
36
37
  @dataclass
trilogy/core/enums.py CHANGED
@@ -217,6 +217,7 @@ class FunctionType(Enum):
217
217
  CONTAINS = "contains"
218
218
  TRIM = "trim"
219
219
  REPLACE = "replace"
220
+ HASH = "hash"
220
221
 
221
222
  # STRING REGEX
222
223
  REGEXP_CONTAINS = "regexp_contains"
trilogy/core/functions.py CHANGED
@@ -928,6 +928,14 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
928
928
  output_type=DataType.TIMESTAMP,
929
929
  arg_count=1,
930
930
  ),
931
+ FunctionType.HASH: FunctionConfig(
932
+ valid_inputs={
933
+ DataType.STRING,
934
+ },
935
+ output_purpose=Purpose.PROPERTY,
936
+ output_type=DataType.STRING,
937
+ arg_count=2,
938
+ ),
931
939
  }
932
940
 
933
941
  EXCLUDED_FUNCTIONS = {
@@ -865,6 +865,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
865
865
  AggregateWrapper,
866
866
  RowsetItem,
867
867
  MultiSelectLineage,
868
+ Comparison,
868
869
  ]
869
870
  ] = None
870
871
  namespace: str = Field(default=DEFAULT_NAMESPACE, validate_default=True)
@@ -1079,6 +1080,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1079
1080
  | AggregateWrapper
1080
1081
  | RowsetItem
1081
1082
  | MultiSelectLineage
1083
+ | Comparison
1082
1084
  | None,
1083
1085
  Grain,
1084
1086
  set[str] | None,
@@ -1178,6 +1180,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1178
1180
  AggregateWrapper,
1179
1181
  RowsetItem,
1180
1182
  MultiSelectLineage,
1183
+ Comparison,
1181
1184
  ],
1182
1185
  output: List[ConceptRef],
1183
1186
  ):
@@ -1204,6 +1207,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1204
1207
  def calculate_derivation(self, lineage, purpose: Purpose) -> Derivation:
1205
1208
  from trilogy.core.models.build import (
1206
1209
  BuildAggregateWrapper,
1210
+ BuildComparison,
1207
1211
  BuildFilterItem,
1208
1212
  BuildFunction,
1209
1213
  BuildMultiSelectLineage,
@@ -1221,6 +1225,8 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1221
1225
  # return Derivation.PARENTHETICAL
1222
1226
  elif lineage and isinstance(lineage, (BuildRowsetItem, RowsetItem)):
1223
1227
  return Derivation.ROWSET
1228
+ elif lineage and isinstance(lineage, BuildComparison):
1229
+ return Derivation.BASIC
1224
1230
  elif lineage and isinstance(
1225
1231
  lineage, (BuildMultiSelectLineage, MultiSelectLineage)
1226
1232
  ):
@@ -2133,6 +2139,89 @@ class AlignClause(Namespaced, BaseModel):
2133
2139
  )
2134
2140
 
2135
2141
 
2142
+ class DeriveItem(Namespaced, DataTyped, ConceptArgs, Mergeable, BaseModel):
2143
+ expr: Expr
2144
+ name: str
2145
+ namespace: str
2146
+
2147
+ @property
2148
+ def derived_concept(self) -> str:
2149
+ return f"{self.namespace}.{self.name}"
2150
+ # return ConceptRef(
2151
+ # address=f"{self.namespace}.{self.name}",
2152
+ # datatype=arg_to_datatype(self.expr),
2153
+ # )
2154
+
2155
+ def with_namespace(self, namespace):
2156
+ return DeriveItem.model_construct(
2157
+ expr=(self.expr.with_namespace(namespace) if self.expr else None),
2158
+ name=self.name,
2159
+ namespace=namespace,
2160
+ )
2161
+
2162
+ def with_merge(
2163
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2164
+ ) -> "DeriveItem":
2165
+ return DeriveItem.model_construct(
2166
+ expr=(
2167
+ self.expr.with_merge(source, target, modifiers)
2168
+ if isinstance(self.expr, Mergeable)
2169
+ else self.expr
2170
+ ),
2171
+ name=self.name,
2172
+ namespace=self.namespace,
2173
+ )
2174
+
2175
+ def with_reference_replacement(self, source, target):
2176
+ return DeriveItem.model_construct(
2177
+ expr=(
2178
+ self.expr.with_reference_replacement(source, target)
2179
+ if isinstance(self.expr, Mergeable)
2180
+ else self.expr
2181
+ ),
2182
+ name=self.name,
2183
+ namespace=self.namespace,
2184
+ )
2185
+
2186
+
2187
+ class DeriveClause(Mergeable, Namespaced, BaseModel):
2188
+ items: List[DeriveItem]
2189
+
2190
+ def with_namespace(self, namespace: str) -> "DeriveClause":
2191
+ return DeriveClause.model_construct(
2192
+ items=[
2193
+ x.with_namespace(namespace) if isinstance(x, Namespaced) else x
2194
+ for x in self.items
2195
+ ]
2196
+ )
2197
+
2198
+ def with_merge(
2199
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2200
+ ) -> "DeriveClause":
2201
+ return DeriveClause.model_construct(
2202
+ items=[
2203
+ (
2204
+ x.with_merge(source, target, modifiers)
2205
+ if isinstance(x, Mergeable)
2206
+ else x
2207
+ )
2208
+ for x in self.items
2209
+ ]
2210
+ )
2211
+
2212
+ def with_reference_replacement(self, source, target):
2213
+ return DeriveClause.model_construct(
2214
+ items=[
2215
+ (
2216
+ x.with_reference_replacement(source, target)
2217
+ if isinstance(x, Mergeable)
2218
+ else x
2219
+ )
2220
+ for x in self.items
2221
+ ]
2222
+ )
2223
+
2224
+
2136
2225
  class SelectLineage(Mergeable, Namespaced, BaseModel):
2137
2226
  selection: List[ConceptRef]
2138
2227
  hidden_components: set[str]
@@ -2177,15 +2266,40 @@ class SelectLineage(Mergeable, Namespaced, BaseModel):
2177
2266
  ),
2178
2267
  )
2179
2268
 
2269
+ def with_namespace(self, namespace):
2270
+ return SelectLineage.model_construct(
2271
+ selection=[x.with_namespace(namespace) for x in self.selection],
2272
+ hidden_components=self.hidden_components,
2273
+ local_concepts={
2274
+ x: y.with_namespace(namespace) for x, y in self.local_concepts.items()
2275
+ },
2276
+ order_by=self.order_by.with_namespace(namespace) if self.order_by else None,
2277
+ limit=self.limit,
2278
+ meta=self.meta,
2279
+ grain=self.grain.with_namespace(namespace),
2280
+ where_clause=(
2281
+ self.where_clause.with_namespace(namespace)
2282
+ if self.where_clause
2283
+ else None
2284
+ ),
2285
+ having_clause=(
2286
+ self.having_clause.with_namespace(namespace)
2287
+ if self.having_clause
2288
+ else None
2289
+ ),
2290
+ )
2291
+
2180
2292
 
2181
2293
  class MultiSelectLineage(Mergeable, ConceptArgs, Namespaced, BaseModel):
2182
2294
  selects: List[SelectLineage]
2183
2295
  align: AlignClause
2296
+
2184
2297
  namespace: str
2185
2298
  order_by: Optional[OrderBy] = None
2186
2299
  limit: Optional[int] = None
2187
2300
  where_clause: Union["WhereClause", None] = Field(default=None)
2188
2301
  having_clause: Union["HavingClause", None] = Field(default=None)
2302
+ derive: DeriveClause | None = None
2189
2303
  hidden_components: set[str]
2190
2304
 
2191
2305
  @property
@@ -2211,6 +2325,11 @@ class MultiSelectLineage(Mergeable, ConceptArgs, Namespaced, BaseModel):
2211
2325
  new = MultiSelectLineage.model_construct(
2212
2326
  selects=[s.with_merge(source, target, modifiers) for s in self.selects],
2213
2327
  align=self.align,
2328
+ derive=(
2329
+ self.derive.with_merge(source, target, modifiers)
2330
+ if self.derive
2331
+ else None
2332
+ ),
2214
2333
  namespace=self.namespace,
2215
2334
  hidden_components=self.hidden_components,
2216
2335
  order_by=(
@@ -2236,6 +2355,7 @@ class MultiSelectLineage(Mergeable, ConceptArgs, Namespaced, BaseModel):
2236
2355
  return MultiSelectLineage.model_construct(
2237
2356
  selects=[c.with_namespace(namespace) for c in self.selects],
2238
2357
  align=self.align.with_namespace(namespace),
2358
+ derive=self.derive.with_namespace(namespace) if self.derive else None,
2239
2359
  namespace=namespace,
2240
2360
  hidden_components=self.hidden_components,
2241
2361
  order_by=self.order_by.with_namespace(namespace) if self.order_by else None,
@@ -2257,6 +2377,9 @@ class MultiSelectLineage(Mergeable, ConceptArgs, Namespaced, BaseModel):
2257
2377
  output = set()
2258
2378
  for item in self.align.items:
2259
2379
  output.add(item.aligned_concept)
2380
+ if self.derive:
2381
+ for ditem in self.derive.items:
2382
+ output.add(ditem.derived_concept)
2260
2383
  return output
2261
2384
 
2262
2385
  @property
@@ -48,6 +48,8 @@ from trilogy.core.models.author import (
48
48
  Concept,
49
49
  ConceptRef,
50
50
  Conditional,
51
+ DeriveClause,
52
+ DeriveItem,
51
53
  FilterItem,
52
54
  FuncArgs,
53
55
  Function,
@@ -247,7 +249,7 @@ class BuildParamaterizedConceptReference(DataTyped):
247
249
  concept: BuildConcept
248
250
 
249
251
  def __str__(self):
250
- return f":{self.concept.address}"
252
+ return f":{self.concept.address.replace('.', '_')}"
251
253
 
252
254
  @property
253
255
  def safe_address(self) -> str:
@@ -1268,6 +1270,22 @@ class BuildAlignClause:
1268
1270
  items: List[BuildAlignItem]
1269
1271
 
1270
1272
 
1273
+ @dataclass
1274
+ class BuildDeriveClause:
1275
+ items: List[BuildDeriveItem]
1276
+
1277
+
1278
+ @dataclass
1279
+ class BuildDeriveItem:
1280
+ expr: BuildExpr
1281
+ name: str
1282
+ namespace: str = field(default=DEFAULT_NAMESPACE)
1283
+
1284
+ @property
1285
+ def address(self) -> str:
1286
+ return f"{self.namespace}.{self.name}"
1287
+
1288
+
1271
1289
  @dataclass
1272
1290
  class BuildSelectLineage:
1273
1291
  selection: List[BuildConcept]
@@ -1299,12 +1317,16 @@ class BuildMultiSelectLineage(BuildConceptArgs):
1299
1317
  limit: Optional[int] = None
1300
1318
  where_clause: Union["BuildWhereClause", None] = field(default=None)
1301
1319
  having_clause: Union["BuildHavingClause", None] = field(default=None)
1320
+ derive: BuildDeriveClause | None = None
1302
1321
 
1303
1322
  @property
1304
1323
  def derived_concepts(self) -> set[str]:
1305
1324
  output = set()
1306
1325
  for item in self.align.items:
1307
1326
  output.add(item.aligned_concept)
1327
+ if self.derive:
1328
+ for ditem in self.derive.items:
1329
+ output.add(ditem.address)
1308
1330
  return output
1309
1331
 
1310
1332
  @property
@@ -1312,10 +1334,12 @@ class BuildMultiSelectLineage(BuildConceptArgs):
1312
1334
  return self.build_output_components
1313
1335
 
1314
1336
  @property
1315
- def derived_concept(self) -> set[str]:
1316
- output = set()
1317
- for item in self.align.items:
1318
- output.add(item.aligned_concept)
1337
+ def calculated_derivations(self) -> set[str]:
1338
+ output: set[str] = set()
1339
+ if not self.derive:
1340
+ return output
1341
+ for item in self.derive.items:
1342
+ output.add(item.address)
1319
1343
  return output
1320
1344
 
1321
1345
  @property
@@ -1335,6 +1359,7 @@ class BuildMultiSelectLineage(BuildConceptArgs):
1335
1359
  for c in x.concepts:
1336
1360
  if c.address in cte.output_lcl:
1337
1361
  return c
1362
+
1338
1363
  raise SyntaxError(
1339
1364
  f"Could not find upstream map for multiselect {str(concept)} on cte ({cte})"
1340
1365
  )
@@ -1961,6 +1986,28 @@ class Factory:
1961
1986
  def _build_align_clause(self, base: AlignClause) -> BuildAlignClause:
1962
1987
  return BuildAlignClause(items=[self._build_align_item(x) for x in base.items])
1963
1988
 
1989
+ @build.register
1990
+ def _(self, base: DeriveItem) -> BuildDeriveItem:
1991
+ return self._build_derive_item(base)
1992
+
1993
+ def _build_derive_item(self, base: DeriveItem) -> BuildDeriveItem:
1994
+ expr: Concept | FuncArgs = base.expr
1995
+ validation = requires_concept_nesting(expr)
1996
+ if validation:
1997
+ expr, _ = self.instantiate_concept(validation)
1998
+ return BuildDeriveItem(
1999
+ expr=self.build(expr),
2000
+ name=base.name,
2001
+ namespace=base.namespace,
2002
+ )
2003
+
2004
+ @build.register
2005
+ def _(self, base: DeriveClause) -> BuildDeriveClause:
2006
+ return self._build_derive_clause(base)
2007
+
2008
+ def _build_derive_clause(self, base: DeriveClause) -> BuildDeriveClause:
2009
+ return BuildDeriveClause(items=[self.build(x) for x in base.items])
2010
+
1964
2011
  @build.register
1965
2012
  def _(self, base: RowsetItem) -> BuildRowsetItem:
1966
2013
  return self._build_rowset_item(base)
@@ -2162,6 +2209,7 @@ class Factory:
2162
2209
  selects=base.selects,
2163
2210
  grain=final_grain,
2164
2211
  align=factory.build(base.align),
2212
+ derive=factory.build(base.derive) if base.derive else None,
2165
2213
  # self.align.with_select_context(
2166
2214
  # local_build_cache, self.grain, environment
2167
2215
  # ),
@@ -413,7 +413,8 @@ class Environment(BaseModel):
413
413
  self.imports[alias].append(imp_stm)
414
414
  # we can't exit early
415
415
  # as there may be new concepts
416
- for k, concept in source.concepts.items():
416
+ iteration: list[tuple[str, Concept]] = list(source.concepts.items())
417
+ for k, concept in iteration:
417
418
  # skip internal namespace
418
419
  if INTERNAL_NAMESPACE in concept.address:
419
420
  continue
@@ -228,7 +228,8 @@ def optimize_ctes(
228
228
  REGISTERED_RULES.append(PredicatePushdown())
229
229
  if CONFIG.optimizations.predicate_pushdown:
230
230
  REGISTERED_RULES.append(PredicatePushdownRemove())
231
- REGISTERED_RULES.append(HideUnusedConcepts())
231
+ if CONFIG.optimizations.hide_unused_concepts:
232
+ REGISTERED_RULES.append(HideUnusedConcepts())
232
233
  for rule in REGISTERED_RULES:
233
234
  loops = 0
234
235
  complete = False
@@ -242,7 +243,7 @@ def optimize_ctes(
242
243
  actions_taken = actions_taken or opt
243
244
  complete = not actions_taken
244
245
  loops += 1
245
- input = reorder_ctes(filter_irrelevant_ctes(input, root_cte))
246
+ input = reorder_ctes(filter_irrelevant_ctes(input, root_cte))
246
247
  logger.info(
247
248
  f"[Optimization] Finished checking for {type(rule).__name__} after {loops} loop(s)"
248
249
  )
@@ -39,11 +39,7 @@ class HideUnusedConcepts(OptimizationRule):
39
39
  self.log(
40
40
  f"Hiding unused concepts {[x.address for x in add_to_hidden]} from {cte.name} (used: {used}, all: {[x.address for x in cte.output_columns]})"
41
41
  )
42
- candidates = [
43
- x.address
44
- for x in cte.output_columns
45
- if x.address not in used and x.address not in cte.hidden_concepts
46
- ]
42
+ candidates = [x.address for x in cte.output_columns if x.address not in used]
47
43
  if len(candidates) == len(set([x.address for x in cte.output_columns])):
48
44
  # pop one out
49
45
  candidates.pop()
@@ -496,6 +496,8 @@ def _search_concepts(
496
496
  conditions=conditions,
497
497
  )
498
498
  partial: set[str] = set()
499
+ virtual: set[str] = set()
500
+ complete = ValidationResult.INCOMPLETE
499
501
  while context.incomplete:
500
502
 
501
503
  priority_concept = get_priority_concept(
@@ -7,7 +7,7 @@ from trilogy.core.models.build_environment import BuildEnvironment
7
7
  from trilogy.core.processing.node_generators.common import (
8
8
  resolve_function_parent_concepts,
9
9
  )
10
- from trilogy.core.processing.nodes import History, StrategyNode
10
+ from trilogy.core.processing.nodes import ConstantNode, History, StrategyNode
11
11
  from trilogy.utility import unique
12
12
 
13
13
  LOGGER_PREFIX = "[GEN_BASIC_NODE]"
@@ -51,11 +51,14 @@ def gen_basic_node(
51
51
  )
52
52
  synonyms: list[BuildConcept] = []
53
53
  ignored_optional: set[str] = set()
54
- assert isinstance(concept.lineage, BuildFunction)
54
+
55
55
  # when we are getting an attribute, if there is anything else
56
56
  # that is an attribute of the same struct in local optional
57
57
  # select that value for discovery as well
58
- if concept.lineage.operator == FunctionType.ATTR_ACCESS:
58
+ if (
59
+ isinstance(concept.lineage, BuildFunction)
60
+ and concept.lineage.operator == FunctionType.ATTR_ACCESS
61
+ ):
59
62
  logger.info(
60
63
  f"{depth_prefix}{LOGGER_PREFIX} checking for synonyms for attribute access"
61
64
  )
@@ -106,20 +109,28 @@ def gen_basic_node(
106
109
  logger.info(
107
110
  f"{depth_prefix}{LOGGER_PREFIX} Fetching parents {[x.address for x in all_parents]}"
108
111
  )
109
- parent_node: StrategyNode | None = source_concepts(
110
- mandatory_list=all_parents,
111
- environment=environment,
112
- g=g,
113
- depth=depth + 1,
114
- history=history,
115
- conditions=conditions,
116
- )
112
+ if all_parents:
113
+ parent_node: StrategyNode | None = source_concepts(
114
+ mandatory_list=all_parents,
115
+ environment=environment,
116
+ g=g,
117
+ depth=depth + 1,
118
+ history=history,
119
+ conditions=conditions,
120
+ )
117
121
 
118
- if not parent_node:
119
- logger.info(
120
- f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
122
+ if not parent_node:
123
+ logger.info(
124
+ f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
125
+ )
126
+ return None
127
+ else:
128
+ return ConstantNode(
129
+ input_concepts=[],
130
+ output_concepts=[concept],
131
+ environment=environment,
132
+ depth=depth,
121
133
  )
122
- return None
123
134
  if parent_node.source_type != SourceType.CONSTANT:
124
135
  parent_node.source_type = SourceType.BASIC
125
136
  parent_node.add_output_concept(concept)
@@ -4,6 +4,7 @@ from typing import Callable, List, Tuple
4
4
  from trilogy.core.enums import Derivation, Purpose
5
5
  from trilogy.core.models.build import (
6
6
  BuildAggregateWrapper,
7
+ BuildComparison,
7
8
  BuildConcept,
8
9
  BuildFilterItem,
9
10
  BuildFunction,
@@ -26,7 +27,9 @@ FUNCTION_TYPES = (BuildFunction,)
26
27
  def resolve_function_parent_concepts(
27
28
  concept: BuildConcept, environment: BuildEnvironment
28
29
  ) -> List[BuildConcept]:
29
- if not isinstance(concept.lineage, (*FUNCTION_TYPES, *AGGREGATE_TYPES)):
30
+ if not isinstance(
31
+ concept.lineage, (*FUNCTION_TYPES, *AGGREGATE_TYPES, BuildComparison)
32
+ ):
30
33
  raise ValueError(
31
34
  f"Concept {concept} lineage is not function or aggregate, is {type(concept.lineage)}"
32
35
  )
@@ -157,19 +157,19 @@ def gen_multiselect_node(
157
157
  possible_joins = concept_to_relevant_joins(additional_relevant)
158
158
  if not local_optional:
159
159
  logger.info(
160
- f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node; exiting early"
160
+ f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for multiselect node; exiting early"
161
161
  )
162
162
  return node
163
163
  if not possible_joins:
164
164
  logger.info(
165
- f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node; exiting early"
165
+ f"{padding(depth)}{LOGGER_PREFIX} no possible joins for multiselect node; exiting early"
166
166
  )
167
167
  return node
168
168
  if all(
169
169
  [x.address in [y.address for y in node.output_concepts] for x in local_optional]
170
170
  ):
171
171
  logger.info(
172
- f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base rowset node; exiting early"
172
+ f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base multiselect node; exiting early"
173
173
  )
174
174
  return node
175
175
  logger.info(
@@ -21,6 +21,7 @@ from trilogy.core.models.author import (
21
21
  Concept,
22
22
  ConceptRef,
23
23
  CustomType,
24
+ DeriveClause,
24
25
  Expr,
25
26
  FilterItem,
26
27
  Function,
@@ -352,11 +353,13 @@ class MultiSelectStatement(HasUUID, SelectTypeMixin, BaseModel):
352
353
  local_concepts: Annotated[
353
354
  EnvironmentConceptDict, PlainValidator(validate_concepts)
354
355
  ] = Field(default_factory=EnvironmentConceptDict)
356
+ derive: DeriveClause | None = None
355
357
 
356
358
  def as_lineage(self, environment: Environment):
357
359
  return MultiSelectLineage(
358
360
  selects=[x.as_lineage(environment) for x in self.selects],
359
361
  align=self.align,
362
+ derive=self.derive,
360
363
  namespace=self.namespace,
361
364
  # derived_concepts = self.derived_concepts,
362
365
  limit=self.limit,
trilogy/dialect/base.py CHANGED
@@ -176,6 +176,20 @@ def struct_arg(args):
176
176
  return [f"{x[1]}: {x[0]}" for x in zip(args[::2], args[1::2])]
177
177
 
178
178
 
179
+ def hash_from_args(val, hash_type):
180
+ hash_type = hash_type[1:-1]
181
+ if hash_type.lower() == "md5":
182
+ return f"md5({val})"
183
+ elif hash_type.lower() == "sha1":
184
+ return f"sha1({val})"
185
+ elif hash_type.lower() == "sha256":
186
+ return f"sha256({val})"
187
+ elif hash_type.lower() == "sha512":
188
+ return f"sha512({val})"
189
+ else:
190
+ raise ValueError(f"Unsupported hash type: {hash_type}")
191
+
192
+
179
193
  FUNCTION_MAP = {
180
194
  # generic types
181
195
  FunctionType.ALIAS: lambda x: f"{x[0]}",
@@ -260,6 +274,7 @@ FUNCTION_MAP = {
260
274
  FunctionType.REGEXP_REPLACE: lambda x: f"REGEXP_REPLACE({x[0]},{x[1]}, {x[2]})",
261
275
  FunctionType.TRIM: lambda x: f"TRIM({x[0]})",
262
276
  FunctionType.REPLACE: lambda x: f"REPLACE({x[0]},{x[1]},{x[2]})",
277
+ FunctionType.HASH: lambda x: hash_from_args(x[0], x[1]),
263
278
  # FunctionType.NOT_LIKE: lambda x: f" CASE WHEN {x[0]} like {x[1]} THEN 0 ELSE 1 END",
264
279
  # date types
265
280
  FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc({x[0]},{x[1]})",
@@ -484,7 +499,20 @@ class BaseDialect:
484
499
  elif isinstance(c.lineage, BuildRowsetItem):
485
500
  rval = f"{self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid)}"
486
501
  elif isinstance(c.lineage, BuildMultiSelectLineage):
487
- rval = f"{self.render_concept_sql(c.lineage.find_source(c, cte), cte=cte, alias=False, raise_invalid=raise_invalid)}"
502
+ if c.address in c.lineage.calculated_derivations:
503
+ assert c.lineage.derive is not None
504
+ for x in c.lineage.derive.items:
505
+ if x.address == c.address:
506
+ rval = self.render_expr(
507
+ x.expr,
508
+ cte=cte,
509
+ raise_invalid=raise_invalid,
510
+ )
511
+ break
512
+ else:
513
+ rval = f"{self.render_concept_sql(c.lineage.find_source(c, cte), cte=cte, alias=False, raise_invalid=raise_invalid)}"
514
+ elif isinstance(c.lineage, BuildComparison):
515
+ rval = f"{self.render_expr(c.lineage.left, cte=cte, raise_invalid=raise_invalid)} {c.lineage.operator.value} {self.render_expr(c.lineage.right, cte=cte, raise_invalid=raise_invalid)}"
488
516
  elif isinstance(c.lineage, AGGREGATE_ITEMS):
489
517
  args = [
490
518
  self.render_expr(v, cte) # , alias=False)
@@ -816,7 +844,7 @@ class BaseDialect:
816
844
  if self.rendering.parameters:
817
845
  if e.concept.namespace == DEFAULT_NAMESPACE:
818
846
  return f":{e.concept.name}"
819
- return f":{e.concept.address}"
847
+ return f":{e.concept.address.replace('.', '_')}"
820
848
  elif e.concept.lineage:
821
849
  return self.render_expr(e.concept.lineage, cte=cte, cte_map=cte_map)
822
850
  return f"{self.QUOTE_CHARACTER}{e.concept.address}{self.QUOTE_CHARACTER}"
trilogy/executor.py CHANGED
@@ -397,7 +397,7 @@ class Executor(object):
397
397
  if v.safe_address == param or v.address == param
398
398
  ]
399
399
  if not matched:
400
- raise SyntaxError(f"No concept found for parameter {param}")
400
+ raise SyntaxError(f"No concept found for parameter {param};")
401
401
 
402
402
  concept: Concept = matched.pop()
403
403
  return self._concept_to_value(concept, local_concepts=local_concepts)
trilogy/parsing/common.py CHANGED
@@ -3,9 +3,7 @@ from typing import Iterable, List, Sequence, Tuple
3
3
 
4
4
  from lark.tree import Meta
5
5
 
6
- from trilogy.constants import (
7
- VIRTUAL_CONCEPT_PREFIX,
8
- )
6
+ from trilogy.constants import DEFAULT_NAMESPACE, VIRTUAL_CONCEPT_PREFIX
9
7
  from trilogy.core.constants import ALL_ROWS_CONCEPT
10
8
  from trilogy.core.enums import (
11
9
  ConceptSource,
@@ -64,10 +62,12 @@ ARBITRARY_INPUTS = (
64
62
  | Parenthetical
65
63
  | ListWrapper
66
64
  | MapWrapper
65
+ | Comparison
67
66
  | int
68
67
  | float
69
68
  | str
70
69
  | date
70
+ | bool
71
71
  )
72
72
 
73
73
 
@@ -399,6 +399,10 @@ def _get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
399
399
  return [x.reference for x in arg.by], True
400
400
  elif isinstance(arg, FunctionCallWrapper):
401
401
  return get_relevant_parent_concepts(arg.content)
402
+ elif isinstance(arg, Comparison):
403
+ left, lflag = get_relevant_parent_concepts(arg.left)
404
+ right, rflag = get_relevant_parent_concepts(arg.right)
405
+ return left + right, lflag or rflag
402
406
  return get_concept_arguments(arg), False
403
407
 
404
408
 
@@ -775,6 +779,27 @@ def align_item_to_concept(
775
779
  return new
776
780
 
777
781
 
782
+ def derive_item_to_concept(
783
+ parent: ARBITRARY_INPUTS,
784
+ name: str,
785
+ lineage: MultiSelectLineage,
786
+ namespace: str | None = None,
787
+ ) -> Concept:
788
+ datatype = arg_to_datatype(parent)
789
+ grain = Grain()
790
+ new = Concept(
791
+ name=name,
792
+ datatype=datatype,
793
+ purpose=Purpose.PROPERTY,
794
+ lineage=lineage,
795
+ grain=grain,
796
+ namespace=namespace or DEFAULT_NAMESPACE,
797
+ granularity=Granularity.MULTI_ROW,
798
+ derivation=Derivation.MULTISELECT,
799
+ )
800
+ return new
801
+
802
+
778
803
  def rowset_concept(
779
804
  orig_address: ConceptRef,
780
805
  environment: Environment,
@@ -856,20 +881,7 @@ def rowset_to_concepts(rowset: RowsetDerivationStatement, environment: Environme
856
881
 
857
882
 
858
883
  def generate_concept_name(
859
- parent: (
860
- AggregateWrapper
861
- | FunctionCallWrapper
862
- | WindowItem
863
- | FilterItem
864
- | Function
865
- | ListWrapper
866
- | MapWrapper
867
- | Parenthetical
868
- | int
869
- | float
870
- | str
871
- | date
872
- ),
884
+ parent: ARBITRARY_INPUTS,
873
885
  ) -> str:
874
886
  """Generate a name for a concept based on its parent type and content."""
875
887
  if isinstance(parent, AggregateWrapper):
@@ -890,6 +902,8 @@ def generate_concept_name(
890
902
  return f"{VIRTUAL_CONCEPT_PREFIX}_paren_{string_to_hash(str(parent))}"
891
903
  elif isinstance(parent, FunctionCallWrapper):
892
904
  return f"{VIRTUAL_CONCEPT_PREFIX}_{parent.name}_{string_to_hash(str(parent))}"
905
+ elif isinstance(parent, Comparison):
906
+ return f"{VIRTUAL_CONCEPT_PREFIX}_comp_{string_to_hash(str(parent))}"
893
907
  else: # ListWrapper, MapWrapper, or primitive types
894
908
  return f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
895
909
 
@@ -914,6 +928,82 @@ def parenthetical_to_concept(
914
928
  )
915
929
 
916
930
 
931
+ def comparison_to_concept(
932
+ parent: Comparison,
933
+ name: str,
934
+ namespace: str,
935
+ environment: Environment,
936
+ metadata: Metadata | None = None,
937
+ ):
938
+ fmetadata = metadata or Metadata()
939
+
940
+ pkeys: List[Concept] = []
941
+ namespace = namespace or environment.namespace
942
+ is_metric = False
943
+ ref_args, is_metric = get_relevant_parent_concepts(parent)
944
+ concrete_args = [environment.concepts[c.address] for c in ref_args]
945
+ pkeys += [
946
+ x
947
+ for x in concrete_args
948
+ if not x.derivation == Derivation.CONSTANT
949
+ and not (x.derivation == Derivation.AGGREGATE and not x.grain.components)
950
+ ]
951
+ grain: Grain | None = Grain()
952
+ for x in pkeys:
953
+ grain += x.grain
954
+ if parent.operator in FunctionClass.ONE_TO_MANY.value:
955
+ # if the function will create more rows, we don't know what grain this is at
956
+ grain = None
957
+ modifiers = get_upstream_modifiers(pkeys, environment)
958
+ key_grain: list[str] = []
959
+ for x in pkeys:
960
+ # metrics will group to keys, so do not do key traversal
961
+ if is_metric:
962
+ key_grain.append(x.address)
963
+ # otherwse, for row ops, assume keys are transitive
964
+ elif x.keys:
965
+ key_grain += [*x.keys]
966
+ else:
967
+ key_grain.append(x.address)
968
+ keys = set(key_grain)
969
+ if is_metric:
970
+ purpose = Purpose.METRIC
971
+ elif not pkeys:
972
+ purpose = Purpose.CONSTANT
973
+ else:
974
+ purpose = Purpose.PROPERTY
975
+ fmetadata = metadata or Metadata()
976
+
977
+ if grain is not None:
978
+ r = Concept(
979
+ name=name,
980
+ datatype=parent.output_datatype,
981
+ purpose=purpose,
982
+ lineage=parent,
983
+ namespace=namespace,
984
+ keys=keys,
985
+ modifiers=modifiers,
986
+ grain=grain,
987
+ metadata=fmetadata,
988
+ derivation=Derivation.BASIC,
989
+ granularity=Granularity.MULTI_ROW,
990
+ )
991
+ return r
992
+
993
+ return Concept(
994
+ name=name,
995
+ datatype=parent.output_datatype,
996
+ purpose=purpose,
997
+ lineage=parent,
998
+ namespace=namespace,
999
+ keys=keys,
1000
+ modifiers=modifiers,
1001
+ metadata=fmetadata,
1002
+ derivation=Derivation.BASIC,
1003
+ granularity=Granularity.MULTI_ROW,
1004
+ )
1005
+
1006
+
917
1007
  def arbitrary_to_concept(
918
1008
  parent: ARBITRARY_INPUTS,
919
1009
  environment: Environment,
@@ -973,5 +1063,7 @@ def arbitrary_to_concept(
973
1063
  return constant_to_concept(parent, name, namespace, metadata)
974
1064
  elif isinstance(parent, Parenthetical):
975
1065
  return parenthetical_to_concept(parent, name, namespace, environment, metadata)
1066
+ elif isinstance(parent, Comparison):
1067
+ return comparison_to_concept(parent, name, namespace, environment, metadata)
976
1068
  else:
977
1069
  return constant_to_concept(parent, name, namespace, metadata)
@@ -62,6 +62,8 @@ from trilogy.core.models.author import (
62
62
  Conditional,
63
63
  CustomFunctionFactory,
64
64
  CustomType,
65
+ DeriveClause,
66
+ DeriveItem,
65
67
  Expr,
66
68
  FilterItem,
67
69
  Function,
@@ -69,6 +71,7 @@ from trilogy.core.models.author import (
69
71
  Grain,
70
72
  HavingClause,
71
73
  Metadata,
74
+ MultiSelectLineage,
72
75
  OrderBy,
73
76
  OrderItem,
74
77
  Parenthetical,
@@ -135,6 +138,7 @@ from trilogy.parsing.common import (
135
138
  align_item_to_concept,
136
139
  arbitrary_to_concept,
137
140
  constant_to_concept,
141
+ derive_item_to_concept,
138
142
  process_function_args,
139
143
  rowset_to_concepts,
140
144
  )
@@ -603,6 +607,9 @@ class ParseToObjects(Transformer):
603
607
  def PROPERTY(self, args):
604
608
  return Purpose.PROPERTY
605
609
 
610
+ def HASH_TYPE(self, args):
611
+ return args.value
612
+
606
613
  @v_args(meta=True)
607
614
  def prop_ident(self, meta: Meta, args) -> Tuple[List[Concept], str]:
608
615
  return [self.environment.concepts[grain] for grain in args[:-1]], args[-1]
@@ -707,7 +714,14 @@ class ParseToObjects(Transformer):
707
714
 
708
715
  if isinstance(
709
716
  source_value,
710
- (FilterItem, WindowItem, AggregateWrapper, Function, FunctionCallWrapper),
717
+ (
718
+ FilterItem,
719
+ WindowItem,
720
+ AggregateWrapper,
721
+ Function,
722
+ FunctionCallWrapper,
723
+ Comparison,
724
+ ),
711
725
  ):
712
726
  concept = arbitrary_to_concept(
713
727
  source_value,
@@ -1275,6 +1289,17 @@ class ParseToObjects(Transformer):
1275
1289
  def align_clause(self, meta: Meta, args) -> AlignClause:
1276
1290
  return AlignClause(items=args)
1277
1291
 
1292
+ @v_args(meta=True)
1293
+ def derive_item(self, meta: Meta, args) -> DeriveItem:
1294
+ return DeriveItem(
1295
+ expr=args[0], name=args[1], namespace=self.environment.namespace
1296
+ )
1297
+
1298
+ @v_args(meta=True)
1299
+ def derive_clause(self, meta: Meta, args) -> DeriveClause:
1300
+
1301
+ return DeriveClause(items=args)
1302
+
1278
1303
  @v_args(meta=True)
1279
1304
  def multi_select_statement(self, meta: Meta, args) -> MultiSelectStatement:
1280
1305
 
@@ -1284,6 +1309,7 @@ class ParseToObjects(Transformer):
1284
1309
  order_by: OrderBy | None = None
1285
1310
  where: WhereClause | None = None
1286
1311
  having: HavingClause | None = None
1312
+ derive: DeriveClause | None = None
1287
1313
  for arg in args:
1288
1314
  if isinstance(arg, SelectStatement):
1289
1315
  selects.append(arg)
@@ -1297,11 +1323,24 @@ class ParseToObjects(Transformer):
1297
1323
  having = arg
1298
1324
  elif isinstance(arg, AlignClause):
1299
1325
  align = arg
1326
+ elif isinstance(arg, DeriveClause):
1327
+ derive = arg
1300
1328
 
1301
1329
  assert align
1302
1330
  assert align is not None
1303
1331
 
1304
1332
  derived_concepts = []
1333
+ new_selects = [x.as_lineage(self.environment) for x in selects]
1334
+ lineage = MultiSelectLineage(
1335
+ selects=new_selects,
1336
+ align=align,
1337
+ derive=derive,
1338
+ namespace=self.environment.namespace,
1339
+ where_clause=where,
1340
+ having_clause=having,
1341
+ limit=limit,
1342
+ hidden_components=set(y for x in new_selects for y in x.hidden_components),
1343
+ )
1305
1344
  for x in align.items:
1306
1345
  concept = align_item_to_concept(
1307
1346
  x,
@@ -1314,6 +1353,19 @@ class ParseToObjects(Transformer):
1314
1353
  )
1315
1354
  derived_concepts.append(concept)
1316
1355
  self.environment.add_concept(concept, meta=meta)
1356
+ if derive:
1357
+ for derived in derive.items:
1358
+ derivation = derived.expr
1359
+ name = derived.name
1360
+ if not isinstance(derivation, (Function, Comparison, WindowItem)):
1361
+ raise SyntaxError(
1362
+ f"Invalid derive expression {derivation} in {meta.line}, must be a function or conditional"
1363
+ )
1364
+ concept = derive_item_to_concept(
1365
+ derivation, name, lineage, self.environment.namespace
1366
+ )
1367
+ derived_concepts.append(concept)
1368
+ self.environment.add_concept(concept, meta=meta)
1317
1369
  multi = MultiSelectStatement(
1318
1370
  selects=selects,
1319
1371
  align=align,
@@ -1323,6 +1375,7 @@ class ParseToObjects(Transformer):
1323
1375
  limit=limit,
1324
1376
  meta=Metadata(line_number=meta.line),
1325
1377
  derived_concepts=derived_concepts,
1378
+ derive=derive,
1326
1379
  )
1327
1380
  return multi
1328
1381
 
@@ -1883,6 +1936,10 @@ class ParseToObjects(Transformer):
1883
1936
  def ftrim(self, meta, args):
1884
1937
  return self.function_factory.create_function(args, FunctionType.TRIM, meta)
1885
1938
 
1939
+ @v_args(meta=True)
1940
+ def fhash(self, meta, args):
1941
+ return self.function_factory.create_function(args, FunctionType.HASH, meta)
1942
+
1886
1943
  @v_args(meta=True)
1887
1944
  def fsubstring(self, meta, args):
1888
1945
  return self.function_factory.create_function(args, FunctionType.SUBSTRING, meta)
@@ -74,12 +74,16 @@
74
74
  select_statement: where? "select"i select_list where? having? order_by? limit?
75
75
 
76
76
  // multiple_selects
77
- multi_select_statement: select_statement ("merge" select_statement)+ "align"i align_clause where? order_by? limit?
77
+ multi_select_statement: select_statement ("merge" select_statement)+ "align"i align_clause ("derive" derive_clause)? where? order_by? limit?
78
78
 
79
79
  align_item: IDENTIFIER ":" IDENTIFIER ("," IDENTIFIER)* ","?
80
80
 
81
81
  align_clause: align_item ("AND"i align_item)* "AND"i?
82
82
 
83
+ derive_item: expr "->" IDENTIFIER
84
+
85
+ derive_clause: derive_item ("," derive_item)* ","?
86
+
83
87
  merge_statement: "merge"i WILDCARD_IDENTIFIER "into"i SHORTHAND_MODIFIER? WILDCARD_IDENTIFIER
84
88
 
85
89
  // raw sql statement
@@ -308,8 +312,11 @@
308
312
  fregexp_contains: _REGEXP_CONTAINS expr "," expr ")"
309
313
  _REGEXP_REPLACE.1: "regexp_replace("
310
314
  fregexp_replace: _REGEXP_REPLACE expr "," expr "," expr ")"
315
+ _HASH.1: "hash("
316
+ HASH_TYPE: "md5"i | "sha1"i | "sha256"i | "sha512"i
317
+ fhash: _HASH expr "," HASH_TYPE ")"
311
318
 
312
- _string_functions: like | ilike | upper | flower | fsplit | fstrpos | fsubstring | fcontains | ftrim | freplace | fregexp_extract | fregexp_contains | fregexp_replace
319
+ _string_functions: like | ilike | upper | flower | fsplit | fstrpos | fsubstring | fcontains | ftrim | freplace | fregexp_extract | fregexp_contains | fregexp_replace | fhash
313
320
 
314
321
  //array_functions
315
322
  _ARRAY_SUM.1: "array_sum("i