#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_pg_identifier::{PgIdentifier, PgIdentifierError};
use use_pg_table::PgTableRef;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSequenceName(PgIdentifier);
impl PgSequenceName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgSequenceError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgSequenceError::Identifier)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl fmt::Display for PgSequenceName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for PgSequenceName {
type Err = PgSequenceError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgSequenceOwner {
Unowned,
OwnedBy {
table: PgTableRef,
column: PgIdentifier,
},
}
impl PgSequenceOwner {
#[must_use]
pub const fn owned_by(table: PgTableRef, column: PgIdentifier) -> Self {
Self::OwnedBy { table, column }
}
#[must_use]
pub const fn is_owned(&self) -> bool {
matches!(self, Self::OwnedBy { .. })
}
}
impl fmt::Display for PgSequenceOwner {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unowned => formatter.write_str("unowned"),
Self::OwnedBy { table, column } => write!(formatter, "owned by {table}.{column}"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSequenceOptions {
increment: i64,
min_value: Option<i64>,
max_value: Option<i64>,
start: i64,
cache: u64,
cycle: bool,
}
impl Default for PgSequenceOptions {
fn default() -> Self {
Self {
increment: 1,
min_value: None,
max_value: None,
start: 1,
cache: 1,
cycle: false,
}
}
}
impl PgSequenceOptions {
pub const fn with_increment(mut self, increment: i64) -> Result<Self, PgSequenceError> {
if increment == 0 {
return Err(PgSequenceError::InvalidIncrement);
}
self.increment = increment;
Ok(self)
}
#[must_use]
pub const fn with_bounds(mut self, min_value: Option<i64>, max_value: Option<i64>) -> Self {
self.min_value = min_value;
self.max_value = max_value;
self
}
#[must_use]
pub const fn with_start(mut self, start: i64) -> Self {
self.start = start;
self
}
pub const fn with_cache(mut self, cache: u64) -> Result<Self, PgSequenceError> {
if cache == 0 {
return Err(PgSequenceError::InvalidCache);
}
self.cache = cache;
Ok(self)
}
#[must_use]
pub const fn with_cycle(mut self, cycle: bool) -> Self {
self.cycle = cycle;
self
}
#[must_use]
pub const fn increment(self) -> i64 {
self.increment
}
#[must_use]
pub const fn min_value(self) -> Option<i64> {
self.min_value
}
#[must_use]
pub const fn max_value(self) -> Option<i64> {
self.max_value
}
#[must_use]
pub const fn start(self) -> i64 {
self.start
}
#[must_use]
pub const fn cache(self) -> u64 {
self.cache
}
#[must_use]
pub const fn cycle(self) -> bool {
self.cycle
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSequence {
name: PgSequenceName,
options: PgSequenceOptions,
owner: PgSequenceOwner,
}
impl PgSequence {
#[must_use]
pub fn new(name: PgSequenceName) -> Self {
Self {
name,
options: PgSequenceOptions::default(),
owner: PgSequenceOwner::Unowned,
}
}
#[must_use]
pub const fn with_options(mut self, options: PgSequenceOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn with_owner(mut self, owner: PgSequenceOwner) -> Self {
self.owner = owner;
self
}
#[must_use]
pub const fn name(&self) -> &PgSequenceName {
&self.name
}
#[must_use]
pub const fn options(&self) -> PgSequenceOptions {
self.options
}
#[must_use]
pub const fn owner(&self) -> &PgSequenceOwner {
&self.owner
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgSequenceError {
InvalidIncrement,
InvalidCache,
Identifier(PgIdentifierError),
}
impl fmt::Display for PgSequenceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidIncrement => {
formatter.write_str("PostgreSQL sequence increment cannot be zero")
}
Self::InvalidCache => {
formatter.write_str("PostgreSQL sequence cache must be greater than zero")
}
Self::Identifier(error) => {
write!(formatter, "invalid PostgreSQL sequence identifier: {error}")
}
}
}
}
impl Error for PgSequenceError {}
#[cfg(test)]
mod tests {
use super::{PgSequence, PgSequenceError, PgSequenceName, PgSequenceOptions, PgSequenceOwner};
use use_pg_identifier::PgIdentifier;
use use_pg_schema::PgSchemaName;
use use_pg_table::{PgTableName, PgTableRef};
#[test]
fn uses_simple_default_options() -> Result<(), PgSequenceError> {
let sequence = PgSequence::new(PgSequenceName::new("users_id_seq")?);
assert_eq!(sequence.options().increment(), 1);
assert_eq!(sequence.options().cache(), 1);
assert!(!sequence.options().cycle());
Ok(())
}
#[test]
fn validates_custom_options() -> Result<(), PgSequenceError> {
let options = PgSequenceOptions::default()
.with_increment(10)?
.with_bounds(Some(1), Some(1000))
.with_start(10)
.with_cache(20)?
.with_cycle(true);
assert_eq!(options.increment(), 10);
assert_eq!(options.min_value(), Some(1));
assert_eq!(options.max_value(), Some(1000));
assert!(options.cycle());
assert_eq!(
PgSequenceOptions::default().with_increment(0),
Err(PgSequenceError::InvalidIncrement)
);
Ok(())
}
#[test]
fn tracks_sequence_ownership() -> Result<(), Box<dyn std::error::Error>> {
let table = PgTableRef::qualified(PgSchemaName::public(), PgTableName::new("users")?);
let owner = PgSequenceOwner::owned_by(table, PgIdentifier::new("id")?);
assert!(owner.is_owned());
assert_eq!(owner.to_string(), "owned by public.users.id");
Ok(())
}
}