Skip to main content

hyperdb_api/
names.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! SQL name types for safe identifier handling.
5//!
6//! This module provides types that properly escape SQL identifiers to prevent
7//! SQL injection attacks.
8
9use std::fmt;
10use std::hash::{Hash, Hasher};
11use std::str::FromStr;
12
13use smallvec::SmallVec;
14
15use crate::error::{Error, Result};
16
17/// `PostgreSQL` identifier length limit (in characters).
18pub(crate) const PG_IDENTIFIER_LIMIT: usize = 63;
19
20/// Escapes a SQL identifier for safe use in queries.
21///
22/// This function properly quotes and escapes a name to prevent SQL injection.
23/// The result is wrapped in double quotes with internal quotes escaped.
24///
25/// # Errors
26///
27/// Returns an error if the name exceeds the `PostgreSQL` identifier limit (63 characters).
28/// This behavior is consistent with [`Name::try_new()`].
29///
30/// # Example
31///
32/// ```
33/// use hyperdb_api::{escape_name, Result};
34///
35/// fn demo() -> Result<()> {
36///     let escaped = escape_name("my_table")?;
37///     assert_eq!(escaped, "\"my_table\"");
38///
39///     let special = escape_name("table\"with\"quotes")?;
40///     assert_eq!(special, "\"table\"\"with\"\"quotes\"");
41///     Ok(())
42/// }
43///
44/// // Names exceeding 63 characters are rejected
45/// let long_name = "a".repeat(64);
46/// assert!(escape_name(&long_name).is_err());
47/// ```
48pub fn escape_name(name: &str) -> Result<String> {
49    let len = name.chars().count();
50    if len > PG_IDENTIFIER_LIMIT {
51        return Err(Error::InvalidName(format!(
52            "Name exceeds PostgreSQL identifier limit ({len} > {PG_IDENTIFIER_LIMIT})"
53        )));
54    }
55
56    let escaped_inner = name.replace('"', "\"\"");
57    Ok(format!("\"{escaped_inner}\""))
58}
59
60/// Escapes a database file path for safe use in SQL statements.
61///
62/// This function wraps the path in double quotes and escapes internal quotes,
63/// just like [`escape_name()`], but **without** the 63-character `PostgreSQL`
64/// identifier length limit. Use this for `CREATE DATABASE`, `ATTACH DATABASE`,
65/// `COPY DATABASE`, and similar statements where the argument is a file path
66/// rather than an SQL identifier.
67///
68/// # Example
69///
70/// ```
71/// use hyperdb_api::escape_sql_path;
72///
73/// let simple = escape_sql_path("/tmp/data.hyper");
74/// assert_eq!(simple, "\"/tmp/data.hyper\"");
75///
76/// let special = escape_sql_path("/tmp/my \"db\".hyper");
77/// assert_eq!(special, "\"/tmp/my \"\"db\"\".hyper\"");
78/// ```
79#[must_use]
80pub fn escape_sql_path(path: &str) -> String {
81    let escaped_inner = path.replace('"', "\"\"");
82    format!("\"{escaped_inner}\"")
83}
84
85/// Escapes a SQL string literal for safe use in queries.
86///
87/// This function properly quotes and escapes a string value to prevent SQL injection.
88/// The result is wrapped in single quotes with internal quotes escaped.
89///
90/// # Example
91///
92/// ```
93/// use hyperdb_api::escape_string_literal;
94///
95/// let escaped = escape_string_literal("hello");
96/// assert_eq!(escaped, "'hello'");
97///
98/// let special = escape_string_literal("it's a test");
99/// assert_eq!(special, "'it''s a test'");
100/// ```
101#[must_use]
102pub fn escape_string_literal(value: &str) -> String {
103    format!("'{}'", value.replace('\'', "''"))
104}
105
106/// Represents an escaped SQL identifier name.
107///
108/// `Name` stores both the properly quoted/escaped version (safe for SQL) and
109/// the original unescaped version (for display/logging).
110///
111/// # Example
112///
113/// ```
114/// use hyperdb_api::Name;
115///
116/// let name = Name::try_new("users")?;
117/// assert_eq!(name.to_string(), "\"users\"");
118/// assert_eq!(name.unescaped(), "users");
119/// # Ok::<(), hyperdb_api::Error>(())
120/// ```
121#[derive(Clone, Debug)]
122#[must_use = "Name represents a validated SQL identifier that should not be discarded. Use it in your SQL queries or table definitions"]
123pub struct Name {
124    /// The escaped name (safe for SQL).
125    escaped: String,
126    /// The original unescaped name.
127    unescaped: String,
128}
129
130impl Name {
131    /// Creates a new escaped SQL name.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if the name is empty or exceeds the `PostgreSQL` identifier limit (63 characters).
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// use hyperdb_api::Name;
141    ///
142    /// let name = Name::try_new("users")?;
143    /// assert_eq!(name.unescaped(), "users");
144    /// # Ok::<(), hyperdb_api::Error>(())
145    /// ```
146    pub fn try_new(name: impl Into<String>) -> Result<Self> {
147        let unescaped = name.into();
148        if unescaped.is_empty() {
149            return Err(Error::InvalidName("Name must not be empty".into()));
150        }
151        // escape_name validates the length limit and returns an error if exceeded
152        let escaped = escape_name(&unescaped)?;
153        Ok(Name { escaped, unescaped })
154    }
155
156    /// Returns the properly quoted and escaped string representation.
157    ///
158    /// This is safe to use directly in SQL queries.
159    #[must_use]
160    pub fn as_str(&self) -> &str {
161        &self.escaped
162    }
163
164    /// Returns the original unescaped name.
165    ///
166    /// **Warning:** Do not use this in SQL queries as it may be vulnerable to
167    /// SQL injection. Use this only for logging or display purposes.
168    #[must_use]
169    pub fn unescaped(&self) -> &str {
170        &self.unescaped
171    }
172}
173
174/// Parses a dot-separated SQL identifier into parts, handling quoted sections.
175///
176/// This is a common parsing function used by Name, `SchemaName`, and `TableName`.
177/// Uses `SmallVec` for efficiency since most identifiers have 1-3 parts.
178fn parse_qualified_identifier(s: &str) -> SmallVec<[String; 3]> {
179    let mut parts = SmallVec::new();
180    let mut current = String::new();
181    let mut in_quotes = false;
182    let mut chars = s.chars().peekable();
183
184    while let Some(c) = chars.next() {
185        match c {
186            // Toggle the "in_quotes" state
187            '"' => {
188                // Handle escaped quotes (double double-quotes)
189                if in_quotes && chars.peek() == Some(&'"') {
190                    current.push('"');
191                    chars.next(); // skip the second quote
192                } else {
193                    in_quotes = !in_quotes;
194                    // Don't add the quote character itself to current
195                }
196            }
197            // Split on dots, but ONLY if we aren't inside quotes
198            '.' if !in_quotes => {
199                if !current.is_empty() {
200                    parts.push(current.split_off(0));
201                }
202            }
203            _ => current.push(c),
204        }
205    }
206    if !current.is_empty() {
207        parts.push(current);
208    }
209    parts
210}
211
212impl fmt::Display for Name {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(f, "{}", self.escaped)
215    }
216}
217
218impl PartialEq for Name {
219    fn eq(&self, other: &Self) -> bool {
220        self.unescaped == other.unescaped
221    }
222}
223
224impl Eq for Name {}
225
226impl PartialOrd for Name {
227    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
228        Some(self.cmp(other))
229    }
230}
231
232impl Ord for Name {
233    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
234        self.unescaped.cmp(&other.unescaped)
235    }
236}
237
238impl Hash for Name {
239    fn hash<H: Hasher>(&self, state: &mut H) {
240        self.unescaped.hash(state);
241    }
242}
243
244impl TryFrom<&str> for Name {
245    type Error = Error;
246
247    fn try_from(s: &str) -> Result<Self> {
248        Self::try_new(s)
249    }
250}
251
252impl TryFrom<&String> for Name {
253    type Error = Error;
254
255    fn try_from(s: &String) -> Result<Self> {
256        Self::try_new(s.as_str())
257    }
258}
259
260impl TryFrom<String> for Name {
261    type Error = Error;
262
263    fn try_from(s: String) -> Result<Self> {
264        Self::try_new(s)
265    }
266}
267
268impl FromStr for Name {
269    type Err = Error;
270
271    fn from_str(s: &str) -> Result<Self> {
272        Self::try_new(s)
273    }
274}
275
276/// Represents an escaped SQL database name.
277///
278/// # Example
279///
280/// ```
281/// use hyperdb_api::{DatabaseName, Result};
282///
283/// # fn main() -> Result<()> {
284/// let db = DatabaseName::try_new("mydb")?;
285/// assert_eq!(db.to_string(), "\"mydb\"");
286/// # Ok(())
287/// # }
288/// ```
289#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
290#[must_use = "DatabaseName represents a validated database identifier that should not be discarded. Use it in your connection or table definitions"]
291pub struct DatabaseName {
292    name: Name,
293}
294
295impl DatabaseName {
296    /// Creates a new database name.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if the name is empty or exceeds the `PostgreSQL` identifier limit.
301    pub fn try_new(name: impl Into<String>) -> Result<Self> {
302        Ok(DatabaseName {
303            name: Name::try_new(name)?,
304        })
305    }
306
307    /// Returns the name component.
308    pub fn name(&self) -> &Name {
309        &self.name
310    }
311
312    /// Returns the unescaped name.
313    #[must_use]
314    pub fn unescaped(&self) -> &str {
315        self.name.unescaped()
316    }
317}
318
319impl fmt::Display for DatabaseName {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        write!(f, "{}", self.name)
322    }
323}
324
325impl TryFrom<&str> for DatabaseName {
326    type Error = Error;
327
328    fn try_from(s: &str) -> Result<Self> {
329        Self::try_new(s)
330    }
331}
332
333impl TryFrom<&String> for DatabaseName {
334    type Error = Error;
335
336    fn try_from(s: &String) -> Result<Self> {
337        Self::try_new(s.as_str())
338    }
339}
340
341impl TryFrom<String> for DatabaseName {
342    type Error = Error;
343
344    fn try_from(s: String) -> Result<Self> {
345        Self::try_new(s)
346    }
347}
348
349impl From<Name> for DatabaseName {
350    fn from(name: Name) -> Self {
351        DatabaseName { name }
352    }
353}
354
355impl FromStr for DatabaseName {
356    type Err = Error;
357
358    fn from_str(s: &str) -> Result<Self> {
359        Self::try_new(s)
360    }
361}
362
363/// Represents an escaped SQL schema name with optional database qualifier.
364///
365/// Uses the fluent builder pattern for constructing qualified schema names.
366///
367/// # Example
368///
369/// ```
370/// use hyperdb_api::{SchemaName, Result};
371///
372/// # fn main() -> Result<()> {
373/// // Simple schema name
374/// let schema = SchemaName::try_new("public")?;
375/// assert_eq!(schema.to_string(), "\"public\"");
376///
377/// // Qualified schema name using fluent builder
378/// let qualified = SchemaName::try_new("public")?.with_database("mydb")?;
379/// assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
380/// # Ok(())
381/// # }
382/// ```
383#[derive(Clone, Debug, PartialEq, Eq, Hash)]
384#[must_use = "SchemaName represents a validated schema identifier that should not be discarded. Use it in your table definitions or queries"]
385pub struct SchemaName {
386    database: Option<DatabaseName>,
387    schema: Name,
388}
389
390impl SchemaName {
391    /// Creates a new schema name without a database qualifier (the starting point).
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if the schema name is empty or exceeds the `PostgreSQL` identifier limit.
396    ///
397    /// # Example
398    ///
399    /// ```no_run
400    /// use hyperdb_api::SchemaName;
401    ///
402    /// let schema = SchemaName::try_new("public")?;
403    /// assert_eq!(schema.to_string(), "\"public\"");
404    /// # Ok::<(), hyperdb_api::Error>(())
405    /// ```
406    pub fn try_new(schema: impl Into<String>) -> Result<Self> {
407        Ok(SchemaName {
408            database: None,
409            schema: Name::try_new(schema)?,
410        })
411    }
412
413    /// Builder method: Sets the database qualifier.
414    ///
415    /// This method is part of the fluent builder pattern and can be chained.
416    /// Returns `Result<Self>` to allow fallible method chaining.
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if the database name is empty or exceeds the `PostgreSQL` identifier limit.
421    ///
422    /// # Example
423    ///
424    /// ```
425    /// use hyperdb_api::SchemaName;
426    ///
427    /// let schema = SchemaName::try_new("public")?.with_database("mydb")?;
428    /// assert_eq!(schema.to_string(), "\"mydb\".\"public\"");
429    /// # Ok::<(), hyperdb_api::Error>(())
430    /// ```
431    pub fn with_database(mut self, database: impl Into<String>) -> Result<Self> {
432        self.database = Some(DatabaseName::try_new(database)?);
433        Ok(self)
434    }
435
436    /// Returns the database name, if any.
437    #[must_use]
438    pub fn database(&self) -> Option<&DatabaseName> {
439        self.database.as_ref()
440    }
441
442    /// Returns the schema name component.
443    pub fn schema(&self) -> &Name {
444        &self.schema
445    }
446
447    /// Returns the unescaped schema name.
448    #[must_use]
449    pub fn unescaped(&self) -> &str {
450        self.schema.unescaped()
451    }
452}
453
454impl fmt::Display for SchemaName {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        if let Some(ref db) = self.database {
457            write!(f, "{}.{}", db, self.schema)
458        } else {
459            write!(f, "{}", self.schema)
460        }
461    }
462}
463
464impl TryFrom<&str> for SchemaName {
465    type Error = Error;
466
467    fn try_from(s: &str) -> Result<Self> {
468        s.parse()
469    }
470}
471
472impl TryFrom<&String> for SchemaName {
473    type Error = Error;
474
475    fn try_from(s: &String) -> Result<Self> {
476        s.as_str().parse()
477    }
478}
479
480impl TryFrom<String> for SchemaName {
481    type Error = Error;
482
483    fn try_from(s: String) -> Result<Self> {
484        s.parse()
485    }
486}
487
488impl From<Name> for SchemaName {
489    fn from(name: Name) -> Self {
490        SchemaName {
491            database: None,
492            schema: name,
493        }
494    }
495}
496
497impl FromStr for SchemaName {
498    type Err = Error;
499
500    fn from_str(s: &str) -> Result<Self> {
501        let parts = parse_qualified_identifier(s);
502
503        // Parse database.schema format
504        match parts.as_slice() {
505            [s] => SchemaName::try_new(s),
506            [d, s] => SchemaName::try_new(s)?.with_database(d),
507            _ => Err(Error::InvalidName(format!("Invalid SQL identifier: {s}"))),
508        }
509    }
510}
511
512/// Represents a fully qualified SQL table name.
513///
514/// A table name can optionally include database and schema qualifiers.
515/// Uses the fluent builder pattern for constructing qualified table names.
516///
517/// # Example
518///
519/// ```
520/// use hyperdb_api::{TableName, Result};
521///
522/// # fn main() -> Result<()> {
523/// // Simple table name
524/// let table = TableName::try_new("users")?;
525/// assert_eq!(table.to_string(), "\"users\"");
526///
527/// // With schema using fluent builder
528/// let with_schema = TableName::try_new("users")?.with_schema("public")?;
529/// assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
530///
531/// // Fully qualified using fluent builder
532/// let full = TableName::try_new("users")?
533///     .with_schema("public")?
534///     .with_database("mydb")?;
535/// assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
536/// # Ok(())
537/// # }
538/// ```
539#[derive(Clone, Debug, PartialEq, Eq, Hash)]
540#[must_use = "TableName represents a validated table identifier that should not be discarded. Use it in your queries or table operations"]
541pub struct TableName {
542    database: Option<DatabaseName>,
543    schema: Option<Name>,
544    table: Name,
545}
546
547impl TableName {
548    /// Creates a new table name without qualifiers (the starting point).
549    ///
550    /// # Errors
551    ///
552    /// Returns an error if the table name is empty or exceeds the `PostgreSQL` identifier limit.
553    ///
554    /// # Example
555    ///
556    /// ```
557    /// use hyperdb_api::TableName;
558    ///
559    /// let table = TableName::try_new("users")?;
560    /// assert_eq!(table.to_string(), "\"users\"");
561    /// # Ok::<(), hyperdb_api::Error>(())
562    /// ```
563    pub fn try_new(table: impl Into<String>) -> Result<Self> {
564        Ok(TableName {
565            database: None,
566            schema: None,
567            table: Name::try_new(table)?,
568        })
569    }
570
571    /// Builder method: Sets the schema qualifier.
572    ///
573    /// This method is part of the fluent builder pattern and can be chained.
574    /// Returns `Result<Self>` to allow fallible method chaining.
575    ///
576    /// # Errors
577    ///
578    /// Returns an error if the schema name is empty or exceeds the `PostgreSQL` identifier limit.
579    ///
580    /// # Example
581    ///
582    /// ```
583    /// use hyperdb_api::TableName;
584    ///
585    /// let table = TableName::try_new("users")?.with_schema("public")?;
586    /// assert_eq!(table.to_string(), "\"public\".\"users\"");
587    /// # Ok::<(), hyperdb_api::Error>(())
588    /// ```
589    pub fn with_schema(mut self, schema: impl Into<String>) -> Result<Self> {
590        self.schema = Some(Name::try_new(schema)?);
591        Ok(self)
592    }
593
594    /// Builder method: Sets the database qualifier.
595    ///
596    /// This method is part of the fluent builder pattern and can be chained.
597    /// Returns `Result<Self>` to allow fallible method chaining.
598    ///
599    /// # Errors
600    ///
601    /// Returns an error if the database name is empty or exceeds the `PostgreSQL` identifier limit.
602    ///
603    /// # Example
604    ///
605    /// ```
606    /// use hyperdb_api::TableName;
607    ///
608    /// let table = TableName::try_new("users")?
609    ///     .with_schema("public")?
610    ///     .with_database("mydb")?;
611    /// assert_eq!(table.to_string(), "\"mydb\".\"public\".\"users\"");
612    /// # Ok::<(), hyperdb_api::Error>(())
613    /// ```
614    pub fn with_database(mut self, database: impl Into<String>) -> Result<Self> {
615        self.database = Some(DatabaseName::try_new(database)?);
616        Ok(self)
617    }
618
619    /// Returns the database name, if any.
620    #[must_use]
621    pub fn database(&self) -> Option<&DatabaseName> {
622        self.database.as_ref()
623    }
624
625    /// Returns the schema name, if any.
626    #[must_use]
627    pub fn schema(&self) -> Option<&Name> {
628        self.schema.as_ref()
629    }
630
631    /// Returns the table name component.
632    pub fn table(&self) -> &Name {
633        &self.table
634    }
635
636    /// Returns the unescaped table name.
637    #[must_use]
638    pub fn unescaped(&self) -> &str {
639        self.table.unescaped()
640    }
641}
642
643impl fmt::Display for TableName {
644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645        if let Some(ref db) = self.database {
646            write!(f, "{db}.")?;
647        }
648        if let Some(ref schema) = self.schema {
649            write!(f, "{schema}.")?;
650        }
651        write!(f, "{}", self.table)
652    }
653}
654
655impl TryFrom<&str> for TableName {
656    type Error = Error;
657
658    fn try_from(s: &str) -> Result<Self> {
659        s.parse()
660    }
661}
662
663impl TryFrom<&String> for TableName {
664    type Error = Error;
665
666    fn try_from(s: &String) -> Result<Self> {
667        s.as_str().parse()
668    }
669}
670
671impl TryFrom<String> for TableName {
672    type Error = Error;
673
674    fn try_from(s: String) -> Result<Self> {
675        s.parse()
676    }
677}
678
679impl From<Name> for TableName {
680    fn from(name: Name) -> Self {
681        TableName {
682            database: None,
683            schema: None,
684            table: name,
685        }
686    }
687}
688
689impl FromStr for TableName {
690    type Err = Error;
691
692    fn from_str(s: &str) -> Result<Self> {
693        let mut parts = Vec::new();
694        let mut current = String::new();
695        let mut in_quotes = false;
696        let mut chars = s.chars().peekable();
697
698        while let Some(c) = chars.next() {
699            match c {
700                // Toggle the "in_quotes" state
701                '"' => {
702                    // Handle escaped quotes (double double-quotes)
703                    if in_quotes && chars.peek() == Some(&'"') {
704                        current.push('"');
705                        chars.next(); // skip the second quote
706                    } else {
707                        in_quotes = !in_quotes;
708                        // Don't add the quote character itself to current
709                    }
710                }
711                // Split on dots, but ONLY if we aren't inside quotes
712                '.' if !in_quotes => {
713                    if !current.is_empty() {
714                        parts.push(current.split_off(0));
715                    }
716                }
717                _ => current.push(c),
718            }
719        }
720        if !current.is_empty() {
721            parts.push(current);
722        }
723
724        // Now we use the same match logic as before
725        match parts.as_slice() {
726            [t] => TableName::try_new(t),
727            [s, t] => TableName::try_new(t)?.with_schema(s),
728            [d, s, t] => TableName::try_new(t)?.with_schema(s)?.with_database(d),
729            _ => Err(Error::InvalidName(format!("Invalid SQL identifier: {s}"))),
730        }
731    }
732}
733
734/// Creates a `TableName` with optional database and schema qualifiers.
735///
736/// This macro provides a convenient way to create table names with different
737/// levels of qualification. Returns a `Result` that must be handled with `?` or `.unwrap()`.
738///
739/// # Examples
740///
741/// ```
742/// use hyperdb_api::table_name;
743///
744/// // Simple table name
745/// let table = table_name!("users")?;
746/// assert_eq!(table.to_string(), "\"users\"");
747///
748/// // With schema
749/// let table = table_name!("public", "users")?;
750/// assert_eq!(table.to_string(), "\"public\".\"users\"");
751///
752/// // Fully qualified
753/// let table = table_name!("mydb", "public", "users")?;
754/// assert_eq!(table.to_string(), "\"mydb\".\"public\".\"users\"");
755/// # Ok::<(), hyperdb_api::Error>(())
756/// ```
757#[macro_export]
758macro_rules! table_name {
759    // Case: table_name!(db, schema, table)
760    ($db:expr, $schema:expr, $table:expr) => {
761        $crate::TableName::try_new($table)?
762            .with_schema($schema)?
763            .with_database($db)
764    };
765
766    // Case: table_name!(schema, table)
767    ($schema:expr, $table:expr) => {
768        $crate::TableName::try_new($table)?.with_schema($schema)
769    };
770
771    // Case: table_name!(table)
772    ($table:expr) => {
773        $crate::TableName::try_new($table)
774    };
775}
776
777/// Creates a `SchemaName` with optional database qualifier.
778///
779/// This macro provides a convenient way to create schema names with or without
780/// a database qualifier. Returns a `Result` that must be handled with `?` or `.unwrap()`.
781///
782/// # Examples
783///
784/// ```
785/// use hyperdb_api::schema_name;
786///
787/// // Simple schema name
788/// let schema = schema_name!("public")?;
789/// assert_eq!(schema.to_string(), "\"public\"");
790///
791/// // With database
792/// let schema = schema_name!("mydb", "public")?;
793/// assert_eq!(schema.to_string(), "\"mydb\".\"public\"");
794/// # Ok::<(), hyperdb_api::Error>(())
795/// ```
796#[macro_export]
797macro_rules! schema_name {
798    // Case: schema_name!(db, schema)
799    ($db:expr, $schema:expr) => {
800        $crate::SchemaName::try_new($schema)?.with_database($db)
801    };
802
803    // Case: schema_name!(schema)
804    ($schema:expr) => {
805        $crate::SchemaName::try_new($schema)
806    };
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    #[test]
814    fn test_escape_name() {
815        assert_eq!(escape_name("table").unwrap(), "\"table\"");
816        assert_eq!(escape_name("my_table").unwrap(), "\"my_table\"");
817        assert_eq!(escape_name("table\"quote").unwrap(), "\"table\"\"quote\"");
818        assert_eq!(escape_name("").unwrap(), "\"\"");
819    }
820
821    #[test]
822    fn test_escape_name_too_long() {
823        // 63 characters should be OK
824        let max_name = "a".repeat(PG_IDENTIFIER_LIMIT);
825        assert!(escape_name(&max_name).is_ok());
826
827        // 64 characters should fail
828        let too_long = "a".repeat(PG_IDENTIFIER_LIMIT + 1);
829        let err = escape_name(&too_long).unwrap_err();
830        assert!(err.to_string().contains("identifier limit"));
831    }
832
833    #[test]
834    fn test_escape_sql_path() {
835        assert_eq!(escape_sql_path("/tmp/data.hyper"), "\"/tmp/data.hyper\"");
836        assert_eq!(
837            escape_sql_path("/tmp/my \"db\".hyper"),
838            "\"/tmp/my \"\"db\"\".hyper\""
839        );
840        assert_eq!(escape_sql_path(""), "\"\"");
841
842        // Long paths are allowed (no 63-char limit)
843        let long_path = format!("/very/long/path/{}.hyper", "a".repeat(100));
844        let escaped = escape_sql_path(&long_path);
845        assert!(escaped.starts_with('"'));
846        assert!(escaped.ends_with('"'));
847    }
848
849    #[test]
850    fn test_escape_string_literal() {
851        assert_eq!(escape_string_literal("hello"), "'hello'");
852        assert_eq!(escape_string_literal("it's"), "'it''s'");
853        assert_eq!(escape_string_literal(""), "''");
854    }
855
856    #[test]
857    fn test_name() {
858        let name = Name::try_new("users").unwrap();
859        assert_eq!(name.to_string(), "\"users\"");
860        assert_eq!(name.unescaped(), "users");
861        assert!(!name.unescaped().is_empty());
862    }
863
864    #[test]
865    fn test_name_with_quotes() {
866        let name = Name::try_new("table\"name").unwrap();
867        assert_eq!(name.to_string(), "\"table\"\"name\"");
868        assert_eq!(name.unescaped(), "table\"name");
869    }
870
871    #[test]
872    fn test_database_name() {
873        let db = DatabaseName::try_new("mydb").unwrap();
874        assert_eq!(db.to_string(), "\"mydb\"");
875        assert_eq!(db.unescaped(), "mydb");
876    }
877
878    #[test]
879    fn test_schema_name() {
880        let schema = SchemaName::try_new("public").unwrap();
881        assert_eq!(schema.to_string(), "\"public\"");
882
883        let qualified = SchemaName::try_new("public")
884            .unwrap()
885            .with_database("mydb")
886            .unwrap();
887        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
888    }
889
890    #[test]
891    fn test_table_name() {
892        let simple = TableName::try_new("users").unwrap();
893        assert_eq!(simple.to_string(), "\"users\"");
894
895        let with_schema = TableName::try_new("users")
896            .unwrap()
897            .with_schema("public")
898            .unwrap();
899        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
900
901        let full = TableName::try_new("users")
902            .unwrap()
903            .with_schema("public")
904            .unwrap()
905            .with_database("mydb")
906            .unwrap();
907        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
908    }
909
910    #[test]
911    fn test_name_equality() {
912        let name1 = Name::try_new("test").unwrap();
913        let name2 = Name::try_new("test").unwrap();
914        let name3 = Name::try_new("other").unwrap();
915
916        assert_eq!(name1, name2);
917        assert_ne!(name1, name3);
918    }
919
920    #[test]
921    fn test_schema_name_from_str() {
922        // Simple schema name (using .parse() which uses FromStr)
923        let schema: SchemaName = "public".parse().unwrap();
924        assert_eq!(schema.to_string(), "\"public\"");
925        assert_eq!(schema.unescaped(), "public");
926
927        // Database.schema format
928        let qualified: SchemaName = "mydb.public".parse().unwrap();
929        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
930        assert_eq!(qualified.unescaped(), "public");
931        assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
932
933        // Quoted identifiers
934        let quoted: SchemaName = "\"my db\".\"my schema\"".parse().unwrap();
935        assert_eq!(quoted.to_string(), "\"my db\".\"my schema\"");
936        assert_eq!(quoted.unescaped(), "my schema");
937
938        // Escaped quotes
939        let escaped: SchemaName = "\"schema\"\"name\"".parse().unwrap();
940        assert_eq!(escaped.to_string(), "\"schema\"\"name\"");
941        assert_eq!(escaped.unescaped(), "schema\"name");
942
943        // Invalid formats (testing FromStr error handling)
944        assert!("db.schema.table".parse::<SchemaName>().is_err());
945        assert!("".parse::<SchemaName>().is_err());
946    }
947
948    #[test]
949    fn test_table_name_from_str() {
950        // Simple table name (using .parse() which uses FromStr)
951        let table: TableName = "users".parse().unwrap();
952        assert_eq!(table.to_string(), "\"users\"");
953        assert_eq!(table.unescaped(), "users");
954
955        // Schema.table format
956        let with_schema: TableName = "public.users".parse().unwrap();
957        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
958        assert_eq!(with_schema.unescaped(), "users");
959        assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
960
961        // Database.schema.table format
962        let full: TableName = "mydb.public.users".parse().unwrap();
963        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
964        assert_eq!(full.unescaped(), "users");
965        assert_eq!(full.schema().unwrap().unescaped(), "public");
966        assert_eq!(full.database().unwrap().unescaped(), "mydb");
967
968        // Quoted identifiers
969        let quoted: TableName = "\"my db\".\"my schema\".\"my table\"".parse().unwrap();
970        assert_eq!(quoted.to_string(), "\"my db\".\"my schema\".\"my table\"");
971        assert_eq!(quoted.unescaped(), "my table");
972
973        // Escaped quotes
974        let escaped: TableName = "\"table\"\"name\"".parse().unwrap();
975        assert_eq!(escaped.to_string(), "\"table\"\"name\"");
976        assert_eq!(escaped.unescaped(), "table\"name");
977
978        // Dots inside quoted identifiers should not split
979        let with_dots: TableName = "\"schema.name\".\"table.name\"".parse().unwrap();
980        assert_eq!(with_dots.to_string(), "\"schema.name\".\"table.name\"");
981        assert_eq!(with_dots.schema().unwrap().unescaped(), "schema.name");
982        assert_eq!(with_dots.unescaped(), "table.name");
983
984        // Invalid formats (testing FromStr error handling)
985        assert!("db.schema.table.extra".parse::<TableName>().is_err());
986        assert!("".parse::<TableName>().is_err());
987    }
988
989    #[test]
990    fn test_schema_name_macro() -> Result<()> {
991        // Simple schema name
992        let schema = schema_name!("public")?;
993        assert_eq!(schema.to_string(), "\"public\"");
994        assert_eq!(schema.unescaped(), "public");
995
996        // With database
997        let qualified = schema_name!("mydb", "public")?;
998        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
999        assert_eq!(qualified.unescaped(), "public");
1000        assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
1001        Ok(())
1002    }
1003
1004    #[test]
1005    fn test_table_name_macro() -> Result<()> {
1006        // Simple table name
1007        let table = table_name!("users")?;
1008        assert_eq!(table.to_string(), "\"users\"");
1009        assert_eq!(table.unescaped(), "users");
1010
1011        // With schema
1012        let with_schema = table_name!("public", "users")?;
1013        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
1014        assert_eq!(with_schema.unescaped(), "users");
1015        assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
1016
1017        // Fully qualified
1018        let full = table_name!("mydb", "public", "users")?;
1019        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
1020        assert_eq!(full.unescaped(), "users");
1021        assert_eq!(full.schema().unwrap().unescaped(), "public");
1022        assert_eq!(full.database().unwrap().unescaped(), "mydb");
1023        Ok(())
1024    }
1025
1026    #[test]
1027    fn test_schema_name_try_from() {
1028        // Simple schema name using TryFrom
1029        let schema: SchemaName = "public".try_into().unwrap();
1030        assert_eq!(schema.to_string(), "\"public\"");
1031        assert_eq!(schema.unescaped(), "public");
1032
1033        // Qualified schema using TryFrom (parses dot-separated format)
1034        let qualified: SchemaName = "mydb.public".try_into().unwrap();
1035        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
1036        assert_eq!(qualified.unescaped(), "public");
1037        assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
1038
1039        // From String
1040        let schema_string: SchemaName = String::from("public").try_into().unwrap();
1041        assert_eq!(schema_string.to_string(), "\"public\"");
1042    }
1043
1044    #[test]
1045    fn test_table_name_try_from() {
1046        // Simple table name using TryFrom
1047        let table: TableName = "users".try_into().unwrap();
1048        assert_eq!(table.to_string(), "\"users\"");
1049        assert_eq!(table.unescaped(), "users");
1050
1051        // With schema using TryFrom (parses dot-separated format)
1052        let with_schema: TableName = "public.users".try_into().unwrap();
1053        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
1054        assert_eq!(with_schema.unescaped(), "users");
1055        assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
1056
1057        // Fully qualified using TryFrom (parses dot-separated format)
1058        let full: TableName = "mydb.public.users".try_into().unwrap();
1059        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
1060        assert_eq!(full.unescaped(), "users");
1061        assert_eq!(full.schema().unwrap().unescaped(), "public");
1062        assert_eq!(full.database().unwrap().unescaped(), "mydb");
1063
1064        // From String
1065        let table_string: TableName = String::from("users").try_into().unwrap();
1066        assert_eq!(table_string.to_string(), "\"users\"");
1067
1068        // Invalid format returns error
1069        let invalid: std::result::Result<TableName, _> = "db.schema.table.extra".try_into();
1070        assert!(invalid.is_err());
1071    }
1072}