Skip to main content

use_pg_index/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_pg_identifier::{PgIdentifier, PgIdentifierError};
8use use_pg_table::PgTableRef;
9
10/// PostgreSQL index name primitive.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgIndexName(PgIdentifier);
13
14impl PgIndexName {
15    /// Creates an index name.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`PgIndexError`] when identifier validation fails.
20    pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
21        PgIdentifier::new(input)
22            .map(Self)
23            .map_err(PgIndexError::Identifier)
24    }
25
26    /// Returns the index name text.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        self.0.as_str()
30    }
31}
32
33impl fmt::Display for PgIndexName {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        self.0.fmt(formatter)
36    }
37}
38
39impl FromStr for PgIndexName {
40    type Err = PgIndexError;
41
42    fn from_str(input: &str) -> Result<Self, Self::Err> {
43        Self::new(input)
44    }
45}
46
47/// PostgreSQL index access methods.
48#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum PgIndexMethod {
50    /// B-tree index method.
51    #[default]
52    Btree,
53    /// Hash index method.
54    Hash,
55    /// GiST index method.
56    Gist,
57    /// SP-GiST index method.
58    Spgist,
59    /// GIN index method.
60    Gin,
61    /// BRIN index method.
62    Brin,
63}
64
65impl PgIndexMethod {
66    /// Returns the stable PostgreSQL method label.
67    #[must_use]
68    pub const fn as_str(self) -> &'static str {
69        match self {
70            Self::Btree => "btree",
71            Self::Hash => "hash",
72            Self::Gist => "gist",
73            Self::Spgist => "spgist",
74            Self::Gin => "gin",
75            Self::Brin => "brin",
76        }
77    }
78}
79
80impl fmt::Display for PgIndexMethod {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        formatter.write_str(self.as_str())
83    }
84}
85
86impl FromStr for PgIndexMethod {
87    type Err = PgIndexError;
88
89    fn from_str(input: &str) -> Result<Self, Self::Err> {
90        match normalized_label(input)?.as_str() {
91            "btree" | "b tree" => Ok(Self::Btree),
92            "hash" => Ok(Self::Hash),
93            "gist" => Ok(Self::Gist),
94            "spgist" | "sp gist" => Ok(Self::Spgist),
95            "gin" => Ok(Self::Gin),
96            "brin" => Ok(Self::Brin),
97            _ => Err(PgIndexError::UnknownMethod),
98        }
99    }
100}
101
102/// PostgreSQL index column label.
103#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub struct PgIndexColumn(PgIdentifier);
105
106impl PgIndexColumn {
107    /// Creates an index column label.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`PgIndexError`] when identifier validation fails.
112    pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
113        PgIdentifier::new(input)
114            .map(Self)
115            .map_err(PgIndexError::Identifier)
116    }
117
118    /// Returns the column label text.
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        self.0.as_str()
122    }
123}
124
125impl fmt::Display for PgIndexColumn {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        self.0.fmt(formatter)
128    }
129}
130
131/// PostgreSQL index expression label.
132#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
133pub struct PgIndexExpression(String);
134
135impl PgIndexExpression {
136    /// Creates an expression label without parsing SQL.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`PgIndexError`] when the label is empty or contains control characters.
141    pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
142        validate_label(input.as_ref(), PgIndexError::EmptyExpression)
143            .map(|value| Self(value.to_owned()))
144    }
145
146    /// Returns the expression label.
147    #[must_use]
148    pub fn as_str(&self) -> &str {
149        &self.0
150    }
151}
152
153impl fmt::Display for PgIndexExpression {
154    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
155        formatter.write_str(self.as_str())
156    }
157}
158
159/// PostgreSQL index flag metadata.
160#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub struct PgIndexFlags {
162    bits: u8,
163}
164
165const UNIQUE_FLAG: u8 = 1 << 0;
166const PRIMARY_FLAG: u8 = 1 << 1;
167const PARTIAL_FLAG: u8 = 1 << 2;
168const EXPRESSION_FLAG: u8 = 1 << 3;
169const CONCURRENT_FLAG: u8 = 1 << 4;
170const INVALID_FLAG: u8 = 1 << 5;
171
172impl PgIndexFlags {
173    /// Sets the unique flag.
174    #[must_use]
175    pub const fn unique(mut self, value: bool) -> Self {
176        self.set_flag(UNIQUE_FLAG, value);
177        self
178    }
179
180    /// Sets the primary-index flag.
181    #[must_use]
182    pub const fn primary(mut self, value: bool) -> Self {
183        self.set_flag(PRIMARY_FLAG, value);
184        self
185    }
186
187    /// Sets the partial-index flag.
188    #[must_use]
189    pub const fn partial(mut self, value: bool) -> Self {
190        self.set_flag(PARTIAL_FLAG, value);
191        self
192    }
193
194    /// Sets the expression-index flag.
195    #[must_use]
196    pub const fn expression(mut self, value: bool) -> Self {
197        self.set_flag(EXPRESSION_FLAG, value);
198        self
199    }
200
201    /// Sets the concurrent-build flag.
202    #[must_use]
203    pub const fn concurrent(mut self, value: bool) -> Self {
204        self.set_flag(CONCURRENT_FLAG, value);
205        self
206    }
207
208    /// Sets the invalid-index flag.
209    #[must_use]
210    pub const fn invalid(mut self, value: bool) -> Self {
211        self.set_flag(INVALID_FLAG, value);
212        self
213    }
214
215    /// Returns `true` when the unique flag is set.
216    #[must_use]
217    pub const fn is_unique(self) -> bool {
218        self.has_flag(UNIQUE_FLAG)
219    }
220
221    /// Returns `true` when the primary-index flag is set.
222    #[must_use]
223    pub const fn is_primary(self) -> bool {
224        self.has_flag(PRIMARY_FLAG)
225    }
226
227    /// Returns `true` when the partial-index flag is set.
228    #[must_use]
229    pub const fn is_partial(self) -> bool {
230        self.has_flag(PARTIAL_FLAG)
231    }
232
233    /// Returns `true` when the expression-index flag is set.
234    #[must_use]
235    pub const fn is_expression(self) -> bool {
236        self.has_flag(EXPRESSION_FLAG)
237    }
238
239    /// Returns `true` when the concurrent-build flag is set.
240    #[must_use]
241    pub const fn is_concurrent(self) -> bool {
242        self.has_flag(CONCURRENT_FLAG)
243    }
244
245    /// Returns `true` when the invalid-index flag is set.
246    #[must_use]
247    pub const fn is_invalid(self) -> bool {
248        self.has_flag(INVALID_FLAG)
249    }
250
251    const fn set_flag(&mut self, flag: u8, value: bool) {
252        if value {
253            self.bits |= flag;
254        } else {
255            self.bits &= !flag;
256        }
257    }
258
259    const fn has_flag(self, flag: u8) -> bool {
260        self.bits & flag != 0
261    }
262}
263
264/// PostgreSQL index metadata without SQL generation or execution.
265#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
266pub struct PgIndex {
267    name: PgIndexName,
268    table: Option<PgTableRef>,
269    method: PgIndexMethod,
270    columns: Vec<PgIndexColumn>,
271    expressions: Vec<PgIndexExpression>,
272    predicate: Option<String>,
273    flags: PgIndexFlags,
274}
275
276impl PgIndex {
277    /// Creates index metadata from a name.
278    #[must_use]
279    pub const fn new(name: PgIndexName) -> Self {
280        Self {
281            name,
282            table: None,
283            method: PgIndexMethod::Btree,
284            columns: Vec::new(),
285            expressions: Vec::new(),
286            predicate: None,
287            flags: PgIndexFlags { bits: 0 },
288        }
289    }
290
291    /// Sets the table reference.
292    #[must_use]
293    pub fn with_table(mut self, table: PgTableRef) -> Self {
294        self.table = Some(table);
295        self
296    }
297
298    /// Sets the index method.
299    #[must_use]
300    pub const fn with_method(mut self, method: PgIndexMethod) -> Self {
301        self.method = method;
302        self
303    }
304
305    /// Sets indexed columns.
306    #[must_use]
307    pub fn with_columns(mut self, columns: Vec<PgIndexColumn>) -> Self {
308        self.columns = columns;
309        self
310    }
311
312    /// Adds an index expression label and marks the index as expression-backed.
313    #[must_use]
314    pub fn with_expression(mut self, expression: PgIndexExpression) -> Self {
315        self.expressions.push(expression);
316        self.flags = self.flags.expression(true);
317        self
318    }
319
320    /// Sets a partial-index predicate label without parsing SQL.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`PgIndexError`] when the label is empty or contains control characters.
325    pub fn with_predicate(mut self, predicate: impl AsRef<str>) -> Result<Self, PgIndexError> {
326        self.predicate =
327            Some(validate_label(predicate.as_ref(), PgIndexError::EmptyPredicate)?.to_owned());
328        self.flags = self.flags.partial(true);
329        Ok(self)
330    }
331
332    /// Sets index flags.
333    #[must_use]
334    pub const fn with_flags(mut self, flags: PgIndexFlags) -> Self {
335        self.flags = flags;
336        self
337    }
338
339    /// Returns the index name.
340    #[must_use]
341    pub const fn name(&self) -> &PgIndexName {
342        &self.name
343    }
344
345    /// Returns the optional table reference.
346    #[must_use]
347    pub const fn table(&self) -> Option<&PgTableRef> {
348        self.table.as_ref()
349    }
350
351    /// Returns the index method.
352    #[must_use]
353    pub const fn method(&self) -> PgIndexMethod {
354        self.method
355    }
356
357    /// Returns indexed columns.
358    #[must_use]
359    pub fn columns(&self) -> &[PgIndexColumn] {
360        &self.columns
361    }
362
363    /// Returns expression labels.
364    #[must_use]
365    pub fn expressions(&self) -> &[PgIndexExpression] {
366        &self.expressions
367    }
368
369    /// Returns the optional partial-index predicate label.
370    #[must_use]
371    pub fn predicate(&self) -> Option<&str> {
372        self.predicate.as_deref()
373    }
374
375    /// Returns the index flags.
376    #[must_use]
377    pub const fn flags(&self) -> PgIndexFlags {
378        self.flags
379    }
380}
381
382/// Error returned when PostgreSQL index metadata is invalid.
383#[derive(Clone, Debug, Eq, PartialEq)]
384pub enum PgIndexError {
385    Empty,
386    EmptyExpression,
387    EmptyPredicate,
388    UnknownMethod,
389    ControlCharacter,
390    Identifier(PgIdentifierError),
391}
392
393impl fmt::Display for PgIndexError {
394    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
395        match self {
396            Self::Empty => formatter.write_str("PostgreSQL index label cannot be empty"),
397            Self::EmptyExpression => {
398                formatter.write_str("PostgreSQL index expression cannot be empty")
399            }
400            Self::EmptyPredicate => {
401                formatter.write_str("PostgreSQL index predicate cannot be empty")
402            }
403            Self::UnknownMethod => formatter.write_str("unknown PostgreSQL index method"),
404            Self::ControlCharacter => {
405                formatter.write_str("PostgreSQL index label cannot contain control characters")
406            }
407            Self::Identifier(error) => {
408                write!(formatter, "invalid PostgreSQL index identifier: {error}")
409            }
410        }
411    }
412}
413
414impl Error for PgIndexError {}
415
416fn normalized_label(input: &str) -> Result<String, PgIndexError> {
417    let trimmed = validate_label(input, PgIndexError::Empty)?;
418    Ok(trimmed
419        .replace('_', " ")
420        .split_whitespace()
421        .collect::<Vec<_>>()
422        .join(" ")
423        .to_ascii_lowercase())
424}
425
426fn validate_label(input: &str, empty_error: PgIndexError) -> Result<&str, PgIndexError> {
427    let trimmed = input.trim();
428    if trimmed.is_empty() {
429        return Err(empty_error);
430    }
431    if trimmed.chars().any(char::is_control) {
432        return Err(PgIndexError::ControlCharacter);
433    }
434    Ok(trimmed)
435}
436
437#[cfg(test)]
438mod tests {
439    use super::{
440        PgIndex, PgIndexColumn, PgIndexError, PgIndexExpression, PgIndexFlags, PgIndexMethod,
441        PgIndexName,
442    };
443
444    #[test]
445    fn parses_and_renders_index_methods() -> Result<(), PgIndexError> {
446        assert_eq!("btree".parse::<PgIndexMethod>()?, PgIndexMethod::Btree);
447        assert_eq!("sp gist".parse::<PgIndexMethod>()?, PgIndexMethod::Spgist);
448        assert_eq!(PgIndexMethod::Brin.to_string(), "brin");
449        Ok(())
450    }
451
452    #[test]
453    fn tracks_index_flags() {
454        let flags = PgIndexFlags::default()
455            .unique(true)
456            .primary(true)
457            .concurrent(true)
458            .invalid(true);
459        assert!(flags.is_unique());
460        assert!(flags.is_primary());
461        assert!(flags.is_concurrent());
462        assert!(flags.is_invalid());
463    }
464
465    #[test]
466    fn creates_btree_index_metadata() -> Result<(), PgIndexError> {
467        let index = PgIndex::new(PgIndexName::new("users_email_idx")?)
468            .with_method(PgIndexMethod::Btree)
469            .with_columns(vec![PgIndexColumn::new("email")?])
470            .with_flags(PgIndexFlags::default().unique(true));
471
472        assert_eq!(index.name().as_str(), "users_email_idx");
473        assert_eq!(index.method(), PgIndexMethod::Btree);
474        assert_eq!(index.columns().len(), 1);
475        assert!(index.flags().is_unique());
476        Ok(())
477    }
478
479    #[test]
480    fn tracks_expression_and_partial_labels() -> Result<(), PgIndexError> {
481        let index = PgIndex::new(PgIndexName::new("users_lower_email_idx")?)
482            .with_expression(PgIndexExpression::new("lower(email)")?)
483            .with_predicate("deleted_at IS NULL")?;
484        assert_eq!(index.expressions().len(), 1);
485        assert_eq!(index.predicate(), Some("deleted_at IS NULL"));
486        assert!(index.flags().is_expression());
487        assert!(index.flags().is_partial());
488        Ok(())
489    }
490}