1use crate::{Error, Result};
20
21#[derive(Debug, Clone, PartialEq)]
26pub enum WriteTimeTtlKind {
27 WriteTime,
28 Ttl,
29}
30
31impl WriteTimeTtlKind {
32 fn cassandra_fn_name(&self) -> &'static str {
34 match self {
35 WriteTimeTtlKind::WriteTime => "writeTime",
36 WriteTimeTtlKind::Ttl => "ttl",
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
46pub struct ColumnDescriptor {
47 pub name: String,
49 pub type_str: String,
51 pub is_partition_key: bool,
53 pub is_clustering_key: bool,
55}
56
57pub fn validate_writetime_ttl_call(
70 kind: &WriteTimeTtlKind,
71 column_name: &str,
72 columns: &[ColumnDescriptor],
73) -> Result<()> {
74 let col = columns
76 .iter()
77 .find(|c| c.name.eq_ignore_ascii_case(column_name))
78 .ok_or_else(|| {
79 Error::cql_parse(format!(
80 "Undefined column name {} in selection clause",
81 column_name
82 ))
83 })?;
84
85 if col.is_partition_key || col.is_clustering_key {
86 return Err(Error::cql_parse(format!(
87 "Cannot use selection function {} on PRIMARY KEY part {}",
88 kind.cassandra_fn_name(),
89 col.name,
90 )));
91 }
92
93 if is_non_frozen_collection(&col.type_str) {
94 return Err(Error::cql_parse(format!(
95 "Cannot use selection function {} on non-frozen {}",
96 kind.cassandra_fn_name(),
97 col.type_str,
98 )));
99 }
100
101 Ok(())
102}
103
104pub fn validate_all_writetime_ttl_calls(
108 calls: &[(WriteTimeTtlKind, String)],
109 columns: &[ColumnDescriptor],
110) -> Result<()> {
111 for (kind, column_name) in calls {
112 validate_writetime_ttl_call(kind, column_name, columns)?;
113 }
114 Ok(())
115}
116
117pub fn descriptors_from_table_schema(schema: &crate::schema::TableSchema) -> Vec<ColumnDescriptor> {
121 let pk_names: std::collections::HashSet<&str> = schema
122 .partition_keys
123 .iter()
124 .map(|k| k.name.as_str())
125 .collect();
126 let ck_names: std::collections::HashSet<&str> = schema
127 .clustering_keys
128 .iter()
129 .map(|k| k.name.as_str())
130 .collect();
131
132 schema
133 .columns
134 .iter()
135 .map(|col| ColumnDescriptor {
136 name: col.name.clone(),
137 type_str: col.data_type.clone(),
138 is_partition_key: pk_names.contains(col.name.as_str()),
139 is_clustering_key: ck_names.contains(col.name.as_str()),
140 })
141 .collect()
142}
143
144fn is_non_frozen_collection(type_str: &str) -> bool {
150 let t = type_str.trim().to_lowercase();
151 if t.starts_with("frozen<") {
153 return false;
154 }
155 t.starts_with("list<") || t.starts_with("set<") || t.starts_with("map<")
156}
157
158#[cfg(feature = "state_machine")]
161pub fn extract_writetime_ttl_calls(
162 stmt: &super::select_ast::SelectStatement,
163) -> Vec<(WriteTimeTtlKind, String)> {
164 use super::select_ast::{SelectClause, SelectExpression, WriteTimeTtlFunction};
165
166 let exprs = match &stmt.select_clause {
167 SelectClause::Columns(v) | SelectClause::Distinct(v) => v,
168 SelectClause::All => return vec![],
169 };
170
171 exprs
172 .iter()
173 .filter_map(|expr| {
174 let call = match expr {
175 SelectExpression::WriteTimeTtl(c) => c,
176 SelectExpression::Aliased(inner, _) => {
177 if let SelectExpression::WriteTimeTtl(c) = inner.as_ref() {
178 c
179 } else {
180 return None;
181 }
182 }
183 _ => return None,
184 };
185 let kind = match call.function {
186 WriteTimeTtlFunction::WriteTime => WriteTimeTtlKind::WriteTime,
187 WriteTimeTtlFunction::Ttl => WriteTimeTtlKind::Ttl,
188 };
189 Some((kind, call.column.clone()))
190 })
191 .collect()
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn col(name: &str, type_str: &str, is_pk: bool, is_ck: bool) -> ColumnDescriptor {
199 ColumnDescriptor {
200 name: name.to_string(),
201 type_str: type_str.to_string(),
202 is_partition_key: is_pk,
203 is_clustering_key: is_ck,
204 }
205 }
206
207 fn basic_columns() -> Vec<ColumnDescriptor> {
208 vec![
209 col("user_id", "uuid", true, false),
210 col("bucket", "int", false, true),
211 col("name", "text", false, false),
212 col("scores", "list<int>", false, false),
213 col("tags", "set<text>", false, false),
214 col("meta", "map<text,text>", false, false),
215 col("frozen_scores", "frozen<list<int>>", false, false),
216 col("data", "blob", false, false),
217 ]
218 }
219
220 #[test]
223 fn test_writetime_on_regular_column_is_valid() {
224 let cols = basic_columns();
225 let result = validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "name", &cols);
226 assert!(
227 result.is_ok(),
228 "WRITETIME on a plain text column should be valid"
229 );
230 }
231
232 #[test]
233 fn test_ttl_on_regular_column_is_valid() {
234 let cols = basic_columns();
235 let result = validate_writetime_ttl_call(&WriteTimeTtlKind::Ttl, "name", &cols);
236 assert!(result.is_ok(), "TTL on a plain text column should be valid");
237 }
238
239 #[test]
240 fn test_writetime_on_frozen_collection_is_valid() {
241 let cols = basic_columns();
242 let result =
243 validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "frozen_scores", &cols);
244 assert!(
245 result.is_ok(),
246 "WRITETIME on a frozen<list<int>> should be valid"
247 );
248 }
249
250 #[test]
251 fn test_writetime_on_blob_is_valid() {
252 let cols = basic_columns();
253 let result = validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "data", &cols);
254 assert!(result.is_ok());
255 }
256
257 #[test]
260 fn test_writetime_on_partition_key_is_rejected() {
261 let cols = basic_columns();
262 let err = validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "user_id", &cols)
263 .unwrap_err();
264 let msg = err.to_string();
265 assert!(
266 msg.contains("writeTime"),
267 "Error should name the function: {msg}"
268 );
269 assert!(
270 msg.contains("PRIMARY KEY"),
271 "Error should mention PRIMARY KEY: {msg}"
272 );
273 assert!(
274 msg.contains("user_id"),
275 "Error should name the column: {msg}"
276 );
277 }
278
279 #[test]
280 fn test_ttl_on_partition_key_is_rejected() {
281 let cols = basic_columns();
282 let err =
283 validate_writetime_ttl_call(&WriteTimeTtlKind::Ttl, "user_id", &cols).unwrap_err();
284 let msg = err.to_string();
285 assert!(msg.contains("ttl"), "Error should name the function: {msg}");
286 assert!(
287 msg.contains("PRIMARY KEY"),
288 "Error should mention PRIMARY KEY: {msg}"
289 );
290 }
291
292 #[test]
295 fn test_writetime_on_clustering_key_is_rejected() {
296 let cols = basic_columns();
297 let err =
298 validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "bucket", &cols).unwrap_err();
299 let msg = err.to_string();
300 assert!(
301 msg.contains("PRIMARY KEY"),
302 "Clustering-key error should cite PRIMARY KEY: {msg}"
303 );
304 assert!(msg.contains("bucket"));
305 }
306
307 #[test]
310 fn test_writetime_on_list_is_rejected() {
311 let cols = basic_columns();
312 let err =
313 validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "scores", &cols).unwrap_err();
314 let msg = err.to_string();
315 assert!(
316 msg.contains("non-frozen"),
317 "Error should cite non-frozen: {msg}"
318 );
319 assert!(
320 msg.contains("list<int>"),
321 "Error should include the type: {msg}"
322 );
323 }
324
325 #[test]
326 fn test_writetime_on_set_is_rejected() {
327 let cols = basic_columns();
328 let err =
329 validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "tags", &cols).unwrap_err();
330 let msg = err.to_string();
331 assert!(msg.contains("non-frozen"));
332 }
333
334 #[test]
335 fn test_writetime_on_map_is_rejected() {
336 let cols = basic_columns();
337 let err =
338 validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "meta", &cols).unwrap_err();
339 let msg = err.to_string();
340 assert!(msg.contains("non-frozen"));
341 }
342
343 #[test]
344 fn test_ttl_on_set_is_rejected() {
345 let cols = basic_columns();
346 let err = validate_writetime_ttl_call(&WriteTimeTtlKind::Ttl, "tags", &cols).unwrap_err();
347 let msg = err.to_string();
348 assert!(msg.contains("non-frozen"));
349 assert!(msg.contains("ttl"));
350 }
351
352 #[test]
355 fn test_writetime_on_unknown_column_is_rejected() {
356 let cols = basic_columns();
357 let err =
358 validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "nonexistent_col", &cols)
359 .unwrap_err();
360 let msg = err.to_string();
361 assert!(
362 msg.contains("nonexistent_col"),
363 "Error should name the unknown column: {msg}"
364 );
365 }
366
367 #[test]
370 fn test_column_lookup_is_case_insensitive() {
371 let cols = basic_columns();
372 let result = validate_writetime_ttl_call(&WriteTimeTtlKind::WriteTime, "NAME", &cols);
374 assert!(result.is_ok(), "Column lookup should be case-insensitive");
375 }
376
377 #[test]
380 fn test_validate_all_calls_stops_at_first_error() {
381 let cols = basic_columns();
382 let calls = vec![
383 (WriteTimeTtlKind::WriteTime, "name".to_string()),
384 (WriteTimeTtlKind::Ttl, "user_id".to_string()), (WriteTimeTtlKind::WriteTime, "data".to_string()),
386 ];
387 let result = validate_all_writetime_ttl_calls(&calls, &cols);
388 assert!(
389 result.is_err(),
390 "Batch validation should fail on the PK column"
391 );
392 }
393
394 #[test]
395 fn test_validate_all_calls_succeeds_when_all_valid() {
396 let cols = basic_columns();
397 let calls = vec![
398 (WriteTimeTtlKind::WriteTime, "name".to_string()),
399 (WriteTimeTtlKind::Ttl, "data".to_string()),
400 ];
401 assert!(validate_all_writetime_ttl_calls(&calls, &cols).is_ok());
402 }
403
404 #[test]
407 fn test_non_frozen_collection_detection() {
408 assert!(is_non_frozen_collection("list<int>"));
409 assert!(is_non_frozen_collection("set<text>"));
410 assert!(is_non_frozen_collection("map<text,int>"));
411 assert!(is_non_frozen_collection("LIST<INT>")); assert!(!is_non_frozen_collection("frozen<list<int>>"));
413 assert!(!is_non_frozen_collection("frozen<set<text>>"));
414 assert!(!is_non_frozen_collection("frozen<map<text,int>>"));
415 assert!(!is_non_frozen_collection("text"));
416 assert!(!is_non_frozen_collection("bigint"));
417 assert!(!is_non_frozen_collection("uuid"));
418 }
419}