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_type::{PgBuiltInType, PgTypeName};
9
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgColumnName(PgIdentifier);
13
14impl PgColumnName {
15 pub fn new(input: impl AsRef<str>) -> Result<Self, PgColumnError> {
21 PgIdentifier::new(input)
22 .map(Self)
23 .map_err(PgColumnError::Identifier)
24 }
25
26 #[must_use]
28 pub fn as_str(&self) -> &str {
29 self.0.as_str()
30 }
31}
32
33impl AsRef<str> for PgColumnName {
34 fn as_ref(&self) -> &str {
35 self.as_str()
36 }
37}
38
39impl fmt::Display for PgColumnName {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 self.0.fmt(formatter)
42 }
43}
44
45impl FromStr for PgColumnName {
46 type Err = PgColumnError;
47
48 fn from_str(input: &str) -> Result<Self, Self::Err> {
49 Self::new(input)
50 }
51}
52
53impl TryFrom<&str> for PgColumnName {
54 type Error = PgColumnError;
55
56 fn try_from(value: &str) -> Result<Self, Self::Error> {
57 Self::new(value)
58 }
59}
60
61#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct PgColumnDefault(String);
64
65impl PgColumnDefault {
66 pub fn new(input: impl AsRef<str>) -> Result<Self, PgColumnError> {
72 validate_label(input.as_ref(), PgColumnError::EmptyDefault)
73 .map(|value| Self(value.to_owned()))
74 }
75
76 #[must_use]
78 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81}
82
83impl fmt::Display for PgColumnDefault {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter.write_str(self.as_str())
86 }
87}
88
89#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
91pub enum PgNullability {
92 #[default]
94 Nullable,
95 NotNull,
97}
98
99impl PgNullability {
100 #[must_use]
102 pub const fn is_nullable(self) -> bool {
103 matches!(self, Self::Nullable)
104 }
105
106 #[must_use]
108 pub const fn as_str(self) -> &'static str {
109 match self {
110 Self::Nullable => "NULL",
111 Self::NotNull => "NOT NULL",
112 }
113 }
114}
115
116impl fmt::Display for PgNullability {
117 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118 formatter.write_str(self.as_str())
119 }
120}
121
122impl FromStr for PgNullability {
123 type Err = PgColumnError;
124
125 fn from_str(input: &str) -> Result<Self, Self::Err> {
126 match normalized_label(input, PgColumnError::UnknownNullability)?.as_str() {
127 "null" | "nullable" => Ok(Self::Nullable),
128 "not null" | "notnull" | "required" => Ok(Self::NotNull),
129 _ => Err(PgColumnError::UnknownNullability),
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
136pub enum PgGeneratedKind {
137 Stored,
139 Virtual,
141}
142
143impl PgGeneratedKind {
144 #[must_use]
146 pub const fn as_str(self) -> &'static str {
147 match self {
148 Self::Stored => "STORED",
149 Self::Virtual => "VIRTUAL",
150 }
151 }
152}
153
154impl fmt::Display for PgGeneratedKind {
155 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 formatter.write_str(self.as_str())
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
162pub enum PgIdentityKind {
163 Always,
165 ByDefault,
167}
168
169impl PgIdentityKind {
170 #[must_use]
172 pub const fn as_str(self) -> &'static str {
173 match self {
174 Self::Always => "ALWAYS",
175 Self::ByDefault => "BY DEFAULT",
176 }
177 }
178}
179
180impl fmt::Display for PgIdentityKind {
181 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182 formatter.write_str(self.as_str())
183 }
184}
185
186#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub struct PgColumn {
189 name: PgColumnName,
190 type_name: PgTypeName,
191 nullability: PgNullability,
192 default: Option<PgColumnDefault>,
193 generated: Option<PgGeneratedKind>,
194 identity: Option<PgIdentityKind>,
195}
196
197impl PgColumn {
198 #[must_use]
200 pub const fn new(name: PgColumnName, type_name: PgTypeName) -> Self {
201 Self {
202 name,
203 type_name,
204 nullability: PgNullability::Nullable,
205 default: None,
206 generated: None,
207 identity: None,
208 }
209 }
210
211 #[must_use]
213 pub fn with_built_in_type(name: PgColumnName, ty: PgBuiltInType) -> Self {
214 Self::new(name, PgTypeName::built_in(ty))
215 }
216
217 #[must_use]
219 pub const fn with_nullability(mut self, nullability: PgNullability) -> Self {
220 self.nullability = nullability;
221 self
222 }
223
224 #[must_use]
226 pub fn with_default(mut self, default: PgColumnDefault) -> Self {
227 self.default = Some(default);
228 self
229 }
230
231 #[must_use]
233 pub const fn with_generated(mut self, generated: PgGeneratedKind) -> Self {
234 self.generated = Some(generated);
235 self
236 }
237
238 #[must_use]
240 pub const fn with_identity(mut self, identity: PgIdentityKind) -> Self {
241 self.identity = Some(identity);
242 self
243 }
244
245 #[must_use]
247 pub const fn name(&self) -> &PgColumnName {
248 &self.name
249 }
250
251 #[must_use]
253 pub const fn type_name(&self) -> &PgTypeName {
254 &self.type_name
255 }
256
257 #[must_use]
259 pub const fn nullability(&self) -> PgNullability {
260 self.nullability
261 }
262
263 #[must_use]
265 pub const fn default(&self) -> Option<&PgColumnDefault> {
266 self.default.as_ref()
267 }
268
269 #[must_use]
271 pub const fn generated(&self) -> Option<PgGeneratedKind> {
272 self.generated
273 }
274
275 #[must_use]
277 pub const fn identity(&self) -> Option<PgIdentityKind> {
278 self.identity
279 }
280}
281
282#[derive(Clone, Debug, Eq, PartialEq)]
284pub enum PgColumnError {
285 EmptyDefault,
286 ControlCharacter,
287 UnknownNullability,
288 Identifier(PgIdentifierError),
289}
290
291impl fmt::Display for PgColumnError {
292 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
293 match self {
294 Self::EmptyDefault => formatter.write_str("PostgreSQL column default cannot be empty"),
295 Self::ControlCharacter => {
296 formatter.write_str("PostgreSQL column label cannot contain control characters")
297 }
298 Self::UnknownNullability => {
299 formatter.write_str("unknown PostgreSQL column nullability label")
300 }
301 Self::Identifier(error) => {
302 write!(formatter, "invalid PostgreSQL column identifier: {error}")
303 }
304 }
305 }
306}
307
308impl Error for PgColumnError {}
309
310fn validate_label(input: &str, empty_error: PgColumnError) -> Result<&str, PgColumnError> {
311 let trimmed = input.trim();
312 if trimmed.is_empty() {
313 return Err(empty_error);
314 }
315 if trimmed.chars().any(char::is_control) {
316 return Err(PgColumnError::ControlCharacter);
317 }
318 Ok(trimmed)
319}
320
321fn normalized_label(input: &str, empty_error: PgColumnError) -> Result<String, PgColumnError> {
322 let trimmed = validate_label(input, empty_error)?;
323 Ok(trimmed
324 .replace('_', " ")
325 .split_whitespace()
326 .collect::<Vec<_>>()
327 .join(" ")
328 .to_ascii_lowercase())
329}
330
331#[cfg(test)]
332mod tests {
333 use super::{
334 PgColumn, PgColumnDefault, PgColumnError, PgColumnName, PgGeneratedKind, PgIdentityKind,
335 PgNullability,
336 };
337 use use_pg_type::PgBuiltInType;
338
339 #[test]
340 fn creates_column_metadata() -> Result<(), PgColumnError> {
341 let column = PgColumn::with_built_in_type(PgColumnName::new("id")?, PgBuiltInType::BigInt)
342 .with_nullability(PgNullability::NotNull)
343 .with_identity(PgIdentityKind::Always);
344
345 assert_eq!(column.name().as_str(), "id");
346 assert_eq!(column.type_name().as_str(), "bigint");
347 assert_eq!(column.nullability(), PgNullability::NotNull);
348 assert_eq!(column.identity(), Some(PgIdentityKind::Always));
349 Ok(())
350 }
351
352 #[test]
353 fn stores_default_and_generated_labels() -> Result<(), PgColumnError> {
354 let column = PgColumn::with_built_in_type(
355 PgColumnName::new("created_at")?,
356 PgBuiltInType::TimestampTz,
357 )
358 .with_default(PgColumnDefault::new("now()")?)
359 .with_generated(PgGeneratedKind::Stored);
360
361 assert_eq!(column.default().map(PgColumnDefault::as_str), Some("now()"));
362 assert_eq!(column.generated(), Some(PgGeneratedKind::Stored));
363 assert_eq!(PgNullability::NotNull.to_string(), "NOT NULL");
364 Ok(())
365 }
366
367 #[test]
368 fn parses_nullability_labels() -> Result<(), PgColumnError> {
369 assert_eq!(
370 "nullable".parse::<PgNullability>()?,
371 PgNullability::Nullable
372 );
373 assert_eq!("not null".parse::<PgNullability>()?, PgNullability::NotNull);
374 assert!(PgNullability::Nullable.is_nullable());
375 Ok(())
376 }
377}