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