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