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    #[must_use]
318    pub fn name(&self) -> &Name {
319        &self.name
320    }
321
322    /// Returns the unescaped name.
323    #[must_use]
324    pub fn unescaped(&self) -> &str {
325        self.name.unescaped()
326    }
327}
328
329impl fmt::Display for DatabaseName {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        write!(f, "{}", self.name)
332    }
333}
334
335impl TryFrom<&str> for DatabaseName {
336    type Error = Error;
337
338    fn try_from(s: &str) -> Result<Self> {
339        Self::try_new(s)
340    }
341}
342
343impl TryFrom<&String> for DatabaseName {
344    type Error = Error;
345
346    fn try_from(s: &String) -> Result<Self> {
347        Self::try_new(s.as_str())
348    }
349}
350
351impl TryFrom<String> for DatabaseName {
352    type Error = Error;
353
354    fn try_from(s: String) -> Result<Self> {
355        Self::try_new(s)
356    }
357}
358
359impl From<Name> for DatabaseName {
360    fn from(name: Name) -> Self {
361        DatabaseName { name }
362    }
363}
364
365impl FromStr for DatabaseName {
366    type Err = Error;
367
368    fn from_str(s: &str) -> Result<Self> {
369        Self::try_new(s)
370    }
371}
372
373/// Represents an escaped SQL schema name with optional database qualifier.
374///
375/// Uses the fluent builder pattern for constructing qualified schema names.
376///
377/// # Example
378///
379/// ```
380/// use hyperdb_api::{SchemaName, Result};
381///
382/// # fn main() -> Result<()> {
383/// // Simple schema name
384/// let schema = SchemaName::try_new("public")?;
385/// assert_eq!(schema.to_string(), "\"public\"");
386///
387/// // Qualified schema name using fluent builder
388/// let qualified = SchemaName::try_new("public")?.with_database("mydb")?;
389/// assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
390/// # Ok(())
391/// # }
392/// ```
393#[derive(Clone, Debug, PartialEq, Eq, Hash)]
394#[must_use = "SchemaName represents a validated schema identifier that should not be discarded. Use it in your table definitions or queries"]
395pub struct SchemaName {
396    database: Option<DatabaseName>,
397    schema: Name,
398}
399
400impl SchemaName {
401    /// Creates a new schema name without a database qualifier (the starting point).
402    ///
403    /// # Errors
404    ///
405    /// Returns an error if the schema name is empty or exceeds the identifier length limit.
406    ///
407    /// # Example
408    ///
409    /// ```no_run
410    /// use hyperdb_api::SchemaName;
411    ///
412    /// let schema = SchemaName::try_new("public")?;
413    /// assert_eq!(schema.to_string(), "\"public\"");
414    /// # Ok::<(), hyperdb_api::Error>(())
415    /// ```
416    pub fn try_new(schema: impl Into<String>) -> Result<Self> {
417        Ok(SchemaName {
418            database: None,
419            schema: Name::try_new(schema)?,
420        })
421    }
422
423    /// Builder method: Sets the database qualifier.
424    ///
425    /// This method is part of the fluent builder pattern and can be chained.
426    /// Returns `Result<Self>` to allow fallible method chaining.
427    ///
428    /// # Errors
429    ///
430    /// Returns an error if the database name is empty or exceeds the identifier length limit.
431    ///
432    /// # Example
433    ///
434    /// ```
435    /// use hyperdb_api::SchemaName;
436    ///
437    /// let schema = SchemaName::try_new("public")?.with_database("mydb")?;
438    /// assert_eq!(schema.to_string(), "\"mydb\".\"public\"");
439    /// # Ok::<(), hyperdb_api::Error>(())
440    /// ```
441    pub fn with_database(mut self, database: impl Into<String>) -> Result<Self> {
442        self.database = Some(DatabaseName::try_new(database)?);
443        Ok(self)
444    }
445
446    /// Returns the database name, if any.
447    #[must_use]
448    pub fn database(&self) -> Option<&DatabaseName> {
449        self.database.as_ref()
450    }
451
452    /// Returns the schema name component.
453    #[must_use]
454    pub fn schema(&self) -> &Name {
455        &self.schema
456    }
457
458    /// Returns the unescaped schema name.
459    #[must_use]
460    pub fn unescaped(&self) -> &str {
461        self.schema.unescaped()
462    }
463}
464
465impl fmt::Display for SchemaName {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        if let Some(ref db) = self.database {
468            write!(f, "{}.{}", db, self.schema)
469        } else {
470            write!(f, "{}", self.schema)
471        }
472    }
473}
474
475impl TryFrom<&str> for SchemaName {
476    type Error = Error;
477
478    fn try_from(s: &str) -> Result<Self> {
479        s.parse()
480    }
481}
482
483impl TryFrom<&String> for SchemaName {
484    type Error = Error;
485
486    fn try_from(s: &String) -> Result<Self> {
487        s.as_str().parse()
488    }
489}
490
491impl TryFrom<String> for SchemaName {
492    type Error = Error;
493
494    fn try_from(s: String) -> Result<Self> {
495        s.parse()
496    }
497}
498
499impl From<Name> for SchemaName {
500    fn from(name: Name) -> Self {
501        SchemaName {
502            database: None,
503            schema: name,
504        }
505    }
506}
507
508impl FromStr for SchemaName {
509    type Err = Error;
510
511    fn from_str(s: &str) -> Result<Self> {
512        let parts = parse_qualified_identifier(s);
513
514        // Parse database.schema format
515        match parts.as_slice() {
516            [s] => SchemaName::try_new(s),
517            [d, s] => SchemaName::try_new(s)?.with_database(d),
518            _ => Err(Error::invalid_name(format!("Invalid SQL identifier: {s}"))),
519        }
520    }
521}
522
523/// Represents a fully qualified SQL table name.
524///
525/// A table name can optionally include database and schema qualifiers.
526/// Uses the fluent builder pattern for constructing qualified table names.
527///
528/// # Example
529///
530/// ```
531/// use hyperdb_api::{TableName, Result};
532///
533/// # fn main() -> Result<()> {
534/// // Simple table name
535/// let table = TableName::try_new("users")?;
536/// assert_eq!(table.to_string(), "\"users\"");
537///
538/// // With schema using fluent builder
539/// let with_schema = TableName::try_new("users")?.with_schema("public")?;
540/// assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
541///
542/// // Fully qualified using fluent builder
543/// let full = TableName::try_new("users")?
544///     .with_schema("public")?
545///     .with_database("mydb")?;
546/// assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
547/// # Ok(())
548/// # }
549/// ```
550#[derive(Clone, Debug, PartialEq, Eq, Hash)]
551#[must_use = "TableName represents a validated table identifier that should not be discarded. Use it in your queries or table operations"]
552pub struct TableName {
553    database: Option<DatabaseName>,
554    schema: Option<Name>,
555    table: Name,
556}
557
558impl TableName {
559    /// Creates a new table name without qualifiers (the starting point).
560    ///
561    /// # Errors
562    ///
563    /// Returns an error if the table name is empty or exceeds the identifier length limit.
564    ///
565    /// # Example
566    ///
567    /// ```
568    /// use hyperdb_api::TableName;
569    ///
570    /// let table = TableName::try_new("users")?;
571    /// assert_eq!(table.to_string(), "\"users\"");
572    /// # Ok::<(), hyperdb_api::Error>(())
573    /// ```
574    pub fn try_new(table: impl Into<String>) -> Result<Self> {
575        Ok(TableName {
576            database: None,
577            schema: None,
578            table: Name::try_new(table)?,
579        })
580    }
581
582    /// Builder method: Sets the schema qualifier.
583    ///
584    /// This method is part of the fluent builder pattern and can be chained.
585    /// Returns `Result<Self>` to allow fallible method chaining.
586    ///
587    /// # Errors
588    ///
589    /// Returns an error if the schema name is empty or exceeds the identifier length limit.
590    ///
591    /// # Example
592    ///
593    /// ```
594    /// use hyperdb_api::TableName;
595    ///
596    /// let table = TableName::try_new("users")?.with_schema("public")?;
597    /// assert_eq!(table.to_string(), "\"public\".\"users\"");
598    /// # Ok::<(), hyperdb_api::Error>(())
599    /// ```
600    pub fn with_schema(mut self, schema: impl Into<String>) -> Result<Self> {
601        self.schema = Some(Name::try_new(schema)?);
602        Ok(self)
603    }
604
605    /// Builder method: Sets the database qualifier.
606    ///
607    /// This method is part of the fluent builder pattern and can be chained.
608    /// Returns `Result<Self>` to allow fallible method chaining.
609    ///
610    /// # Errors
611    ///
612    /// Returns an error if the database name is empty or exceeds the identifier length limit.
613    ///
614    /// # Example
615    ///
616    /// ```
617    /// use hyperdb_api::TableName;
618    ///
619    /// let table = TableName::try_new("users")?
620    ///     .with_schema("public")?
621    ///     .with_database("mydb")?;
622    /// assert_eq!(table.to_string(), "\"mydb\".\"public\".\"users\"");
623    /// # Ok::<(), hyperdb_api::Error>(())
624    /// ```
625    pub fn with_database(mut self, database: impl Into<String>) -> Result<Self> {
626        self.database = Some(DatabaseName::try_new(database)?);
627        Ok(self)
628    }
629
630    /// Returns the database name, if any.
631    #[must_use]
632    pub fn database(&self) -> Option<&DatabaseName> {
633        self.database.as_ref()
634    }
635
636    /// Returns the schema name, if any.
637    #[must_use]
638    pub fn schema(&self) -> Option<&Name> {
639        self.schema.as_ref()
640    }
641
642    /// Returns the table name component.
643    #[must_use]
644    pub fn table(&self) -> &Name {
645        &self.table
646    }
647
648    /// Returns the unescaped table name.
649    #[must_use]
650    pub fn unescaped(&self) -> &str {
651        self.table.unescaped()
652    }
653}
654
655impl fmt::Display for TableName {
656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657        if let Some(ref db) = self.database {
658            write!(f, "{db}.")?;
659        }
660        if let Some(ref schema) = self.schema {
661            write!(f, "{schema}.")?;
662        }
663        write!(f, "{}", self.table)
664    }
665}
666
667impl TryFrom<&str> for TableName {
668    type Error = Error;
669
670    fn try_from(s: &str) -> Result<Self> {
671        s.parse()
672    }
673}
674
675impl TryFrom<&String> for TableName {
676    type Error = Error;
677
678    fn try_from(s: &String) -> Result<Self> {
679        s.as_str().parse()
680    }
681}
682
683impl TryFrom<String> for TableName {
684    type Error = Error;
685
686    fn try_from(s: String) -> Result<Self> {
687        s.parse()
688    }
689}
690
691impl From<Name> for TableName {
692    fn from(name: Name) -> Self {
693        TableName {
694            database: None,
695            schema: None,
696            table: name,
697        }
698    }
699}
700
701impl FromStr for TableName {
702    type Err = Error;
703
704    fn from_str(s: &str) -> Result<Self> {
705        let mut parts = Vec::new();
706        let mut current = String::new();
707        let mut in_quotes = false;
708        let mut chars = s.chars().peekable();
709
710        while let Some(c) = chars.next() {
711            match c {
712                // Toggle the "in_quotes" state
713                '"' => {
714                    // Handle escaped quotes (double double-quotes)
715                    if in_quotes && chars.peek() == Some(&'"') {
716                        current.push('"');
717                        chars.next(); // skip the second quote
718                    } else {
719                        in_quotes = !in_quotes;
720                        // Don't add the quote character itself to current
721                    }
722                }
723                // Split on dots, but ONLY if we aren't inside quotes
724                '.' if !in_quotes => {
725                    if !current.is_empty() {
726                        parts.push(current.split_off(0));
727                    }
728                }
729                _ => current.push(c),
730            }
731        }
732        if !current.is_empty() {
733            parts.push(current);
734        }
735
736        // Now we use the same match logic as before
737        match parts.as_slice() {
738            [t] => TableName::try_new(t),
739            [s, t] => TableName::try_new(t)?.with_schema(s),
740            [d, s, t] => TableName::try_new(t)?.with_schema(s)?.with_database(d),
741            _ => Err(Error::invalid_name(format!("Invalid SQL identifier: {s}"))),
742        }
743    }
744}
745
746/// Creates a `TableName` with optional database and schema qualifiers.
747///
748/// This macro provides a convenient way to create table names with different
749/// levels of qualification. Returns a `Result` that must be handled with `?` or `.unwrap()`.
750///
751/// # Examples
752///
753/// ```
754/// use hyperdb_api::table_name;
755///
756/// // Simple table name
757/// let table = table_name!("users")?;
758/// assert_eq!(table.to_string(), "\"users\"");
759///
760/// // With schema
761/// let table = table_name!("public", "users")?;
762/// assert_eq!(table.to_string(), "\"public\".\"users\"");
763///
764/// // Fully qualified
765/// let table = table_name!("mydb", "public", "users")?;
766/// assert_eq!(table.to_string(), "\"mydb\".\"public\".\"users\"");
767/// # Ok::<(), hyperdb_api::Error>(())
768/// ```
769#[macro_export]
770macro_rules! table_name {
771    // Case: table_name!(db, schema, table)
772    ($db:expr, $schema:expr, $table:expr) => {
773        $crate::TableName::try_new($table)?
774            .with_schema($schema)?
775            .with_database($db)
776    };
777
778    // Case: table_name!(schema, table)
779    ($schema:expr, $table:expr) => {
780        $crate::TableName::try_new($table)?.with_schema($schema)
781    };
782
783    // Case: table_name!(table)
784    ($table:expr) => {
785        $crate::TableName::try_new($table)
786    };
787}
788
789/// Creates a `SchemaName` with optional database qualifier.
790///
791/// This macro provides a convenient way to create schema names with or without
792/// a database qualifier. Returns a `Result` that must be handled with `?` or `.unwrap()`.
793///
794/// # Examples
795///
796/// ```
797/// use hyperdb_api::schema_name;
798///
799/// // Simple schema name
800/// let schema = schema_name!("public")?;
801/// assert_eq!(schema.to_string(), "\"public\"");
802///
803/// // With database
804/// let schema = schema_name!("mydb", "public")?;
805/// assert_eq!(schema.to_string(), "\"mydb\".\"public\"");
806/// # Ok::<(), hyperdb_api::Error>(())
807/// ```
808#[macro_export]
809macro_rules! schema_name {
810    // Case: schema_name!(db, schema)
811    ($db:expr, $schema:expr) => {
812        $crate::SchemaName::try_new($schema)?.with_database($db)
813    };
814
815    // Case: schema_name!(schema)
816    ($schema:expr) => {
817        $crate::SchemaName::try_new($schema)
818    };
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824
825    #[test]
826    fn test_escape_name() {
827        assert_eq!(escape_name("table").unwrap(), "\"table\"");
828        assert_eq!(escape_name("my_table").unwrap(), "\"my_table\"");
829        assert_eq!(escape_name("table\"quote").unwrap(), "\"table\"\"quote\"");
830        assert_eq!(escape_name("").unwrap(), "\"\"");
831    }
832
833    #[test]
834    fn test_escape_name_too_long() {
835        let max_name = "a".repeat(IDENTIFIER_LIMIT);
836        assert!(escape_name(&max_name).is_ok());
837
838        let too_long = "a".repeat(IDENTIFIER_LIMIT + 1);
839        let err = escape_name(&too_long).unwrap_err();
840        assert!(err.to_string().contains("identifier limit"));
841    }
842
843    #[test]
844    fn names_longer_than_postgresql_namedatalen_are_accepted() {
845        // Hyper stores a 93-character table name happily, so the typed name
846        // API must be able to address one. Rejecting at PostgreSQL's 63
847        // meant a whole-database copy failed on a table the engine had
848        // already written.
849        let name = "a".repeat(93);
850        assert!(escape_name(&name).is_ok());
851        assert!(Name::try_new(name.clone()).is_ok());
852        assert!(TableName::try_new(name).is_ok());
853    }
854
855    #[test]
856    fn test_escape_sql_path() {
857        assert_eq!(escape_sql_path("/tmp/data.hyper"), "\"/tmp/data.hyper\"");
858        assert_eq!(
859            escape_sql_path("/tmp/my \"db\".hyper"),
860            "\"/tmp/my \"\"db\"\".hyper\""
861        );
862        assert_eq!(escape_sql_path(""), "\"\"");
863
864        // Long paths are allowed (no 63-char limit)
865        let long_path = format!("/very/long/path/{}.hyper", "a".repeat(100));
866        let escaped = escape_sql_path(&long_path);
867        assert!(escaped.starts_with('"'));
868        assert!(escaped.ends_with('"'));
869    }
870
871    #[test]
872    fn test_escape_string_literal() {
873        assert_eq!(escape_string_literal("hello"), "'hello'");
874        assert_eq!(escape_string_literal("it's"), "'it''s'");
875        assert_eq!(escape_string_literal(""), "''");
876    }
877
878    #[test]
879    fn test_name() {
880        let name = Name::try_new("users").unwrap();
881        assert_eq!(name.to_string(), "\"users\"");
882        assert_eq!(name.unescaped(), "users");
883        assert_ne!(name.unescaped(), "");
884    }
885
886    #[test]
887    fn test_name_with_quotes() {
888        let name = Name::try_new("table\"name").unwrap();
889        assert_eq!(name.to_string(), "\"table\"\"name\"");
890        assert_eq!(name.unescaped(), "table\"name");
891    }
892
893    #[test]
894    fn test_database_name() {
895        let db = DatabaseName::try_new("mydb").unwrap();
896        assert_eq!(db.to_string(), "\"mydb\"");
897        assert_eq!(db.unescaped(), "mydb");
898    }
899
900    #[test]
901    fn test_schema_name() {
902        let schema = SchemaName::try_new("public").unwrap();
903        assert_eq!(schema.to_string(), "\"public\"");
904
905        let qualified = SchemaName::try_new("public")
906            .unwrap()
907            .with_database("mydb")
908            .unwrap();
909        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
910    }
911
912    #[test]
913    fn test_table_name() {
914        let simple = TableName::try_new("users").unwrap();
915        assert_eq!(simple.to_string(), "\"users\"");
916
917        let with_schema = TableName::try_new("users")
918            .unwrap()
919            .with_schema("public")
920            .unwrap();
921        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
922
923        let full = TableName::try_new("users")
924            .unwrap()
925            .with_schema("public")
926            .unwrap()
927            .with_database("mydb")
928            .unwrap();
929        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
930    }
931
932    #[test]
933    fn test_name_equality() {
934        let name1 = Name::try_new("test").unwrap();
935        let name2 = Name::try_new("test").unwrap();
936        let name3 = Name::try_new("other").unwrap();
937
938        assert_eq!(name1, name2);
939        assert_ne!(name1, name3);
940    }
941
942    #[test]
943    fn test_schema_name_from_str() {
944        // Simple schema name (using .parse() which uses FromStr)
945        let schema: SchemaName = "public".parse().unwrap();
946        assert_eq!(schema.to_string(), "\"public\"");
947        assert_eq!(schema.unescaped(), "public");
948
949        // Database.schema format
950        let qualified: SchemaName = "mydb.public".parse().unwrap();
951        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
952        assert_eq!(qualified.unescaped(), "public");
953        assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
954
955        // Quoted identifiers
956        let quoted: SchemaName = "\"my db\".\"my schema\"".parse().unwrap();
957        assert_eq!(quoted.to_string(), "\"my db\".\"my schema\"");
958        assert_eq!(quoted.unescaped(), "my schema");
959
960        // Escaped quotes
961        let escaped: SchemaName = "\"schema\"\"name\"".parse().unwrap();
962        assert_eq!(escaped.to_string(), "\"schema\"\"name\"");
963        assert_eq!(escaped.unescaped(), "schema\"name");
964
965        // Invalid formats (testing FromStr error handling)
966        assert!("db.schema.table".parse::<SchemaName>().is_err());
967        assert!("".parse::<SchemaName>().is_err());
968    }
969
970    #[test]
971    fn test_table_name_from_str() {
972        // Simple table name (using .parse() which uses FromStr)
973        let table: TableName = "users".parse().unwrap();
974        assert_eq!(table.to_string(), "\"users\"");
975        assert_eq!(table.unescaped(), "users");
976
977        // Schema.table format
978        let with_schema: TableName = "public.users".parse().unwrap();
979        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
980        assert_eq!(with_schema.unescaped(), "users");
981        assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
982
983        // Database.schema.table format
984        let full: TableName = "mydb.public.users".parse().unwrap();
985        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
986        assert_eq!(full.unescaped(), "users");
987        assert_eq!(full.schema().unwrap().unescaped(), "public");
988        assert_eq!(full.database().unwrap().unescaped(), "mydb");
989
990        // Quoted identifiers
991        let quoted: TableName = "\"my db\".\"my schema\".\"my table\"".parse().unwrap();
992        assert_eq!(quoted.to_string(), "\"my db\".\"my schema\".\"my table\"");
993        assert_eq!(quoted.unescaped(), "my table");
994
995        // Escaped quotes
996        let escaped: TableName = "\"table\"\"name\"".parse().unwrap();
997        assert_eq!(escaped.to_string(), "\"table\"\"name\"");
998        assert_eq!(escaped.unescaped(), "table\"name");
999
1000        // Dots inside quoted identifiers should not split
1001        let with_dots: TableName = "\"schema.name\".\"table.name\"".parse().unwrap();
1002        assert_eq!(with_dots.to_string(), "\"schema.name\".\"table.name\"");
1003        assert_eq!(with_dots.schema().unwrap().unescaped(), "schema.name");
1004        assert_eq!(with_dots.unescaped(), "table.name");
1005
1006        // Invalid formats (testing FromStr error handling)
1007        assert!("db.schema.table.extra".parse::<TableName>().is_err());
1008        assert!("".parse::<TableName>().is_err());
1009    }
1010
1011    #[test]
1012    fn test_schema_name_macro() -> Result<()> {
1013        // Simple schema name
1014        let schema = schema_name!("public")?;
1015        assert_eq!(schema.to_string(), "\"public\"");
1016        assert_eq!(schema.unescaped(), "public");
1017
1018        // With database
1019        let qualified = schema_name!("mydb", "public")?;
1020        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
1021        assert_eq!(qualified.unescaped(), "public");
1022        assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
1023        Ok(())
1024    }
1025
1026    #[test]
1027    fn test_table_name_macro() -> Result<()> {
1028        // Simple table name
1029        let table = table_name!("users")?;
1030        assert_eq!(table.to_string(), "\"users\"");
1031        assert_eq!(table.unescaped(), "users");
1032
1033        // With schema
1034        let with_schema = table_name!("public", "users")?;
1035        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
1036        assert_eq!(with_schema.unescaped(), "users");
1037        assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
1038
1039        // Fully qualified
1040        let full = table_name!("mydb", "public", "users")?;
1041        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
1042        assert_eq!(full.unescaped(), "users");
1043        assert_eq!(full.schema().unwrap().unescaped(), "public");
1044        assert_eq!(full.database().unwrap().unescaped(), "mydb");
1045        Ok(())
1046    }
1047
1048    #[test]
1049    fn test_schema_name_try_from() {
1050        // Simple schema name using TryFrom
1051        let schema: SchemaName = "public".try_into().unwrap();
1052        assert_eq!(schema.to_string(), "\"public\"");
1053        assert_eq!(schema.unescaped(), "public");
1054
1055        // Qualified schema using TryFrom (parses dot-separated format)
1056        let qualified: SchemaName = "mydb.public".try_into().unwrap();
1057        assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
1058        assert_eq!(qualified.unescaped(), "public");
1059        assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
1060
1061        // From String
1062        let schema_string: SchemaName = String::from("public").try_into().unwrap();
1063        assert_eq!(schema_string.to_string(), "\"public\"");
1064    }
1065
1066    #[test]
1067    fn test_table_name_try_from() {
1068        // Simple table name using TryFrom
1069        let table: TableName = "users".try_into().unwrap();
1070        assert_eq!(table.to_string(), "\"users\"");
1071        assert_eq!(table.unescaped(), "users");
1072
1073        // With schema using TryFrom (parses dot-separated format)
1074        let with_schema: TableName = "public.users".try_into().unwrap();
1075        assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
1076        assert_eq!(with_schema.unescaped(), "users");
1077        assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
1078
1079        // Fully qualified using TryFrom (parses dot-separated format)
1080        let full: TableName = "mydb.public.users".try_into().unwrap();
1081        assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
1082        assert_eq!(full.unescaped(), "users");
1083        assert_eq!(full.schema().unwrap().unescaped(), "public");
1084        assert_eq!(full.database().unwrap().unescaped(), "mydb");
1085
1086        // From String
1087        let table_string: TableName = String::from("users").try_into().unwrap();
1088        assert_eq!(table_string.to_string(), "\"users\"");
1089
1090        // Invalid format returns error
1091        let invalid: std::result::Result<TableName, _> = "db.schema.table.extra".try_into();
1092        assert!(invalid.is_err());
1093    }
1094}