Skip to main content

drizzle_types/postgres/ddl/
index.rs

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