Skip to main content

use_pg_sequence/
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 sequence name primitive.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgSequenceName(PgIdentifier);
13
14impl PgSequenceName {
15    /// Creates a sequence name.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`PgSequenceError`] when identifier validation fails.
20    pub fn new(input: impl AsRef<str>) -> Result<Self, PgSequenceError> {
21        PgIdentifier::new(input)
22            .map(Self)
23            .map_err(PgSequenceError::Identifier)
24    }
25
26    /// Returns the sequence name text.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        self.0.as_str()
30    }
31}
32
33impl fmt::Display for PgSequenceName {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        self.0.fmt(formatter)
36    }
37}
38
39impl FromStr for PgSequenceName {
40    type Err = PgSequenceError;
41
42    fn from_str(input: &str) -> Result<Self, Self::Err> {
43        Self::new(input)
44    }
45}
46
47/// PostgreSQL sequence ownership metadata.
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum PgSequenceOwner {
50    /// No ownership metadata.
51    Unowned,
52    /// Owned by a table column label.
53    OwnedBy {
54        /// Referenced table metadata.
55        table: PgTableRef,
56        /// Referenced column identifier.
57        column: PgIdentifier,
58    },
59}
60
61impl PgSequenceOwner {
62    /// Creates an ownership label from a table and column.
63    #[must_use]
64    pub const fn owned_by(table: PgTableRef, column: PgIdentifier) -> Self {
65        Self::OwnedBy { table, column }
66    }
67
68    /// Returns `true` when ownership metadata points to a table column.
69    #[must_use]
70    pub const fn is_owned(&self) -> bool {
71        matches!(self, Self::OwnedBy { .. })
72    }
73}
74
75impl fmt::Display for PgSequenceOwner {
76    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Unowned => formatter.write_str("unowned"),
79            Self::OwnedBy { table, column } => write!(formatter, "owned by {table}.{column}"),
80        }
81    }
82}
83
84/// PostgreSQL sequence options using simple numeric primitives.
85#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub struct PgSequenceOptions {
87    increment: i64,
88    min_value: Option<i64>,
89    max_value: Option<i64>,
90    start: i64,
91    cache: u64,
92    cycle: bool,
93}
94
95impl Default for PgSequenceOptions {
96    fn default() -> Self {
97        Self {
98            increment: 1,
99            min_value: None,
100            max_value: None,
101            start: 1,
102            cache: 1,
103            cycle: false,
104        }
105    }
106}
107
108impl PgSequenceOptions {
109    /// Sets the increment.
110    ///
111    /// # Errors
112    ///
113    /// Returns [`PgSequenceError::InvalidIncrement`] when `increment` is zero.
114    pub const fn with_increment(mut self, increment: i64) -> Result<Self, PgSequenceError> {
115        if increment == 0 {
116            return Err(PgSequenceError::InvalidIncrement);
117        }
118        self.increment = increment;
119        Ok(self)
120    }
121
122    /// Sets min and max labels.
123    #[must_use]
124    pub const fn with_bounds(mut self, min_value: Option<i64>, max_value: Option<i64>) -> Self {
125        self.min_value = min_value;
126        self.max_value = max_value;
127        self
128    }
129
130    /// Sets the start value.
131    #[must_use]
132    pub const fn with_start(mut self, start: i64) -> Self {
133        self.start = start;
134        self
135    }
136
137    /// Sets the cache value.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`PgSequenceError::InvalidCache`] when `cache` is zero.
142    pub const fn with_cache(mut self, cache: u64) -> Result<Self, PgSequenceError> {
143        if cache == 0 {
144            return Err(PgSequenceError::InvalidCache);
145        }
146        self.cache = cache;
147        Ok(self)
148    }
149
150    /// Sets the cycle flag.
151    #[must_use]
152    pub const fn with_cycle(mut self, cycle: bool) -> Self {
153        self.cycle = cycle;
154        self
155    }
156
157    /// Returns the increment.
158    #[must_use]
159    pub const fn increment(self) -> i64 {
160        self.increment
161    }
162
163    /// Returns the min value label.
164    #[must_use]
165    pub const fn min_value(self) -> Option<i64> {
166        self.min_value
167    }
168
169    /// Returns the max value label.
170    #[must_use]
171    pub const fn max_value(self) -> Option<i64> {
172        self.max_value
173    }
174
175    /// Returns the start value.
176    #[must_use]
177    pub const fn start(self) -> i64 {
178        self.start
179    }
180
181    /// Returns the cache value.
182    #[must_use]
183    pub const fn cache(self) -> u64 {
184        self.cache
185    }
186
187    /// Returns the cycle flag.
188    #[must_use]
189    pub const fn cycle(self) -> bool {
190        self.cycle
191    }
192}
193
194/// PostgreSQL sequence metadata.
195#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
196pub struct PgSequence {
197    name: PgSequenceName,
198    options: PgSequenceOptions,
199    owner: PgSequenceOwner,
200}
201
202impl PgSequence {
203    /// Creates sequence metadata from a name.
204    #[must_use]
205    pub fn new(name: PgSequenceName) -> Self {
206        Self {
207            name,
208            options: PgSequenceOptions::default(),
209            owner: PgSequenceOwner::Unowned,
210        }
211    }
212
213    /// Sets sequence options.
214    #[must_use]
215    pub const fn with_options(mut self, options: PgSequenceOptions) -> Self {
216        self.options = options;
217        self
218    }
219
220    /// Sets ownership metadata.
221    #[must_use]
222    pub fn with_owner(mut self, owner: PgSequenceOwner) -> Self {
223        self.owner = owner;
224        self
225    }
226
227    /// Returns the sequence name.
228    #[must_use]
229    pub const fn name(&self) -> &PgSequenceName {
230        &self.name
231    }
232
233    /// Returns sequence options.
234    #[must_use]
235    pub const fn options(&self) -> PgSequenceOptions {
236        self.options
237    }
238
239    /// Returns ownership metadata.
240    #[must_use]
241    pub const fn owner(&self) -> &PgSequenceOwner {
242        &self.owner
243    }
244}
245
246/// Error returned when PostgreSQL sequence metadata is invalid.
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub enum PgSequenceError {
249    InvalidIncrement,
250    InvalidCache,
251    Identifier(PgIdentifierError),
252}
253
254impl fmt::Display for PgSequenceError {
255    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256        match self {
257            Self::InvalidIncrement => {
258                formatter.write_str("PostgreSQL sequence increment cannot be zero")
259            }
260            Self::InvalidCache => {
261                formatter.write_str("PostgreSQL sequence cache must be greater than zero")
262            }
263            Self::Identifier(error) => {
264                write!(formatter, "invalid PostgreSQL sequence identifier: {error}")
265            }
266        }
267    }
268}
269
270impl Error for PgSequenceError {}
271
272#[cfg(test)]
273mod tests {
274    use super::{PgSequence, PgSequenceError, PgSequenceName, PgSequenceOptions, PgSequenceOwner};
275    use use_pg_identifier::PgIdentifier;
276    use use_pg_schema::PgSchemaName;
277    use use_pg_table::{PgTableName, PgTableRef};
278
279    #[test]
280    fn uses_simple_default_options() -> Result<(), PgSequenceError> {
281        let sequence = PgSequence::new(PgSequenceName::new("users_id_seq")?);
282        assert_eq!(sequence.options().increment(), 1);
283        assert_eq!(sequence.options().cache(), 1);
284        assert!(!sequence.options().cycle());
285        Ok(())
286    }
287
288    #[test]
289    fn validates_custom_options() -> Result<(), PgSequenceError> {
290        let options = PgSequenceOptions::default()
291            .with_increment(10)?
292            .with_bounds(Some(1), Some(1000))
293            .with_start(10)
294            .with_cache(20)?
295            .with_cycle(true);
296        assert_eq!(options.increment(), 10);
297        assert_eq!(options.min_value(), Some(1));
298        assert_eq!(options.max_value(), Some(1000));
299        assert!(options.cycle());
300        assert_eq!(
301            PgSequenceOptions::default().with_increment(0),
302            Err(PgSequenceError::InvalidIncrement)
303        );
304        Ok(())
305    }
306
307    #[test]
308    fn tracks_sequence_ownership() -> Result<(), Box<dyn std::error::Error>> {
309        let table = PgTableRef::qualified(PgSchemaName::public(), PgTableName::new("users")?);
310        let owner = PgSequenceOwner::owned_by(table, PgIdentifier::new("id")?);
311        assert!(owner.is_owned());
312        assert_eq!(owner.to_string(), "owned by public.users.id");
313        Ok(())
314    }
315}