Skip to main content

cqlc_core/
schema.rs

1//! The resolved schema model.
2
3use crate::span::Span;
4use crate::types::{CqlType, Order};
5
6/// The role of a column in the primary key.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ColumnRole {
10    /// Part of the partition key.
11    PartitionKey,
12    /// A clustering column.
13    ClusteringKey,
14    /// Not part of the primary key.
15    Regular,
16}
17
18/// A table column.
19#[derive(Clone, Debug, PartialEq)]
20#[non_exhaustive]
21pub struct Column {
22    /// Normalized column name.
23    pub name: String,
24    /// The declared type.
25    pub cql_type: CqlType,
26    /// Position in the primary key.
27    pub role: ColumnRole,
28    /// Declared `STATIC`.
29    pub is_static: bool,
30    /// Declared `MASKED WITH ...` (Cassandra 5.0 dynamic data masking).
31    pub masked: bool,
32    /// Where the column is defined.
33    pub span: Span,
34}
35
36impl Column {
37    /// Whether the column is part of the primary key.
38    pub fn is_key(&self) -> bool {
39        self.role != ColumnRole::Regular
40    }
41
42    /// Primary key columns are never null; everything else may be absent.
43    pub fn nullable(&self) -> bool {
44        !self.is_key()
45    }
46}
47
48/// A clustering column with its sort order.
49#[derive(Clone, Debug, PartialEq)]
50#[non_exhaustive]
51pub struct ClusteringColumn {
52    /// Column name.
53    pub name: String,
54    /// Declared order (`CLUSTERING ORDER BY`), ascending by default.
55    pub order: Order,
56}
57
58/// A table definition.
59#[derive(Clone, Debug, PartialEq)]
60#[non_exhaustive]
61pub struct Table {
62    /// Keyspace, if the `CREATE TABLE` qualified the name.
63    pub keyspace: Option<String>,
64    /// Table name.
65    pub name: String,
66    /// In declaration order.
67    pub columns: Vec<Column>,
68    /// Names of the partition key columns, in key order.
69    pub partition_key: Vec<String>,
70    /// Clustering columns, in key order.
71    pub clustering: Vec<ClusteringColumn>,
72    /// For a materialized view: the base table it reads from. Views are read-only.
73    pub base_table: Option<String>,
74    /// Secondary indexes (legacy, SAI, SASI or custom) defined on this table.
75    pub indexes: Vec<Index>,
76    /// Where the table is defined.
77    pub span: Span,
78}
79
80/// Which kind of index implementation serves a column.
81#[derive(Clone, Debug, PartialEq, Eq)]
82#[non_exhaustive]
83pub enum IndexClass {
84    /// `CREATE INDEX` without `USING`: the classic secondary index (equality, `CONTAINS`).
85    Legacy,
86    /// Storage-Attached Index, `USING 'sai'` (Cassandra 5.0): equality, ranges, `CONTAINS`, vector `ANN`.
87    Sai,
88    /// SASI, `USING '...SASIIndex'`: equality, ranges and `LIKE`.
89    Sasi,
90    /// Any other custom index class; cqlc does not know its capabilities.
91    Custom(String),
92}
93
94impl IndexClass {
95    /// Classifies a `USING 'class'` string.
96    pub fn from_using(class: Option<&str>) -> IndexClass {
97        match class.map(str::to_ascii_lowercase).as_deref() {
98            None => IndexClass::Legacy,
99            Some(c) if c == "sai" || c.contains("storageattachedindex") => IndexClass::Sai,
100            Some(c) if c.contains("sasi") => IndexClass::Sasi,
101            Some(_) => IndexClass::Custom(class.unwrap_or_default().to_string()),
102        }
103    }
104}
105
106/// A secondary index on one column.
107#[derive(Clone, Debug, PartialEq)]
108#[non_exhaustive]
109pub struct Index {
110    /// Index name, if given.
111    pub name: Option<String>,
112    /// The indexed column.
113    pub column: String,
114    /// Which part of the column is indexed.
115    pub kind: crate::ast::IndexKind,
116    /// Implementation.
117    pub class: IndexClass,
118}
119
120/// A user-defined function or aggregate.
121#[derive(Clone, Debug, PartialEq)]
122#[non_exhaustive]
123pub struct Function {
124    /// Function name (keyspace dropped).
125    pub name: String,
126    /// Argument types.
127    pub args: Vec<CqlType>,
128    /// Result type.
129    pub returns: CqlType,
130    /// `true` for `CREATE AGGREGATE`.
131    pub aggregate: bool,
132}
133
134impl Table {
135    /// Whether `column` has an index that can serve `op` (see [`Table::indexes`]).
136    pub fn index_for(&self, column: &str) -> Option<&Index> {
137        self.indexes.iter().find(|i| i.column == column)
138    }
139
140    /// Finds a column by its normalized name.
141    pub fn column(&self, name: &str) -> Option<&Column> {
142        self.columns.iter().find(|c| c.name == name)
143    }
144
145    /// Whether the table has a counter column.
146    pub fn is_counter_table(&self) -> bool {
147        self.columns
148            .iter()
149            .any(|c| matches!(c.cql_type, CqlType::Counter))
150    }
151
152    /// `keyspace.table`, or just the table name if unqualified.
153    pub fn qualified_name(&self) -> String {
154        match &self.keyspace {
155            Some(ks) => format!("{ks}.{}", self.name),
156            None => self.name.clone(),
157        }
158    }
159}
160
161/// A user-defined type.
162#[derive(Clone, Debug, PartialEq)]
163#[non_exhaustive]
164pub struct Udt {
165    /// Keyspace, if qualified.
166    pub keyspace: Option<String>,
167    /// Type name.
168    pub name: String,
169    /// Fields in declaration order.
170    pub fields: Vec<(String, CqlType)>,
171    /// Where the type is defined.
172    pub span: Span,
173}
174
175/// All tables and user-defined types.
176#[derive(Clone, Debug, Default, PartialEq)]
177#[non_exhaustive]
178pub struct Schema {
179    /// Tables in definition order.
180    pub tables: Vec<Table>,
181    /// User-defined types in definition order.
182    pub udts: Vec<Udt>,
183    /// User-defined functions and aggregates.
184    pub functions: Vec<Function>,
185}
186
187impl Schema {
188    /// Finds a user-defined function or aggregate by name.
189    pub fn function(&self, name: &str) -> Option<&Function> {
190        self.functions.iter().find(|f| f.name == name)
191    }
192
193    /// Finds a user-defined type by name.
194    pub fn udt(&self, name: &str) -> Option<&Udt> {
195        self.udts.iter().find(|u| u.name == name)
196    }
197}
198
199/// Result of [`Schema::lookup_table`].
200#[derive(Debug)]
201#[non_exhaustive]
202pub enum Lookup<'a> {
203    /// Exactly one table matches.
204    Found(&'a Table),
205    /// No table matches.
206    NotFound,
207    /// Unqualified name that exists in several keyspaces.
208    Ambiguous(Vec<&'a Table>),
209}
210
211impl Schema {
212    /// Qualified names must match a table declared in that keyspace;
213    /// unqualified names match by table name alone.
214    pub fn lookup_table(&self, keyspace: Option<&str>, name: &str) -> Lookup<'_> {
215        let matches: Vec<&Table> = self
216            .tables
217            .iter()
218            .filter(|t| {
219                t.name == name && keyspace.is_none_or(|ks| t.keyspace.as_deref() == Some(ks))
220            })
221            .collect();
222        match matches.len() {
223            0 => Lookup::NotFound,
224            1 => Lookup::Found(matches[0]),
225            _ => Lookup::Ambiguous(matches),
226        }
227    }
228}