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
247/// A schema-qualified PostgreSQL table name.
248#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
249pub struct QualifiedTable {
250    /// The schema name.
251    pub schema: Schema,
252    /// The table name.
253    pub table: Table,
254}
255
256impl Display for QualifiedTable {
257    fn fmt(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
258        write!(formatter, "{}.{}", self.schema, self.table)
259    }
260}
261
262define_identifier_type!(
263    /// A PostgreSQL column name.
264    Column,
265    column
266);
267
268define_identifier_type!(
269    /// A PostgreSQL index name.
270    Index,
271    index
272);
273
274define_identifier_type!(
275    /// A PostgreSQL constraint name.
276    ///
277    /// Includes PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, and EXCLUSION constraints.
278    Constraint,
279    constraint
280);
281
282define_identifier_type!(
283    /// A PostgreSQL extension name.
284    Extension,
285    extension
286);
287
288define_identifier_type!(
289    /// A PostgreSQL sequence name.
290    Sequence,
291    sequence
292);
293
294define_identifier_type!(
295    /// A PostgreSQL function or procedure name.
296    Function,
297    function
298);
299
300define_identifier_type!(
301    /// A PostgreSQL trigger name.
302    Trigger,
303    trigger
304);
305
306define_identifier_type!(
307    /// A PostgreSQL domain name.
308    Domain,
309    domain
310);
311
312define_identifier_type!(
313    /// A PostgreSQL type name.
314    ///
315    /// Includes custom types, enums, and composite types.
316    Type,
317    r#type
318);
319
320define_identifier_type!(
321    /// A PostgreSQL view name.
322    View,
323    view
324);
325
326define_identifier_type!(
327    /// A PostgreSQL relation name.
328    ///
329    /// A relation is either a table or a view. Use this type when an operation
330    /// accepts both tables and views (e.g., SELECT queries).
331    Relation,
332    relation
333);
334
335impl From<Table> for Relation {
336    fn from(table: Table) -> Self {
337        Self(table.0)
338    }
339}
340
341impl From<View> for Relation {
342    fn from(view: View) -> Self {
343        Self(view.0)
344    }
345}
346
347define_identifier_type!(
348    /// A PostgreSQL materialized view name.
349    MaterializedView,
350    materialized_view
351);
352
353impl From<MaterializedView> for Relation {
354    fn from(materialized_view: MaterializedView) -> Self {
355        Self(materialized_view.0)
356    }
357}
358
359define_identifier_type!(
360    /// A PostgreSQL operator name.
361    Operator,
362    operator
363);
364
365define_identifier_type!(
366    /// A PostgreSQL aggregate function name.
367    Aggregate,
368    aggregate
369);
370
371define_identifier_type!(
372    /// A PostgreSQL collation name.
373    Collation,
374    collation
375);
376
377define_identifier_type!(
378    /// A PostgreSQL tablespace name.
379    Tablespace,
380    tablespace
381);
382
383define_identifier_type!(
384    /// A PostgreSQL row-level security policy name.
385    Policy,
386    policy
387);
388
389define_identifier_type!(
390    /// A PostgreSQL rule name.
391    Rule,
392    rule
393);
394
395define_identifier_type!(
396    /// A PostgreSQL publication name (for logical replication).
397    Publication,
398    publication
399);
400
401define_identifier_type!(
402    /// A PostgreSQL subscription name (for logical replication).
403    Subscription,
404    subscription
405);
406
407define_identifier_type!(
408    /// A PostgreSQL foreign server name.
409    ForeignServer,
410    foreign_server
411);
412
413define_identifier_type!(
414    /// A PostgreSQL foreign data wrapper name.
415    ForeignDataWrapper,
416    foreign_data_wrapper
417);
418
419define_identifier_type!(
420    /// A PostgreSQL foreign table name.
421    ForeignTable,
422    foreign_table
423);
424
425define_identifier_type!(
426    /// A PostgreSQL event trigger name.
427    EventTrigger,
428    event_trigger
429);
430
431define_identifier_type!(
432    /// A PostgreSQL procedural language name.
433    Language,
434    language
435);
436
437define_identifier_type!(
438    /// A PostgreSQL text search configuration name.
439    TextSearchConfiguration,
440    text_search_configuration
441);
442
443define_identifier_type!(
444    /// A PostgreSQL text search dictionary name.
445    TextSearchDictionary,
446    text_search_dictionary
447);
448
449define_identifier_type!(
450    /// A PostgreSQL encoding conversion name.
451    Conversion,
452    conversion
453);
454
455define_identifier_type!(
456    /// A PostgreSQL operator class name.
457    OperatorClass,
458    operator_class
459);
460
461define_identifier_type!(
462    /// A PostgreSQL operator family name.
463    OperatorFamily,
464    operator_family
465);
466
467define_identifier_type!(
468    /// A PostgreSQL access method name.
469    AccessMethod,
470    access_method
471);
472
473define_identifier_type!(
474    /// A PostgreSQL extended statistics object name.
475    StatisticsObject,
476    statistics_object
477);
478
479define_identifier_type!(
480    /// A PostgreSQL database name.
481    Database,
482    database
483);
484
485impl Database {
486    /// The default `postgres` database.
487    pub const POSTGRES: Self = Self::from_static_or_panic("postgres");
488}
489
490define_identifier_type!(
491    /// A PostgreSQL role name.
492    ///
493    /// Roles with the `LOGIN` attribute are typically called users.
494    Role,
495    role
496);
497
498impl Role {
499    /// The default `postgres` superuser role.
500    pub const POSTGRES: Self = Self::from_static_or_panic("postgres");
501}
502
503/// A PostgreSQL user (alias for [`Role`]).
504///
505/// A user is a role with the `LOGIN` attribute.
506pub type User = Role;
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    mod identifier {
513        use super::*;
514
515        #[test]
516        fn parse_valid_simple() {
517            let identifier: Identifier = "users".parse().unwrap();
518            assert_eq!(identifier.to_string(), "users");
519        }
520
521        #[test]
522        fn parse_valid_with_space() {
523            let identifier: Identifier = "my table".parse().unwrap();
524            assert_eq!(identifier.to_string(), "my table");
525        }
526
527        #[test]
528        fn parse_valid_with_special_chars() {
529            let identifier: Identifier = "my-table.name".parse().unwrap();
530            assert_eq!(identifier.to_string(), "my-table.name");
531        }
532
533        #[test]
534        fn parse_valid_starting_with_digit() {
535            let identifier: Identifier = "1table".parse().unwrap();
536            assert_eq!(identifier.to_string(), "1table");
537        }
538
539        #[test]
540        fn parse_valid_max_length() {
541            let input = "a".repeat(MAX_LENGTH);
542            let identifier: Identifier = input.parse().unwrap();
543            assert_eq!(identifier.to_string(), input);
544        }
545
546        #[test]
547        fn parse_empty_fails() {
548            let result: Result<Identifier, _> = "".parse();
549            assert_eq!(result, Err(ParseError::Empty));
550        }
551
552        #[test]
553        fn parse_too_long_fails() {
554            let input = "a".repeat(MAX_LENGTH + 1);
555            let result: Result<Identifier, _> = input.parse();
556            assert_eq!(result, Err(ParseError::TooLong));
557        }
558
559        #[test]
560        fn parse_contains_nul_fails() {
561            let result: Result<Identifier, _> = "my\0table".parse();
562            assert_eq!(result, Err(ParseError::ContainsNul));
563        }
564    }
565}