1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_pg_column::PgColumnName;
8use use_pg_identifier::{PgIdentifier, PgIdentifierError};
9use use_pg_table::PgTableRef;
10
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct PgConstraintName(PgIdentifier);
14
15impl PgConstraintName {
16 pub fn new(input: impl AsRef<str>) -> Result<Self, PgConstraintError> {
22 PgIdentifier::new(input)
23 .map(Self)
24 .map_err(PgConstraintError::Identifier)
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 self.0.as_str()
31 }
32}
33
34impl fmt::Display for PgConstraintName {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 self.0.fmt(formatter)
37 }
38}
39
40impl FromStr for PgConstraintName {
41 type Err = PgConstraintError;
42
43 fn from_str(input: &str) -> Result<Self, Self::Err> {
44 Self::new(input)
45 }
46}
47
48#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum PgConstraintKind {
51 #[default]
53 PrimaryKey,
54 ForeignKey,
56 Unique,
58 Check,
60 Exclusion,
62 NotNull,
64}
65
66impl PgConstraintKind {
67 #[must_use]
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::PrimaryKey => "PRIMARY KEY",
72 Self::ForeignKey => "FOREIGN KEY",
73 Self::Unique => "UNIQUE",
74 Self::Check => "CHECK",
75 Self::Exclusion => "EXCLUDE",
76 Self::NotNull => "NOT NULL",
77 }
78 }
79}
80
81impl fmt::Display for PgConstraintKind {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter.write_str(self.as_str())
84 }
85}
86
87impl FromStr for PgConstraintKind {
88 type Err = PgConstraintError;
89
90 fn from_str(input: &str) -> Result<Self, Self::Err> {
91 match normalized_label(input)?.as_str() {
92 "primary" | "primary key" => Ok(Self::PrimaryKey),
93 "foreign" | "foreign key" => Ok(Self::ForeignKey),
94 "unique" => Ok(Self::Unique),
95 "check" => Ok(Self::Check),
96 "exclude" | "exclusion" => Ok(Self::Exclusion),
97 "not null" | "notnull" => Ok(Self::NotNull),
98 _ => Err(PgConstraintError::UnknownKind),
99 }
100 }
101}
102
103#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub enum PgInitially {
106 #[default]
108 Immediate,
109 Deferred,
111}
112
113impl PgInitially {
114 #[must_use]
116 pub const fn as_str(self) -> &'static str {
117 match self {
118 Self::Immediate => "INITIALLY IMMEDIATE",
119 Self::Deferred => "INITIALLY DEFERRED",
120 }
121 }
122}
123
124impl fmt::Display for PgInitially {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 formatter.write_str(self.as_str())
127 }
128}
129
130#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct PgDeferrability {
133 deferrable: bool,
134 initially: PgInitially,
135}
136
137impl PgDeferrability {
138 #[must_use]
140 pub const fn not_deferrable() -> Self {
141 Self {
142 deferrable: false,
143 initially: PgInitially::Immediate,
144 }
145 }
146
147 #[must_use]
149 pub const fn deferrable(initially: PgInitially) -> Self {
150 Self {
151 deferrable: true,
152 initially,
153 }
154 }
155
156 #[must_use]
158 pub const fn is_deferrable(self) -> bool {
159 self.deferrable
160 }
161
162 #[must_use]
164 pub const fn initially(self) -> PgInitially {
165 self.initially
166 }
167}
168
169impl fmt::Display for PgDeferrability {
170 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171 if self.deferrable {
172 write!(formatter, "DEFERRABLE {}", self.initially)
173 } else {
174 formatter.write_str("NOT DEFERRABLE")
175 }
176 }
177}
178
179#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
181pub struct PgConstraint {
182 kind: PgConstraintKind,
183 name: Option<PgConstraintName>,
184 columns: Vec<PgColumnName>,
185 referenced_table: Option<PgTableRef>,
186 expression: Option<String>,
187 deferrability: PgDeferrability,
188}
189
190impl PgConstraint {
191 #[must_use]
193 pub const fn new(kind: PgConstraintKind) -> Self {
194 Self {
195 kind,
196 name: None,
197 columns: Vec::new(),
198 referenced_table: None,
199 expression: None,
200 deferrability: PgDeferrability::not_deferrable(),
201 }
202 }
203
204 #[must_use]
206 pub fn with_name(mut self, name: PgConstraintName) -> Self {
207 self.name = Some(name);
208 self
209 }
210
211 #[must_use]
213 pub fn with_columns(mut self, columns: Vec<PgColumnName>) -> Self {
214 self.columns = columns;
215 self
216 }
217
218 #[must_use]
220 pub fn with_referenced_table(mut self, table: PgTableRef) -> Self {
221 self.referenced_table = Some(table);
222 self
223 }
224
225 pub fn with_expression(
231 mut self,
232 expression: impl AsRef<str>,
233 ) -> Result<Self, PgConstraintError> {
234 self.expression = Some(validate_expression(expression.as_ref())?.to_owned());
235 Ok(self)
236 }
237
238 #[must_use]
240 pub const fn with_deferrability(mut self, deferrability: PgDeferrability) -> Self {
241 self.deferrability = deferrability;
242 self
243 }
244
245 #[must_use]
247 pub const fn kind(&self) -> PgConstraintKind {
248 self.kind
249 }
250
251 #[must_use]
253 pub const fn name(&self) -> Option<&PgConstraintName> {
254 self.name.as_ref()
255 }
256
257 #[must_use]
259 pub fn columns(&self) -> &[PgColumnName] {
260 &self.columns
261 }
262
263 #[must_use]
265 pub const fn referenced_table(&self) -> Option<&PgTableRef> {
266 self.referenced_table.as_ref()
267 }
268
269 #[must_use]
271 pub fn expression(&self) -> Option<&str> {
272 self.expression.as_deref()
273 }
274
275 #[must_use]
277 pub const fn deferrability(&self) -> PgDeferrability {
278 self.deferrability
279 }
280}
281
282impl fmt::Display for PgConstraint {
283 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284 if let Some(name) = &self.name {
285 write!(formatter, "CONSTRAINT {name} ")?;
286 }
287 write!(formatter, "{}", self.kind)
288 }
289}
290
291#[derive(Clone, Debug, Eq, PartialEq)]
293pub enum PgConstraintError {
294 Empty,
295 UnknownKind,
296 ControlCharacter,
297 Identifier(PgIdentifierError),
298}
299
300impl fmt::Display for PgConstraintError {
301 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302 match self {
303 Self::Empty => formatter.write_str("PostgreSQL constraint label cannot be empty"),
304 Self::UnknownKind => formatter.write_str("unknown PostgreSQL constraint kind"),
305 Self::ControlCharacter => {
306 formatter.write_str("PostgreSQL constraint label cannot contain control characters")
307 }
308 Self::Identifier(error) => {
309 write!(
310 formatter,
311 "invalid PostgreSQL constraint identifier: {error}"
312 )
313 }
314 }
315 }
316}
317
318impl Error for PgConstraintError {}
319
320fn normalized_label(input: &str) -> Result<String, PgConstraintError> {
321 let trimmed = input.trim();
322 if trimmed.is_empty() {
323 return Err(PgConstraintError::Empty);
324 }
325 Ok(trimmed
326 .replace('_', " ")
327 .split_whitespace()
328 .collect::<Vec<_>>()
329 .join(" ")
330 .to_ascii_lowercase())
331}
332
333fn validate_expression(input: &str) -> Result<&str, PgConstraintError> {
334 let trimmed = input.trim();
335 if trimmed.is_empty() {
336 return Err(PgConstraintError::Empty);
337 }
338 if trimmed.chars().any(char::is_control) {
339 return Err(PgConstraintError::ControlCharacter);
340 }
341 Ok(trimmed)
342}
343
344#[cfg(test)]
345mod tests {
346 use super::{
347 PgConstraint, PgConstraintError, PgConstraintKind, PgConstraintName, PgDeferrability,
348 PgInitially,
349 };
350 use use_pg_column::PgColumnName;
351
352 #[test]
353 fn parses_and_renders_constraint_kinds() -> Result<(), PgConstraintError> {
354 assert_eq!(
355 "primary key".parse::<PgConstraintKind>()?,
356 PgConstraintKind::PrimaryKey
357 );
358 assert_eq!(
359 "exclude".parse::<PgConstraintKind>()?,
360 PgConstraintKind::Exclusion
361 );
362 assert_eq!(PgConstraintKind::ForeignKey.to_string(), "FOREIGN KEY");
363 Ok(())
364 }
365
366 #[test]
367 fn creates_primary_key_metadata() -> Result<(), PgConstraintError> {
368 let constraint = PgConstraint::new(PgConstraintKind::PrimaryKey)
369 .with_name(PgConstraintName::new("users_pkey")?)
370 .with_columns(vec![PgColumnName::new("id").expect("valid column")]);
371
372 assert_eq!(constraint.to_string(), "CONSTRAINT users_pkey PRIMARY KEY");
373 assert_eq!(constraint.columns().len(), 1);
374 Ok(())
375 }
376
377 #[test]
378 fn tracks_deferrability() {
379 let deferrability = PgDeferrability::deferrable(PgInitially::Deferred);
380 assert!(deferrability.is_deferrable());
381 assert_eq!(deferrability.to_string(), "DEFERRABLE INITIALLY DEFERRED");
382 }
383
384 #[test]
385 fn stores_check_expression_labels() -> Result<(), PgConstraintError> {
386 let constraint =
387 PgConstraint::new(PgConstraintKind::Check).with_expression("amount > 0")?;
388 assert_eq!(constraint.expression(), Some("amount > 0"));
389 Ok(())
390 }
391}