drizzle_types/sqlite/ddl/
index.rs

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