Skip to main content

drizzle_types/sqlite/ddl/
index.rs

1//! `SQLite` indexes: [`IndexDef`] (const) and [`Index`] (runtime), with
2//! their key parts [`IndexColumnDef`] and [`IndexColumn`].
3
4use crate::alloc_prelude::*;
5
6#[cfg(feature = "serde")]
7use crate::serde_helpers::{cow_from_string, cow_option_from_string};
8
9// =============================================================================
10// Index Origin
11// =============================================================================
12
13/// How an index was created.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
17pub enum IndexOrigin {
18    /// By `CREATE INDEX` (the default).
19    #[default]
20    Manual,
21    /// By `SQLite` itself, for a `UNIQUE` constraint.
22    Auto,
23}
24
25// =============================================================================
26// Const-friendly Definition Types
27// =============================================================================
28
29/// One key part of an [`IndexDef`]: a column name or a SQL expression.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
33pub struct IndexColumnDef {
34    /// Column name or expression
35    pub value: &'static str,
36    /// Whether this is an expression (vs column name)
37    #[cfg_attr(feature = "serde", serde(default))]
38    pub is_expression: bool,
39}
40
41impl IndexColumnDef {
42    /// Creates a key part for a column.
43    #[must_use]
44    pub const fn new(value: &'static str) -> Self {
45        Self {
46            value,
47            is_expression: false,
48        }
49    }
50
51    /// Creates a key part for a SQL expression.
52    #[must_use]
53    pub const fn expression(value: &'static str) -> Self {
54        Self {
55            value,
56            is_expression: true,
57        }
58    }
59
60    /// Converts to the runtime [`IndexColumn`].
61    #[must_use]
62    pub const fn into_column(self) -> IndexColumn {
63        IndexColumn {
64            value: Cow::Borrowed(self.value),
65            is_expression: self.is_expression,
66        }
67    }
68}
69
70/// One key part of an [`Index`]: a column name or a SQL expression.
71#[derive(Clone, Debug, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
74pub struct IndexColumn {
75    /// Column name or expression
76    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
77    pub value: Cow<'static, str>,
78    /// Whether this is an expression (vs column name)
79    #[cfg_attr(feature = "serde", serde(default))]
80    pub is_expression: bool,
81}
82
83impl IndexColumn {
84    /// Creates a key part for a column.
85    #[must_use]
86    pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
87        Self {
88            value: value.into(),
89            is_expression: false,
90        }
91    }
92
93    /// Creates a key part for a SQL expression.
94    #[must_use]
95    pub fn expression(value: impl Into<Cow<'static, str>>) -> Self {
96        Self {
97            value: value.into(),
98            is_expression: true,
99        }
100    }
101}
102
103impl IndexColumn {
104    /// Renders the key part: the column name in backticks, or the
105    /// expression in parentheses.
106    #[must_use]
107    pub fn to_sql(&self) -> String {
108        if self.is_expression {
109            format!("({})", self.value)
110        } else {
111            format!("`{}`", self.value.replace('`', "``"))
112        }
113    }
114}
115
116impl From<IndexColumnDef> for IndexColumn {
117    fn from(def: IndexColumnDef) -> Self {
118        def.into_column()
119    }
120}
121
122/// An index that can be built in a `const`.
123///
124/// # Examples
125///
126/// ```
127/// use drizzle_types::sqlite::ddl::{IndexDef, IndexColumnDef};
128///
129/// const COLS: &[IndexColumnDef] = &[
130///     IndexColumnDef::new("email"),
131///     IndexColumnDef::new("created_at"),
132/// ];
133///
134/// const IDX: IndexDef = IndexDef::new("users", "idx_users_email")
135///     .unique()
136///     .columns(COLS);
137/// ```
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
139pub struct IndexDef {
140    /// Parent table name
141    pub table: &'static str,
142    /// Index name
143    pub name: &'static str,
144    /// Index columns
145    pub columns: &'static [IndexColumnDef],
146    /// Is this a UNIQUE index?
147    pub is_unique: bool,
148    /// `WHERE` condition of a partial index, as SQL
149    pub where_clause: Option<&'static str>,
150    /// How the index was created
151    pub origin: IndexOrigin,
152}
153
154impl IndexDef {
155    /// Creates a non-unique index with no columns; set them with
156    /// [`columns`](Self::columns()).
157    #[must_use]
158    pub const fn new(table: &'static str, name: &'static str) -> Self {
159        Self {
160            table,
161            name,
162            columns: &[],
163            is_unique: false,
164            where_clause: None,
165            origin: IndexOrigin::Manual,
166        }
167    }
168
169    /// Makes the index `UNIQUE`.
170    #[must_use]
171    pub const fn unique(self) -> Self {
172        Self {
173            is_unique: true,
174            ..self
175        }
176    }
177
178    /// Sets the key parts.
179    #[must_use]
180    pub const fn columns(self, columns: &'static [IndexColumnDef]) -> Self {
181        Self { columns, ..self }
182    }
183
184    /// Sets the `WHERE` condition, making it a partial index.
185    #[must_use]
186    pub const fn where_clause(self, clause: &'static str) -> Self {
187        Self {
188            where_clause: Some(clause),
189            ..self
190        }
191    }
192
193    /// Marks the index as created by `SQLite` for a `UNIQUE` constraint.
194    #[must_use]
195    pub const fn auto_origin(self) -> Self {
196        Self {
197            origin: IndexOrigin::Auto,
198            ..self
199        }
200    }
201
202    /// Converts to the runtime [`Index`].
203    #[must_use]
204    pub fn into_index(self) -> Index {
205        Index {
206            table: Cow::Borrowed(self.table),
207            name: Cow::Borrowed(self.name),
208            columns: self.columns.iter().map(|c| IndexColumn::from(*c)).collect(),
209            is_unique: self.is_unique,
210            where_clause: self.where_clause.map(Cow::Borrowed),
211            origin: self.origin,
212        }
213    }
214}
215
216impl Default for IndexDef {
217    fn default() -> Self {
218        Self::new("", "")
219    }
220}
221
222// =============================================================================
223// Runtime Type for Serde
224// =============================================================================
225
226/// An index, as stored in migration snapshots.
227#[derive(Clone, Debug, PartialEq, Eq)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
229#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
230pub struct Index {
231    /// Parent table name
232    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
233    pub table: Cow<'static, str>,
234
235    /// Index name
236    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
237    pub name: Cow<'static, str>,
238
239    /// Columns included in the index
240    pub columns: Vec<IndexColumn>,
241
242    /// Is this a unique index?
243    #[cfg_attr(feature = "serde", serde(default))]
244    pub is_unique: bool,
245
246    /// `WHERE` condition of a partial index, as SQL (serialized as `where`)
247    #[cfg_attr(
248        feature = "serde",
249        serde(
250            default,
251            skip_serializing_if = "Option::is_none",
252            rename = "where",
253            deserialize_with = "cow_option_from_string"
254        )
255    )]
256    pub where_clause: Option<Cow<'static, str>>,
257
258    /// How the index was created
259    #[cfg_attr(feature = "serde", serde(default))]
260    pub origin: IndexOrigin,
261}
262
263impl Index {
264    /// Creates a non-unique index.
265    #[must_use]
266    pub fn new(
267        table: impl Into<Cow<'static, str>>,
268        name: impl Into<Cow<'static, str>>,
269        columns: Vec<IndexColumn>,
270    ) -> Self {
271        Self {
272            table: table.into(),
273            name: name.into(),
274            columns,
275            is_unique: false,
276            where_clause: None,
277            origin: IndexOrigin::Manual,
278        }
279    }
280
281    /// Makes the index `UNIQUE`.
282    #[must_use]
283    pub const fn unique(mut self) -> Self {
284        self.is_unique = true;
285        self
286    }
287
288    /// Returns the index name.
289    #[inline]
290    #[must_use]
291    pub fn name(&self) -> &str {
292        &self.name
293    }
294
295    /// Returns the table name.
296    #[inline]
297    #[must_use]
298    pub fn table(&self) -> &str {
299        &self.table
300    }
301}
302
303impl Default for Index {
304    fn default() -> Self {
305        Self::new("", "", vec![])
306    }
307}
308
309impl From<IndexDef> for Index {
310    fn from(def: IndexDef) -> Self {
311        def.into_index()
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn column_names_with_backticks_are_escaped() {
321        let column = IndexColumn::new("a`b");
322        assert_eq!(column.to_sql(), "`a``b`");
323    }
324
325    #[test]
326    fn test_const_index_def() {
327        const COLS: &[IndexColumnDef] = &[
328            IndexColumnDef::new("email"),
329            IndexColumnDef::new("created_at"),
330        ];
331
332        const IDX: IndexDef = IndexDef::new("users", "idx_users_email")
333            .unique()
334            .columns(COLS);
335
336        assert_eq!(IDX.name, "idx_users_email");
337        assert_eq!(IDX.table, "users");
338        const {
339            assert!(IDX.is_unique);
340        }
341        assert_eq!(IDX.columns.len(), 2);
342    }
343
344    #[test]
345    fn test_expression_column() {
346        const COLS: &[IndexColumnDef] = &[IndexColumnDef::expression("lower(email)")];
347
348        const IDX: IndexDef = IndexDef::new("users", "idx_email_lower").columns(COLS);
349
350        assert!(IDX.columns[0].is_expression);
351    }
352
353    #[test]
354    fn test_index_def_to_index() {
355        const COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
356        const DEF: IndexDef = IndexDef::new("users", "idx_email").unique().columns(COLS);
357
358        let idx = DEF.into_index();
359        assert_eq!(idx.name(), "idx_email");
360        assert!(idx.is_unique);
361        assert_eq!(idx.columns.len(), 1);
362    }
363
364    #[test]
365    fn test_into_index() {
366        const COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
367        const DEF: IndexDef = IndexDef::new("users", "idx_email").unique().columns(COLS);
368        let idx = DEF.into_index();
369
370        assert_eq!(idx.name, Cow::Borrowed("idx_email"));
371        assert!(idx.is_unique);
372    }
373}