Skip to main content

drizzle_types/postgres/ddl/
index.rs

1//! `PostgreSQL` Index DDL types
2//!
3//! See: <https://github.com/drizzle-team/drizzle-orm/blob/beta/drizzle-kit/src/dialects/postgres/ddl.ts>
4
5use crate::alloc_prelude::*;
6use core::fmt::Write;
7
8#[cfg(feature = "serde")]
9use crate::serde_helpers::{cow_from_string, cow_option_from_string};
10
11// =============================================================================
12// Const-friendly Definition Types
13// =============================================================================
14
15/// Const-friendly operator class definition
16///
17/// Represents the operator class for an index column with optional default flag.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
21pub struct OpclassDef {
22    /// Operator class name
23    pub name: &'static str,
24    /// Whether this is the default operator class
25    #[cfg_attr(feature = "serde", serde(default))]
26    pub default: bool,
27}
28
29impl OpclassDef {
30    /// Create a new operator class definition
31    #[must_use]
32    pub const fn new(name: &'static str) -> Self {
33        Self {
34            name,
35            default: false,
36        }
37    }
38
39    /// Mark as default operator class
40    #[must_use]
41    pub const fn default_opclass(self) -> Self {
42        Self {
43            default: true,
44            ..self
45        }
46    }
47
48    /// Convert to runtime [`Opclass`] type
49    #[must_use]
50    pub const fn into_opclass(self) -> Opclass {
51        Opclass {
52            name: Cow::Borrowed(self.name),
53            default: self.default,
54        }
55    }
56}
57
58/// Runtime operator class entity
59#[derive(Clone, Debug, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
62pub struct Opclass {
63    /// Operator class name
64    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
65    pub name: Cow<'static, str>,
66    /// Whether this is the default operator class
67    #[cfg_attr(feature = "serde", serde(default))]
68    pub default: bool,
69}
70
71impl Opclass {
72    /// Create a new operator class
73    #[must_use]
74    pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
75        Self {
76            name: name.into(),
77            default: false,
78        }
79    }
80
81    /// Get the operator class name
82    #[inline]
83    #[must_use]
84    pub fn name(&self) -> &str {
85        &self.name
86    }
87}
88
89impl core::fmt::Display for OpclassDef {
90    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91        write!(f, "{}", self.name)
92    }
93}
94
95impl core::fmt::Display for Opclass {
96    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
97        write!(f, "{}", self.name)
98    }
99}
100
101/// Runtime index column entity for serde serialization
102#[derive(Clone, Debug, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
105pub struct IndexColumn {
106    /// Column name or expression
107    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
108    pub value: Cow<'static, str>,
109    /// Whether this is an expression (vs column name)
110    #[cfg_attr(feature = "serde", serde(default))]
111    pub is_expression: bool,
112    /// Ascending order (true) or descending (false)
113    #[cfg_attr(feature = "serde", serde(default = "default_true"))]
114    pub asc: bool,
115    /// NULLS FIRST ordering
116    #[cfg_attr(feature = "serde", serde(default))]
117    pub nulls_first: bool,
118    /// Operator class (optional)
119    #[cfg_attr(
120        feature = "serde",
121        serde(default, skip_serializing_if = "Option::is_none")
122    )]
123    pub opclass: Option<Opclass>,
124}
125
126impl IndexColumn {
127    /// Create a new index column
128    #[must_use]
129    pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
130        Self {
131            value: value.into(),
132            is_expression: false,
133            asc: true,
134            nulls_first: false,
135            opclass: None,
136        }
137    }
138
139    /// Create an expression-based index column
140    #[must_use]
141    pub fn expression(expr: impl Into<Cow<'static, str>>) -> Self {
142        Self {
143            value: expr.into(),
144            is_expression: true,
145            asc: true,
146            nulls_first: false,
147            opclass: None,
148        }
149    }
150
151    /// Set descending order
152    #[must_use]
153    pub const fn desc(mut self) -> Self {
154        self.asc = false;
155        self
156    }
157
158    /// Set NULLS FIRST
159    #[must_use]
160    pub const fn nulls_first(mut self) -> Self {
161        self.nulls_first = true;
162        self
163    }
164
165    /// Set operator class
166    #[must_use]
167    pub fn with_opclass(mut self, opclass: Opclass) -> Self {
168        self.opclass = Some(opclass);
169        self
170    }
171}
172
173impl IndexColumn {
174    /// Generate SQL for this index column
175    #[must_use]
176    pub fn to_sql(&self) -> String {
177        let mut sql = if self.is_expression {
178            format!("({})", self.value)
179        } else {
180            format!("\"{}\"", self.value)
181        };
182
183        if let Some(ref op) = self.opclass {
184            let _ = write!(sql, " {op}");
185        }
186        if !self.asc {
187            sql.push_str(" DESC");
188        }
189        if self.nulls_first {
190            sql.push_str(" NULLS FIRST");
191        }
192
193        sql
194    }
195}
196
197impl From<IndexColumnDef> for IndexColumn {
198    fn from(def: IndexColumnDef) -> Self {
199        Self {
200            value: Cow::Borrowed(def.value),
201            is_expression: def.is_expression,
202            asc: def.asc,
203            nulls_first: def.nulls_first,
204            opclass: def.opclass.map(OpclassDef::into_opclass),
205        }
206    }
207}
208
209impl From<OpclassDef> for Opclass {
210    fn from(def: OpclassDef) -> Self {
211        def.into_opclass()
212    }
213}
214
215/// Const-friendly index column definition
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
219pub struct IndexColumnDef {
220    /// Column name or expression
221    pub value: &'static str,
222    /// Whether this is an expression (vs column name)
223    #[cfg_attr(feature = "serde", serde(default))]
224    pub is_expression: bool,
225    /// Ascending order (true) or descending (false)
226    #[cfg_attr(feature = "serde", serde(default = "default_true"))]
227    pub asc: bool,
228    /// NULLS FIRST ordering
229    #[cfg_attr(feature = "serde", serde(default))]
230    pub nulls_first: bool,
231    /// Operator class (optional)
232    #[cfg_attr(
233        feature = "serde",
234        serde(default, skip_serializing_if = "Option::is_none")
235    )]
236    pub opclass: Option<OpclassDef>,
237}
238
239#[cfg(feature = "serde")]
240const fn default_true() -> bool {
241    true
242}
243
244impl IndexColumnDef {
245    /// Create a new index column definition
246    #[must_use]
247    pub const fn new(value: &'static str) -> Self {
248        Self {
249            value,
250            is_expression: false,
251            asc: true,
252            nulls_first: false,
253            opclass: None,
254        }
255    }
256
257    /// Create an expression-based index column
258    #[must_use]
259    pub const fn expression(expr: &'static str) -> Self {
260        Self {
261            value: expr,
262            is_expression: true,
263            asc: true,
264            nulls_first: false,
265            opclass: None,
266        }
267    }
268
269    /// Set descending order
270    #[must_use]
271    pub const fn desc(self) -> Self {
272        Self { asc: false, ..self }
273    }
274
275    /// Set NULLS FIRST
276    #[must_use]
277    pub const fn nulls_first(self) -> Self {
278        Self {
279            nulls_first: true,
280            ..self
281        }
282    }
283
284    /// Set operator class
285    #[must_use]
286    pub const fn opclass(self, opclass: OpclassDef) -> Self {
287        Self {
288            opclass: Some(opclass),
289            ..self
290        }
291    }
292
293    /// Set operator class by name (convenience method)
294    #[must_use]
295    pub const fn opclass_name(self, name: &'static str) -> Self {
296        Self {
297            opclass: Some(OpclassDef::new(name)),
298            ..self
299        }
300    }
301}
302
303/// Const-friendly index definition
304#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
305pub struct IndexDef {
306    /// Schema name
307    pub schema: &'static str,
308    /// Parent table name
309    pub table: &'static str,
310    /// Index name
311    pub name: &'static str,
312    /// Whether the index name was explicitly specified
313    pub name_explicit: bool,
314    /// Columns included in the index
315    pub columns: &'static [IndexColumnDef],
316    /// Is this a unique index?
317    pub is_unique: bool,
318    /// WHERE clause for partial indexes
319    pub where_clause: Option<&'static str>,
320    /// Index method (btree, hash, etc.)
321    pub method: Option<&'static str>,
322    /// Storage parameters (WITH clause)
323    pub with: Option<&'static str>,
324    /// Create concurrently?
325    pub concurrently: bool,
326}
327
328impl IndexDef {
329    /// Create a new index definition
330    #[must_use]
331    pub const fn new(
332        schema: &'static str,
333        table: &'static str,
334        name: &'static str,
335        columns: &'static [IndexColumnDef],
336    ) -> Self {
337        Self {
338            schema,
339            table,
340            name,
341            name_explicit: false,
342            columns,
343            is_unique: false,
344            where_clause: None,
345            method: None,
346            with: None,
347            concurrently: false,
348        }
349    }
350
351    /// Make this a unique index
352    #[must_use]
353    pub const fn unique(self) -> Self {
354        Self {
355            is_unique: true,
356            ..self
357        }
358    }
359
360    /// Mark the name as explicitly specified
361    #[must_use]
362    pub const fn explicit_name(self) -> Self {
363        Self {
364            name_explicit: true,
365            ..self
366        }
367    }
368
369    /// Set WHERE clause for partial index
370    #[must_use]
371    pub const fn where_clause(self, clause: &'static str) -> Self {
372        Self {
373            where_clause: Some(clause),
374            ..self
375        }
376    }
377
378    /// Set index method
379    #[must_use]
380    pub const fn method(self, method: &'static str) -> Self {
381        Self {
382            method: Some(method),
383            ..self
384        }
385    }
386
387    /// Set storage parameters (WITH clause)
388    #[must_use]
389    pub const fn with(self, params: &'static str) -> Self {
390        Self {
391            with: Some(params),
392            ..self
393        }
394    }
395
396    /// Set concurrently flag
397    #[must_use]
398    pub const fn concurrently(self) -> Self {
399        Self {
400            concurrently: true,
401            ..self
402        }
403    }
404
405    /// Convert to runtime [`Index`] type
406    #[must_use]
407    pub fn into_index(self) -> Index {
408        Index {
409            schema: Cow::Borrowed(self.schema),
410            table: Cow::Borrowed(self.table),
411            name: Cow::Borrowed(self.name),
412            name_explicit: self.name_explicit,
413            columns: self.columns.iter().map(|c| IndexColumn::from(*c)).collect(),
414            is_unique: self.is_unique,
415            where_clause: self.where_clause.map(Cow::Borrowed),
416            method: self.method.map(Cow::Borrowed),
417            with: self.with.map(Cow::Borrowed),
418            concurrently: self.concurrently,
419        }
420    }
421}
422
423// =============================================================================
424// Runtime Type for Serde
425// =============================================================================
426
427/// Runtime index entity for serde serialization
428#[derive(Clone, Debug, PartialEq, Eq)]
429#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
430#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
431pub struct Index {
432    /// Schema name
433    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
434    pub schema: Cow<'static, str>,
435
436    /// Parent table name
437    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
438    pub table: Cow<'static, str>,
439
440    /// Index name
441    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
442    pub name: Cow<'static, str>,
443
444    /// Whether the index name was explicitly specified
445    #[cfg_attr(feature = "serde", serde(default))]
446    pub name_explicit: bool,
447
448    /// Columns included in the index
449    pub columns: Vec<IndexColumn>,
450
451    /// Is this a unique index?
452    #[cfg_attr(feature = "serde", serde(default))]
453    pub is_unique: bool,
454
455    /// WHERE clause for partial indexes
456    #[cfg_attr(
457        feature = "serde",
458        serde(
459            default,
460            skip_serializing_if = "Option::is_none",
461            rename = "where",
462            deserialize_with = "cow_option_from_string"
463        )
464    )]
465    pub where_clause: Option<Cow<'static, str>>,
466
467    /// Index method (btree, hash, etc.)
468    #[cfg_attr(
469        feature = "serde",
470        serde(
471            default,
472            skip_serializing_if = "Option::is_none",
473            deserialize_with = "cow_option_from_string"
474        )
475    )]
476    pub method: Option<Cow<'static, str>>,
477
478    /// Storage parameters (WITH clause)
479    #[cfg_attr(
480        feature = "serde",
481        serde(
482            default,
483            skip_serializing_if = "Option::is_none",
484            deserialize_with = "cow_option_from_string"
485        )
486    )]
487    pub with: Option<Cow<'static, str>>,
488
489    /// Create concurrently?
490    #[cfg_attr(feature = "serde", serde(default))]
491    pub concurrently: bool,
492}
493
494impl Index {
495    /// Create a new index
496    #[must_use]
497    pub fn new(
498        schema: impl Into<Cow<'static, str>>,
499        table: impl Into<Cow<'static, str>>,
500        name: impl Into<Cow<'static, str>>,
501        columns: Vec<IndexColumn>,
502    ) -> Self {
503        Self {
504            schema: schema.into(),
505            table: table.into(),
506            name: name.into(),
507            name_explicit: false,
508            columns,
509            is_unique: false,
510            where_clause: None,
511            method: None,
512            with: None,
513            concurrently: false,
514        }
515    }
516
517    /// Make this a unique index
518    #[must_use]
519    pub const fn unique(mut self) -> Self {
520        self.is_unique = true;
521        self
522    }
523
524    /// Mark the name as explicitly specified
525    #[must_use]
526    pub const fn explicit_name(mut self) -> Self {
527        self.name_explicit = true;
528        self
529    }
530
531    /// Get the schema name
532    #[inline]
533    #[must_use]
534    pub fn schema(&self) -> &str {
535        &self.schema
536    }
537
538    /// Get the index name
539    #[inline]
540    #[must_use]
541    pub fn name(&self) -> &str {
542        &self.name
543    }
544
545    /// Get the table name
546    #[inline]
547    #[must_use]
548    pub fn table(&self) -> &str {
549        &self.table
550    }
551}
552
553impl Default for Index {
554    fn default() -> Self {
555        Self::new("public", "", "", vec![])
556    }
557}
558
559impl From<IndexDef> for Index {
560    fn from(def: IndexDef) -> Self {
561        def.into_index()
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568
569    #[test]
570    fn test_const_index_def() {
571        const COLS: &[IndexColumnDef] = &[
572            IndexColumnDef::new("email"),
573            IndexColumnDef::new("created_at").desc(),
574        ];
575
576        const IDX: IndexDef = IndexDef::new("public", "users", "idx_users_email", COLS).unique();
577
578        assert_eq!(IDX.name, "idx_users_email");
579        assert_eq!(IDX.table, "users");
580        const {
581            assert!(IDX.is_unique);
582        }
583        assert_eq!(IDX.columns.len(), 2);
584    }
585
586    #[test]
587    fn test_index_def_to_index() {
588        const COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
589        const DEF: IndexDef = IndexDef::new("public", "users", "idx_email", COLS).unique();
590
591        let idx = DEF.into_index();
592        assert_eq!(idx.name(), "idx_email");
593        assert!(idx.is_unique);
594        assert_eq!(idx.columns.len(), 1);
595    }
596
597    #[test]
598    fn test_into_index() {
599        const COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
600        const DEF: IndexDef = IndexDef::new("public", "users", "idx_email", COLS).unique();
601        let idx = DEF.into_index();
602
603        assert_eq!(idx.name, Cow::Borrowed("idx_email"));
604        assert!(idx.is_unique);
605    }
606}