Skip to main content

pg_client/
identifier.rs

1//! PostgreSQL identifier types.
2//!
3//! This module provides types for PostgreSQL identifier values (table names, schema names, etc.).
4//!
5//! **Important:** These types represent identifier *values*, not SQL syntax. They do not parse
6//! or produce quoted identifier syntax. For example, a table named `my table` (with a space)
7//! is represented as the string `my table`, not as `"my table"`.
8//!
9//! Validation rules:
10//! - Cannot be empty
11//! - Maximum length of 63 bytes (NAMEDATALEN - 1)
12//! - Cannot contain NUL bytes
13
14use std::borrow::Cow;
15
16use core::fmt::{Display, Formatter};
17use core::str::FromStr;
18
19/// Maximum length of a PostgreSQL identifier in bytes.
20pub const MAX_LENGTH: usize = 63;
21
22/// Const-compatible validation that returns an optional error.
23const fn validate(input: &str) -> Option<ParseError> {
24    if input.is_empty() {
25        return Some(ParseError::Empty);
26    }
27
28    if input.len() > MAX_LENGTH {
29        return Some(ParseError::TooLong);
30    }
31
32    let bytes = input.as_bytes();
33    let mut index = 0;
34
35    while index < bytes.len() {
36        if bytes[index] == 0 {
37            return Some(ParseError::ContainsNul);
38        }
39        index += 1;
40    }
41
42    None
43}
44
45/// A validated PostgreSQL identifier value.
46///
47/// This represents the actual identifier value, not SQL syntax. Identifiers can contain
48/// spaces and special characters (which would require quoting in SQL).
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
50#[serde(try_from = "String")]
51struct Identifier(Cow<'static, str>);
52
53impl TryFrom<String> for Identifier {
54    type Error = ParseError;
55
56    fn try_from(value: String) -> Result<Self, Self::Error> {
57        value.parse()
58    }
59}
60
61impl Identifier {
62    /// Creates a new identifier from a static string.
63    ///
64    /// # Panics
65    ///
66    /// Panics if the input is empty, exceeds [`MAX_LENGTH`], or contains NUL bytes.
67    #[must_use]
68    const fn from_static_or_panic(input: &'static str) -> Self {
69        match validate(input) {
70            Some(error) => panic!("{}", error.message()),
71            None => Self(Cow::Borrowed(input)),
72        }
73    }
74
75    /// Returns the identifier as a string slice.
76    #[must_use]
77    fn as_str(&self) -> &str {
78        &self.0
79    }
80}
81
82impl Display for Identifier {
83    fn fmt(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
84        write!(formatter, "{}", self.0)
85    }
86}
87
88impl AsRef<str> for Identifier {
89    fn as_ref(&self) -> &str {
90        &self.0
91    }
92}
93
94impl FromStr for Identifier {
95    type Err = ParseError;
96
97    fn from_str(input: &str) -> Result<Self, Self::Err> {
98        match validate(input) {
99            Some(error) => Err(error),
100            None => Ok(Self(Cow::Owned(input.to_owned()))),
101        }
102    }
103}
104
105/// Error parsing a PostgreSQL identifier.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ParseError {
108    /// Identifier cannot be empty.
109    Empty,
110
111    /// Identifier exceeds maximum length.
112    TooLong,
113
114    /// Identifier contains a NUL byte.
115    ContainsNul,
116}
117
118impl ParseError {
119    /// Returns the error message.
120    #[must_use]
121    pub const fn message(&self) -> &'static str {
122        match self {
123            Self::Empty => "identifier cannot be empty",
124            Self::TooLong => "identifier exceeds maximum length",
125            Self::ContainsNul => "identifier cannot contain NUL bytes",
126        }
127    }
128}
129
130impl Display for ParseError {
131    fn fmt(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
132        write!(formatter, "{}", self.message())
133    }
134}
135
136impl std::error::Error for ParseError {}
137
138/// Macro to define identifier-backed newtypes.
139macro_rules! define_identifier_type {
140    ($(#[$meta:meta])* $name:ident, $test_mod:ident) => {
141        $(#[$meta])*
142        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
143        #[serde(try_from = "String")]
144        pub struct $name(Identifier);
145
146        impl TryFrom<String> for $name {
147            type Error = ParseError;
148
149            fn try_from(value: String) -> Result<Self, Self::Error> {
150                value.parse()
151            }
152        }
153
154        impl $name {
155            /// Creates a new value from a static string.
156            ///
157            /// # Panics
158            ///
159            /// Panics if the input is empty, exceeds [`MAX_LENGTH`], or contains NUL bytes.
160            #[must_use]
161            pub const fn from_static_or_panic(input: &'static str) -> Self {
162                Self(Identifier::from_static_or_panic(input))
163            }
164
165            /// Returns the value as a string slice.
166            #[must_use]
167            pub fn as_str(&self) -> &str {
168                self.0.as_str()
169            }
170        }
171
172        impl Display for $name {
173            fn fmt(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
174                write!(formatter, "{}", self.0)
175            }
176        }
177
178        impl AsRef<str> for $name {
179            fn as_ref(&self) -> &str {
180                self.0.as_ref()
181            }
182        }
183
184        impl FromStr for $name {
185            type Err = ParseError;
186
187            fn from_str(input: &str) -> Result<Self, Self::Err> {
188                Identifier::from_str(input).map(Self)
189            }
190        }
191
192        #[cfg(test)]
193        mod $test_mod {
194            use super::*;
195
196            #[test]
197            fn parse_valid() {
198                let value: $name = "test".parse().unwrap();
199                assert_eq!(value.to_string(), "test");
200            }
201
202            #[test]
203            fn parse_valid_with_space() {
204                let value: $name = "test value".parse().unwrap();
205                assert_eq!(value.to_string(), "test value");
206            }
207
208            #[test]
209            fn parse_empty_fails() {
210                let result: Result<$name, _> = "".parse();
211                assert!(matches!(result, Err(ParseError::Empty)));
212            }
213
214            #[test]
215            fn parse_contains_nul_fails() {
216                let result: Result<$name, _> = "test\0value".parse();
217                assert!(matches!(result, Err(ParseError::ContainsNul)));
218            }
219
220            #[test]
221            fn parse_too_long_fails() {
222                let input = "a".repeat(MAX_LENGTH + 1);
223                let result: Result<$name, _> = input.parse();
224                assert!(matches!(result, Err(ParseError::TooLong)));
225            }
226        }
227    };
228}
229
230define_identifier_type!(
231    /// A PostgreSQL table name.
232    Table,
233    table
234);
235
236define_identifier_type!(
237    /// A PostgreSQL schema name.
238    Schema,
239    schema
240);
241
242impl Schema {
243    /// The default `public` schema.
244    pub const PUBLIC: Self = Self::from_static_or_panic("public");
245}
246
247define_identifier_type!(
248    /// A PostgreSQL column name.
249    Column,
250    column
251);
252
253define_identifier_type!(
254    /// A PostgreSQL index name.
255    Index,
256    index
257);
258
259define_identifier_type!(
260    /// A PostgreSQL constraint name.
261    ///
262    /// Includes PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, and EXCLUSION constraints.
263    Constraint,
264    constraint
265);
266
267define_identifier_type!(
268    /// A PostgreSQL extension name.
269    Extension,
270    extension
271);
272
273define_identifier_type!(
274    /// A PostgreSQL sequence name.
275    Sequence,
276    sequence
277);
278
279define_identifier_type!(
280    /// A PostgreSQL function or procedure name.
281    Function,
282    function
283);
284
285define_identifier_type!(
286    /// A PostgreSQL trigger name.
287    Trigger,
288    trigger
289);
290
291define_identifier_type!(
292    /// A PostgreSQL domain name.
293    Domain,
294    domain
295);
296
297define_identifier_type!(
298    /// A PostgreSQL type name.
299    ///
300    /// Includes custom types, enums, and composite types.
301    Type,
302    r#type
303);
304
305define_identifier_type!(
306    /// A PostgreSQL view name.
307    View,
308    view
309);
310
311define_identifier_type!(
312    /// A PostgreSQL relation name.
313    ///
314    /// A relation is either a table or a view. Use this type when an operation
315    /// accepts both tables and views (e.g., SELECT queries).
316    Relation,
317    relation
318);
319
320impl From<Table> for Relation {
321    fn from(table: Table) -> Self {
322        Self(table.0)
323    }
324}
325
326impl From<View> for Relation {
327    fn from(view: View) -> Self {
328        Self(view.0)
329    }
330}
331
332define_identifier_type!(
333    /// A PostgreSQL materialized view name.
334    MaterializedView,
335    materialized_view
336);
337
338impl From<MaterializedView> for Relation {
339    fn from(materialized_view: MaterializedView) -> Self {
340        Self(materialized_view.0)
341    }
342}
343
344define_identifier_type!(
345    /// A PostgreSQL operator name.
346    Operator,
347    operator
348);
349
350define_identifier_type!(
351    /// A PostgreSQL aggregate function name.
352    Aggregate,
353    aggregate
354);
355
356define_identifier_type!(
357    /// A PostgreSQL collation name.
358    Collation,
359    collation
360);
361
362define_identifier_type!(
363    /// A PostgreSQL tablespace name.
364    Tablespace,
365    tablespace
366);
367
368define_identifier_type!(
369    /// A PostgreSQL row-level security policy name.
370    Policy,
371    policy
372);
373
374define_identifier_type!(
375    /// A PostgreSQL rule name.
376    Rule,
377    rule
378);
379
380define_identifier_type!(
381    /// A PostgreSQL publication name (for logical replication).
382    Publication,
383    publication
384);
385
386define_identifier_type!(
387    /// A PostgreSQL subscription name (for logical replication).
388    Subscription,
389    subscription
390);
391
392define_identifier_type!(
393    /// A PostgreSQL foreign server name.
394    ForeignServer,
395    foreign_server
396);
397
398define_identifier_type!(
399    /// A PostgreSQL foreign data wrapper name.
400    ForeignDataWrapper,
401    foreign_data_wrapper
402);
403
404define_identifier_type!(
405    /// A PostgreSQL foreign table name.
406    ForeignTable,
407    foreign_table
408);
409
410define_identifier_type!(
411    /// A PostgreSQL event trigger name.
412    EventTrigger,
413    event_trigger
414);
415
416define_identifier_type!(
417    /// A PostgreSQL procedural language name.
418    Language,
419    language
420);
421
422define_identifier_type!(
423    /// A PostgreSQL text search configuration name.
424    TextSearchConfiguration,
425    text_search_configuration
426);
427
428define_identifier_type!(
429    /// A PostgreSQL text search dictionary name.
430    TextSearchDictionary,
431    text_search_dictionary
432);
433
434define_identifier_type!(
435    /// A PostgreSQL encoding conversion name.
436    Conversion,
437    conversion
438);
439
440define_identifier_type!(
441    /// A PostgreSQL operator class name.
442    OperatorClass,
443    operator_class
444);
445
446define_identifier_type!(
447    /// A PostgreSQL operator family name.
448    OperatorFamily,
449    operator_family
450);
451
452define_identifier_type!(
453    /// A PostgreSQL access method name.
454    AccessMethod,
455    access_method
456);
457
458define_identifier_type!(
459    /// A PostgreSQL extended statistics object name.
460    StatisticsObject,
461    statistics_object
462);
463
464define_identifier_type!(
465    /// A PostgreSQL database name.
466    Database,
467    database
468);
469
470impl Database {
471    /// The default `postgres` database.
472    pub const POSTGRES: Self = Self::from_static_or_panic("postgres");
473}
474
475define_identifier_type!(
476    /// A PostgreSQL role name.
477    ///
478    /// Roles with the `LOGIN` attribute are typically called users.
479    Role,
480    role
481);
482
483impl Role {
484    /// The default `postgres` superuser role.
485    pub const POSTGRES: Self = Self::from_static_or_panic("postgres");
486}
487
488/// A PostgreSQL user (alias for [`Role`]).
489///
490/// A user is a role with the `LOGIN` attribute.
491pub type User = Role;
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    mod identifier {
498        use super::*;
499
500        #[test]
501        fn parse_valid_simple() {
502            let identifier: Identifier = "users".parse().unwrap();
503            assert_eq!(identifier.to_string(), "users");
504        }
505
506        #[test]
507        fn parse_valid_with_space() {
508            let identifier: Identifier = "my table".parse().unwrap();
509            assert_eq!(identifier.to_string(), "my table");
510        }
511
512        #[test]
513        fn parse_valid_with_special_chars() {
514            let identifier: Identifier = "my-table.name".parse().unwrap();
515            assert_eq!(identifier.to_string(), "my-table.name");
516        }
517
518        #[test]
519        fn parse_valid_starting_with_digit() {
520            let identifier: Identifier = "1table".parse().unwrap();
521            assert_eq!(identifier.to_string(), "1table");
522        }
523
524        #[test]
525        fn parse_valid_max_length() {
526            let input = "a".repeat(MAX_LENGTH);
527            let identifier: Identifier = input.parse().unwrap();
528            assert_eq!(identifier.to_string(), input);
529        }
530
531        #[test]
532        fn parse_empty_fails() {
533            let result: Result<Identifier, _> = "".parse();
534            assert_eq!(result, Err(ParseError::Empty));
535        }
536
537        #[test]
538        fn parse_too_long_fails() {
539            let input = "a".repeat(MAX_LENGTH + 1);
540            let result: Result<Identifier, _> = input.parse();
541            assert_eq!(result, Err(ParseError::TooLong));
542        }
543
544        #[test]
545        fn parse_contains_nul_fails() {
546            let result: Result<Identifier, _> = "my\0table".parse();
547            assert_eq!(result, Err(ParseError::ContainsNul));
548        }
549    }
550}