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_schema::PgSchemaName;
9
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgTableName(PgIdentifier);
13
14impl PgTableName {
15 pub fn new(input: impl AsRef<str>) -> Result<Self, PgTableError> {
21 PgIdentifier::new(input)
22 .map(Self)
23 .map_err(PgTableError::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 PgTableName {
34 fn as_ref(&self) -> &str {
35 self.as_str()
36 }
37}
38
39impl fmt::Display for PgTableName {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 self.0.fmt(formatter)
42 }
43}
44
45impl FromStr for PgTableName {
46 type Err = PgTableError;
47
48 fn from_str(input: &str) -> Result<Self, Self::Err> {
49 Self::new(input)
50 }
51}
52
53impl TryFrom<&str> for PgTableName {
54 type Error = PgTableError;
55
56 fn try_from(value: &str) -> Result<Self, Self::Error> {
57 Self::new(value)
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub enum PgTableKind {
64 #[default]
66 Ordinary,
67 Partitioned,
69 Foreign,
71 Temporary,
73 View,
75 MaterializedView,
77 Toast,
79}
80
81impl PgTableKind {
82 #[must_use]
84 pub const fn as_str(self) -> &'static str {
85 match self {
86 Self::Ordinary => "ordinary table",
87 Self::Partitioned => "partitioned table",
88 Self::Foreign => "foreign table",
89 Self::Temporary => "temporary table",
90 Self::View => "view",
91 Self::MaterializedView => "materialized view",
92 Self::Toast => "toast table",
93 }
94 }
95}
96
97impl fmt::Display for PgTableKind {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 formatter.write_str(self.as_str())
100 }
101}
102
103impl FromStr for PgTableKind {
104 type Err = PgTableError;
105
106 fn from_str(input: &str) -> Result<Self, Self::Err> {
107 match normalized_label(input)?.as_str() {
108 "ordinary" | "ordinary table" | "table" | "base table" => Ok(Self::Ordinary),
109 "partitioned" | "partitioned table" => Ok(Self::Partitioned),
110 "foreign" | "foreign table" => Ok(Self::Foreign),
111 "temporary" | "temporary table" | "temp" | "temp table" => Ok(Self::Temporary),
112 "view" => Ok(Self::View),
113 "materialized view" | "matview" => Ok(Self::MaterializedView),
114 "toast" | "toast table" => Ok(Self::Toast),
115 _ => Err(PgTableError::UnknownKind),
116 }
117 }
118}
119
120#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
122pub enum PgTablePersistence {
123 #[default]
125 Permanent,
126 Unlogged,
128 Temporary,
130}
131
132impl PgTablePersistence {
133 #[must_use]
135 pub const fn as_str(self) -> &'static str {
136 match self {
137 Self::Permanent => "permanent",
138 Self::Unlogged => "unlogged",
139 Self::Temporary => "temporary",
140 }
141 }
142}
143
144impl fmt::Display for PgTablePersistence {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 formatter.write_str(self.as_str())
147 }
148}
149
150impl FromStr for PgTablePersistence {
151 type Err = PgTableError;
152
153 fn from_str(input: &str) -> Result<Self, Self::Err> {
154 match normalized_label(input)?.as_str() {
155 "permanent" | "persistent" => Ok(Self::Permanent),
156 "unlogged" => Ok(Self::Unlogged),
157 "temporary" | "temp" => Ok(Self::Temporary),
158 _ => Err(PgTableError::UnknownPersistence),
159 }
160 }
161}
162
163#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
165pub struct PgTableRef {
166 schema: Option<PgSchemaName>,
167 name: PgTableName,
168}
169
170impl PgTableRef {
171 #[must_use]
173 pub const fn new(name: PgTableName) -> Self {
174 Self { schema: None, name }
175 }
176
177 #[must_use]
179 pub const fn qualified(schema: PgSchemaName, name: PgTableName) -> Self {
180 Self {
181 schema: Some(schema),
182 name,
183 }
184 }
185
186 #[must_use]
188 pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
189 self.schema = Some(schema);
190 self
191 }
192
193 #[must_use]
195 pub const fn schema(&self) -> Option<&PgSchemaName> {
196 self.schema.as_ref()
197 }
198
199 #[must_use]
201 pub const fn name(&self) -> &PgTableName {
202 &self.name
203 }
204}
205
206impl fmt::Display for PgTableRef {
207 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208 if let Some(schema) = &self.schema {
209 write!(formatter, "{schema}.")?;
210 }
211 write!(formatter, "{}", self.name)
212 }
213}
214
215#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
217pub struct PgTable {
218 reference: PgTableRef,
219 kind: PgTableKind,
220 persistence: PgTablePersistence,
221}
222
223impl PgTable {
224 #[must_use]
226 pub const fn new(reference: PgTableRef) -> Self {
227 Self {
228 reference,
229 kind: PgTableKind::Ordinary,
230 persistence: PgTablePersistence::Permanent,
231 }
232 }
233
234 #[must_use]
236 pub const fn with_kind(mut self, kind: PgTableKind) -> Self {
237 self.kind = kind;
238 self
239 }
240
241 #[must_use]
243 pub const fn with_persistence(mut self, persistence: PgTablePersistence) -> Self {
244 self.persistence = persistence;
245 self
246 }
247
248 #[must_use]
250 pub const fn reference(&self) -> &PgTableRef {
251 &self.reference
252 }
253
254 #[must_use]
256 pub const fn kind(&self) -> PgTableKind {
257 self.kind
258 }
259
260 #[must_use]
262 pub const fn persistence(&self) -> PgTablePersistence {
263 self.persistence
264 }
265}
266
267#[derive(Clone, Debug, Eq, PartialEq)]
269pub enum PgTableError {
270 Empty,
271 UnknownKind,
272 UnknownPersistence,
273 Identifier(PgIdentifierError),
274}
275
276impl fmt::Display for PgTableError {
277 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
278 match self {
279 Self::Empty => formatter.write_str("PostgreSQL table label cannot be empty"),
280 Self::UnknownKind => formatter.write_str("unknown PostgreSQL table kind"),
281 Self::UnknownPersistence => {
282 formatter.write_str("unknown PostgreSQL table persistence label")
283 }
284 Self::Identifier(error) => {
285 write!(formatter, "invalid PostgreSQL table identifier: {error}")
286 }
287 }
288 }
289}
290
291impl Error for PgTableError {}
292
293fn normalized_label(input: &str) -> Result<String, PgTableError> {
294 let trimmed = input.trim();
295 if trimmed.is_empty() {
296 return Err(PgTableError::Empty);
297 }
298 Ok(trimmed
299 .replace('_', " ")
300 .split_whitespace()
301 .collect::<Vec<_>>()
302 .join(" ")
303 .to_ascii_lowercase())
304}
305
306#[cfg(test)]
307mod tests {
308 use super::{PgTable, PgTableError, PgTableKind, PgTableName, PgTablePersistence, PgTableRef};
309 use use_pg_schema::PgSchemaName;
310
311 #[test]
312 fn renders_schema_qualified_table_refs() -> Result<(), Box<dyn std::error::Error>> {
313 let table = PgTableRef::qualified(PgSchemaName::public(), PgTableName::new("users")?);
314 assert_eq!(table.to_string(), "public.users");
315 Ok(())
316 }
317
318 #[test]
319 fn parses_table_kind_and_persistence() -> Result<(), PgTableError> {
320 assert_eq!(
321 "partitioned table".parse::<PgTableKind>()?,
322 PgTableKind::Partitioned
323 );
324 assert_eq!("foreign".parse::<PgTableKind>()?, PgTableKind::Foreign);
325 assert_eq!(
326 "unlogged".parse::<PgTablePersistence>()?,
327 PgTablePersistence::Unlogged
328 );
329 assert_eq!(PgTableKind::Temporary.to_string(), "temporary table");
330 Ok(())
331 }
332
333 #[test]
334 fn creates_table_metadata() -> Result<(), Box<dyn std::error::Error>> {
335 let reference = PgTableRef::new(PgTableName::new("events")?);
336 let table = PgTable::new(reference)
337 .with_kind(PgTableKind::Ordinary)
338 .with_persistence(PgTablePersistence::Unlogged);
339 assert_eq!(table.reference().to_string(), "events");
340 assert_eq!(table.persistence(), PgTablePersistence::Unlogged);
341 Ok(())
342 }
343}