use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub name: String,
pub type_name: String,
}
impl ColumnSpec {
pub fn new(name: impl Into<String>, type_name: impl Into<String>) -> Self {
Self {
name: name.into(),
type_name: type_name.into().to_ascii_lowercase(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitLogSchema {
pub keyspace: String,
pub table: String,
pub partition_key: Vec<ColumnSpec>,
pub clustering: Vec<ColumnSpec>,
pub columns: Vec<ColumnSpec>,
}
impl CommitLogSchema {
pub fn column_type(&self, name: &str) -> Option<&str> {
self.columns
.iter()
.find(|c| c.name == name)
.map(|c| c.type_name.as_str())
}
}
pub type SchemaSet = HashMap<[u8; 16], CommitLogSchema>;
pub fn cql_fixed_len(type_name: &str) -> Option<usize> {
match type_name.trim().to_ascii_lowercase().as_str() {
"boolean" => Some(1),
"int" | "float" => Some(4),
"bigint" | "timestamp" | "double" => Some(8),
"uuid" | "timeuuid" => Some(16),
_ => None,
}
}
pub fn is_simple_scalar_type(type_name: &str) -> bool {
let t = type_name.trim().to_ascii_lowercase();
matches!(
t.as_str(),
"boolean"
| "tinyint"
| "smallint"
| "int"
| "date"
| "float"
| "bigint"
| "timestamp"
| "time"
| "double"
| "uuid"
| "timeuuid"
| "text"
| "varchar"
| "ascii"
| "blob"
| "varint"
| "decimal"
| "inet"
| "counter"
)
}
pub fn format_table_id(id: &[u8; 16]) -> String {
let hex: String = id.iter().map(|b| format!("{b:02x}")).collect();
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32],
)
}
pub fn parse_table_id(s: &str) -> Option<[u8; 16]> {
let hex: String = s.chars().filter(|c| *c != '-').collect();
if !hex.is_ascii() || hex.len() != 32 {
return None;
}
let mut out = [0u8; 16];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_lengths_match_cassandra() {
assert_eq!(cql_fixed_len("int"), Some(4));
assert_eq!(cql_fixed_len("bigint"), Some(8));
assert_eq!(cql_fixed_len("uuid"), Some(16));
assert_eq!(cql_fixed_len("boolean"), Some(1));
assert_eq!(cql_fixed_len("text"), None);
assert_eq!(cql_fixed_len("blob"), None);
assert_eq!(cql_fixed_len("varint"), None);
}
#[test]
fn table_id_round_trips() {
let s = "d6de7150-8448-11f1-bf5a-bf4cbc47cb4a";
let id = parse_table_id(s).expect("parse");
assert_eq!(format_table_id(&id), s);
}
#[test]
fn counter_is_not_fixed_length() {
assert_eq!(cql_fixed_len("counter"), None);
}
#[test]
fn tinyint_smallint_date_time_are_not_fixed_length() {
for t in ["tinyint", "smallint", "date", "time"] {
assert_eq!(cql_fixed_len(t), None, "{t} must be variable-length");
assert!(is_simple_scalar_type(t), "{t} stays a simple scalar");
}
}
#[test]
fn simple_scalar_type_allowlist_excludes_complex_types() {
for t in [
"int", "bigint", "text", "blob", "uuid", "counter", "decimal", "varint", "inet",
] {
assert!(is_simple_scalar_type(t), "{t} should be a simple scalar");
}
for t in [
"list<text>",
"set<int>",
"map<text, int>",
"frozen<list<int>>",
"tuple<int, text>",
"vector<float, 4>",
"some_udt_name",
] {
assert!(
!is_simple_scalar_type(t),
"{t} must bail, not be treated as a simple scalar"
);
}
}
#[test]
fn parse_table_id_rejects_non_ascii_without_panicking() {
assert_eq!(parse_table_id("d6de7150-8448-11f1-bf5a-bf4cbc47cb4€"), None);
}
}