Skip to main content

hyperdb_api/
table_definition.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Table definition types.
5
6use std::borrow::Cow;
7
8use crate::error::{Error, Result};
9use hyperdb_api_core::types::{ColumnDefinition as TypesColumnDefinition, Nullability, SqlType};
10
11/// Possible persistence levels for database objects.
12///
13/// This enum controls whether a table is permanent (persisted to disk) or
14/// temporary (only available in the current session).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum Persistence {
17    /// Permanent: The table is persisted to disk and survives session restarts.
18    #[default]
19    Permanent,
20    /// Temporary: The table only exists for the current session and is not persisted.
21    Temporary,
22}
23
24impl std::fmt::Display for Persistence {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Persistence::Permanent => write!(f, "Permanent"),
28            Persistence::Temporary => write!(f, "Temporary"),
29        }
30    }
31}
32
33/// Internal representation of a column's SQL type.
34///
35/// This enum ensures a single source of truth for the type - either a structured
36/// `SqlType` or a raw string type name.
37#[derive(Debug, Clone)]
38enum SqlTypeOrName {
39    /// Structured SQL type with full type information.
40    SqlType(SqlType),
41    /// Raw string type name (used when `SqlType` is unavailable, e.g., from catalog queries).
42    TypeName(String),
43}
44
45impl SqlTypeOrName {
46    /// Returns the type name as a string.
47    ///
48    /// Returns a borrowed reference for `TypeName` variant to avoid allocation,
49    /// and an owned string for `SqlType` variant (requires formatting).
50    fn type_name(&self) -> Cow<'_, str> {
51        match self {
52            SqlTypeOrName::SqlType(t) => Cow::Owned(t.to_string()),
53            SqlTypeOrName::TypeName(s) => Cow::Borrowed(s),
54        }
55    }
56
57    /// Returns the SQL type if this is a structured type.
58    fn sql_type(&self) -> Option<SqlType> {
59        match self {
60            SqlTypeOrName::SqlType(t) => Some(*t),
61            SqlTypeOrName::TypeName(_) => None,
62        }
63    }
64}
65
66/// A column definition.
67///
68/// This struct supports both string-based type names (for simplicity) and
69/// SqlType-based definitions (for type safety). Internally, it uses a single source
70/// of truth to avoid synchronization issues.
71#[derive(Debug, Clone)]
72pub struct ColumnDefinition {
73    /// Column name.
74    pub name: String,
75    /// SQL type representation (either structured `SqlType` or raw string).
76    sql_type_or_name: SqlTypeOrName,
77    /// Whether the column is nullable.
78    pub nullable: bool,
79    /// The collation for text columns (e.g., "`en_US`", "binary").
80    collation: Option<String>,
81}
82
83impl ColumnDefinition {
84    /// Creates a new column definition using a type name string.
85    ///
86    /// # Example
87    ///
88    /// ```
89    /// use hyperdb_api::ColumnDefinition;
90    ///
91    /// let col = ColumnDefinition::new("id", "INT", false);
92    /// assert_eq!(col.name, "id");
93    /// assert_eq!(col.type_name(), "INT");
94    /// ```
95    pub fn new(name: impl Into<String>, type_name: impl Into<String>, nullable: bool) -> Self {
96        ColumnDefinition {
97            name: name.into(),
98            sql_type_or_name: SqlTypeOrName::TypeName(type_name.into()),
99            nullable,
100            collation: None,
101        }
102    }
103
104    /// Creates a column definition using `SqlType`.
105    ///
106    /// # Example
107    ///
108    /// ```
109    /// use hyperdb_api::ColumnDefinition;
110    /// use hyperdb_api_core::types::{SqlType, Nullability};
111    ///
112    /// let col = ColumnDefinition::with_sql_type("id", SqlType::int(), Nullability::NotNullable);
113    /// assert_eq!(col.name, "id");
114    /// assert!(!col.nullable);
115    /// ```
116    pub fn with_sql_type(
117        name: impl Into<String>,
118        sql_type: SqlType,
119        nullability: Nullability,
120    ) -> Self {
121        ColumnDefinition {
122            name: name.into(),
123            sql_type_or_name: SqlTypeOrName::SqlType(sql_type),
124            nullable: nullability.is_nullable(),
125            collation: None,
126        }
127    }
128
129    /// Creates a column definition with a collation.
130    ///
131    /// The collation specifies the sorting and comparison behavior for text columns.
132    ///
133    /// # Example
134    ///
135    /// ```
136    /// use hyperdb_api::ColumnDefinition;
137    /// use hyperdb_api_core::types::{SqlType, Nullability};
138    ///
139    /// let col = ColumnDefinition::with_collation("name", SqlType::text(), "en_US", Nullability::Nullable);
140    /// assert_eq!(col.collation(), Some("en_US"));
141    /// ```
142    pub fn with_collation(
143        name: impl Into<String>,
144        sql_type: SqlType,
145        collation: impl Into<String>,
146        nullability: Nullability,
147    ) -> Self {
148        ColumnDefinition {
149            name: name.into(),
150            sql_type_or_name: SqlTypeOrName::SqlType(sql_type),
151            nullable: nullability.is_nullable(),
152            collation: Some(collation.into()),
153        }
154    }
155
156    /// Creates a nullable column definition using `SqlType`.
157    pub fn nullable(name: impl Into<String>, sql_type: SqlType) -> Self {
158        Self::with_sql_type(name, sql_type, Nullability::Nullable)
159    }
160
161    /// Creates a non-nullable column definition using `SqlType`.
162    pub fn not_null(name: impl Into<String>, sql_type: SqlType) -> Self {
163        Self::with_sql_type(name, sql_type, Nullability::NotNullable)
164    }
165
166    /// Returns the nullability as a Nullability enum.
167    #[must_use]
168    pub fn nullability(&self) -> Nullability {
169        if self.nullable {
170            Nullability::Nullable
171        } else {
172            Nullability::NotNullable
173        }
174    }
175
176    /// Returns the SQL type if this column was created with a structured type.
177    #[must_use]
178    pub fn sql_type(&self) -> Option<SqlType> {
179        self.sql_type_or_name.sql_type()
180    }
181
182    /// Returns the type name string representation.
183    ///
184    /// When created with `SqlType`, this is derived from it. Otherwise, it's the
185    /// string provided during construction.
186    ///
187    /// Returns `Cow<str>` to avoid allocation when the type name is already stored
188    /// as a string internally.
189    #[must_use]
190    pub fn type_name(&self) -> Cow<'_, str> {
191        self.sql_type_or_name.type_name()
192    }
193
194    /// Returns the collation if set.
195    #[must_use]
196    pub fn collation(&self) -> Option<&str> {
197        self.collation.as_deref()
198    }
199
200    /// Sets the collation for this column.
201    pub fn set_collation(&mut self, collation: impl Into<String>) {
202        self.collation = Some(collation.into());
203    }
204
205    /// Sets the SQL type, replacing any previous type information.
206    ///
207    /// This replaces the internal type representation with the provided `SqlType`.
208    pub fn set_sql_type(&mut self, sql_type: SqlType) {
209        self.sql_type_or_name = SqlTypeOrName::SqlType(sql_type);
210    }
211
212    /// Converts to the hyper-types `ColumnDefinition` (if `SqlType` is set).
213    #[must_use]
214    pub fn to_types_column_definition(&self) -> Option<TypesColumnDefinition> {
215        self.sql_type()
216            .map(|sql_type| TypesColumnDefinition::new(&self.name, sql_type, self.nullability()))
217    }
218}
219
220impl From<TypesColumnDefinition> for ColumnDefinition {
221    fn from(col: TypesColumnDefinition) -> Self {
222        ColumnDefinition {
223            name: col.name.clone(),
224            sql_type_or_name: SqlTypeOrName::SqlType(col.sql_type),
225            nullable: col.nullability.is_nullable(),
226            collation: None,
227        }
228    }
229}
230
231/// A table definition.
232///
233/// This struct defines the schema of a table including its name, optional schema
234/// and database names, and column definitions.
235///
236/// # Example
237///
238/// Using the fluent builder pattern:
239///
240/// ```
241/// use hyperdb_api::{TableDefinition, Result};
242/// use hyperdb_api_core::types::{SqlType, Nullability};
243///
244/// # fn main() -> Result<()> {
245/// let table = TableDefinition::new("users")
246///     .add_required_column("id", SqlType::int())
247///     .add_nullable_column("name", SqlType::text());
248///
249/// let sql = table.to_create_sql(true)?;
250/// assert!(sql.contains("CREATE TABLE"));
251/// # Ok(())
252/// # }
253/// ```
254#[derive(Debug, Clone)]
255#[must_use = "TableDefinition uses a consuming builder pattern - each method takes ownership and returns a new instance. You must use the returned value or your table definition changes will be lost"]
256pub struct TableDefinition {
257    /// Table name.
258    pub name: String,
259    /// Schema name.
260    pub schema: Option<String>,
261    /// Database name.
262    pub database: Option<String>,
263    /// Column definitions.
264    pub columns: Vec<ColumnDefinition>,
265    /// Table persistence (permanent or temporary).
266    persistence: Persistence,
267}
268
269impl Default for TableDefinition {
270    fn default() -> Self {
271        Self::new("unnamed_table")
272    }
273}
274
275impl From<&str> for TableDefinition {
276    fn from(name: &str) -> Self {
277        Self::new(name)
278    }
279}
280
281impl From<String> for TableDefinition {
282    fn from(name: String) -> Self {
283        Self::new(name)
284    }
285}
286
287impl TableDefinition {
288    /// Creates a new table definition.
289    pub fn new(name: impl Into<String>) -> Self {
290        TableDefinition {
291            name: name.into(),
292            schema: None,
293            database: None,
294            columns: Vec::new(),
295            persistence: Persistence::Permanent,
296        }
297    }
298
299    /// Creates a table definition from a validated `TableName`.
300    ///
301    /// This constructor uses a pre-validated `TableName`, ensuring all name components
302    /// have already passed validation (non-empty, within length limits).
303    ///
304    /// # Example
305    ///
306    /// ```
307    /// use hyperdb_api::{TableDefinition, TableName};
308    /// use hyperdb_api_core::types::{SqlType, Nullability};
309    ///
310    /// // First create a validated TableName
311    /// let table_name = TableName::try_new("users")?
312    ///     .with_schema("public")?
313    ///     .with_database("mydb")?;
314    ///
315    /// // Then create TableDefinition from it
316    /// let table = TableDefinition::from_table_name(table_name)?
317    ///     .add_required_column("id", SqlType::int());
318    ///
319    /// assert_eq!(table.name, "users");
320    /// assert_eq!(table.schema, Some("public".to_string()));
321    /// assert_eq!(table.database, Some("mydb".to_string()));
322    ///
323    /// // Direct conversion from string also works
324    /// let table2 = TableDefinition::from_table_name("public.users")?;
325    /// assert_eq!(table2.schema, Some("public".to_string()));
326    /// # Ok::<(), hyperdb_api::Error>(())
327    /// ```
328    ///
329    /// # Errors
330    ///
331    /// Returns the conversion error (typically [`Error::InvalidName`]) if
332    /// `table_name` cannot be parsed into a
333    /// [`TableName`](crate::TableName).
334    pub fn from_table_name<T>(table_name: T) -> Result<Self>
335    where
336        T: TryInto<crate::TableName>,
337        crate::Error: From<T::Error>,
338    {
339        let table_name = table_name.try_into()?;
340        Ok(TableDefinition {
341            name: table_name.table().unescaped().to_string(),
342            schema: table_name.schema().map(|s| s.unescaped().to_string()),
343            database: table_name.database().map(|d| d.unescaped().to_string()),
344            columns: Vec::new(),
345            persistence: Persistence::Permanent,
346        })
347    }
348
349    /// Sets the schema name (fluent builder pattern).
350    ///
351    /// # Example
352    ///
353    /// ```
354    /// use hyperdb_api::TableDefinition;
355    /// use hyperdb_api_core::types::{SqlType, Nullability};
356    ///
357    /// let table = TableDefinition::new("Extract")
358    ///     .with_schema("Extract")
359    ///     .add_required_column("id", SqlType::int());
360    /// ```
361    pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
362        self.schema = Some(schema.into());
363        self
364    }
365
366    /// Sets the database name (fluent builder pattern).
367    pub fn with_database(mut self, database: impl Into<String>) -> Self {
368        self.database = Some(database.into());
369        self
370    }
371
372    /// Sets the persistence (fluent builder pattern).
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use hyperdb_api::{TableDefinition, Persistence};
378    /// use hyperdb_api_core::types::SqlType;
379    ///
380    /// let temp_table = TableDefinition::new("temp_data")
381    ///     .with_persistence(Persistence::Temporary)
382    ///     .add_required_column("id", SqlType::int());
383    /// assert_eq!(temp_table.get_persistence(), Persistence::Temporary);
384    /// ```
385    pub fn with_persistence(mut self, persistence: Persistence) -> Self {
386        self.persistence = persistence;
387        self
388    }
389
390    /// Returns the persistence setting.
391    #[must_use]
392    pub fn get_persistence(&self) -> Persistence {
393        self.persistence
394    }
395
396    /// Sets the persistence.
397    pub fn set_persistence(&mut self, persistence: Persistence) {
398        self.persistence = persistence;
399    }
400
401    /// Adds a column to the table definition (fluent builder pattern).
402    ///
403    /// This is an internal method. Use `add_nullable_column()` or `add_required_column()` instead.
404    #[expect(
405        dead_code,
406        reason = "called from the `table!` declarative macro; not invoked by the crate itself"
407    )]
408    pub(crate) fn add_column(
409        mut self,
410        name: impl Into<String>,
411        sql_type: SqlType,
412        nullability: Nullability,
413    ) -> Self {
414        self.columns
415            .push(ColumnDefinition::with_sql_type(name, sql_type, nullability));
416        self
417    }
418
419    /// Adds a nullable column using `SqlType` (fluent builder pattern).
420    ///
421    /// # Example
422    ///
423    /// ```
424    /// use hyperdb_api::TableDefinition;
425    /// use hyperdb_api_core::types::SqlType;
426    ///
427    /// let table = TableDefinition::new("products")
428    ///     .add_nullable_column("name", SqlType::text())
429    ///     .add_nullable_column("price", SqlType::numeric(18, 2));
430    /// ```
431    pub fn add_nullable_column(mut self, name: impl Into<String>, sql_type: SqlType) -> Self {
432        self.columns.push(ColumnDefinition::with_sql_type(
433            name,
434            sql_type,
435            Nullability::Nullable,
436        ));
437        self
438    }
439
440    /// Adds a required (non-nullable) column using `SqlType` (fluent builder pattern).
441    ///
442    /// # Example
443    ///
444    /// ```
445    /// use hyperdb_api::TableDefinition;
446    /// use hyperdb_api_core::types::SqlType;
447    ///
448    /// let table = TableDefinition::new("products")
449    ///     .add_required_column("id", SqlType::int())
450    ///     .add_required_column("name", SqlType::text());
451    /// ```
452    pub fn add_required_column(mut self, name: impl Into<String>, sql_type: SqlType) -> Self {
453        self.columns.push(ColumnDefinition::with_sql_type(
454            name,
455            sql_type,
456            Nullability::NotNullable,
457        ));
458        self
459    }
460
461    /// Adds a column with a collation (fluent builder pattern).
462    ///
463    /// This is an internal method. Use `add_nullable_column_with_collation()` or `add_required_column_with_collation()` instead.
464    #[expect(
465        dead_code,
466        reason = "called from the `table!` declarative macro; not invoked by the crate itself"
467    )]
468    pub(crate) fn add_column_with_collation(
469        mut self,
470        name: impl Into<String>,
471        sql_type: SqlType,
472        collation: impl Into<String>,
473        nullability: Nullability,
474    ) -> Self {
475        self.columns.push(ColumnDefinition::with_collation(
476            name,
477            sql_type,
478            collation,
479            nullability,
480        ));
481        self
482    }
483
484    /// Adds a nullable column with a collation (fluent builder pattern).
485    ///
486    /// # Example
487    ///
488    /// ```
489    /// use hyperdb_api::TableDefinition;
490    /// use hyperdb_api_core::types::SqlType;
491    ///
492    /// let table = TableDefinition::new("products")
493    ///     .add_nullable_column_with_collation("name", SqlType::text(), "en_US");
494    /// ```
495    pub fn add_nullable_column_with_collation(
496        mut self,
497        name: impl Into<String>,
498        sql_type: SqlType,
499        collation: impl Into<String>,
500    ) -> Self {
501        self.columns.push(ColumnDefinition::with_collation(
502            name,
503            sql_type,
504            collation,
505            Nullability::Nullable,
506        ));
507        self
508    }
509
510    /// Adds a required (non-nullable) column with a collation (fluent builder pattern).
511    ///
512    /// # Example
513    ///
514    /// ```
515    /// use hyperdb_api::TableDefinition;
516    /// use hyperdb_api_core::types::SqlType;
517    ///
518    /// let table = TableDefinition::new("products")
519    ///     .add_required_column_with_collation("name", SqlType::text(), "en_US");
520    /// ```
521    pub fn add_required_column_with_collation(
522        mut self,
523        name: impl Into<String>,
524        sql_type: SqlType,
525        collation: impl Into<String>,
526    ) -> Self {
527        self.columns.push(ColumnDefinition::with_collation(
528            name,
529            sql_type,
530            collation,
531            Nullability::NotNullable,
532        ));
533        self
534    }
535
536    /// Adds a `ColumnDefinition` directly (fluent builder pattern).
537    pub fn add_column_def(mut self, column: ColumnDefinition) -> Self {
538        self.columns.push(column);
539        self
540    }
541
542    /// Adds a column with raw type string (internal use).
543    #[expect(
544        dead_code,
545        reason = "retained for catalog reflection paths that pass string type names"
546    )]
547    pub(crate) fn add_column_raw(&mut self, name: &str, type_name: &str, nullable: bool) {
548        // Map the Hyper type name to SqlType if possible
549        let sql_type_or_name = Self::type_name_to_sql_type(type_name).map_or_else(
550            || SqlTypeOrName::TypeName(type_name.to_string()),
551            SqlTypeOrName::SqlType,
552        );
553
554        self.columns.push(ColumnDefinition {
555            name: name.to_string(),
556            sql_type_or_name,
557            nullable,
558            collation: None,
559        });
560    }
561
562    /// Adds a column with a pre-constructed `SqlType` (internal use).
563    ///
564    /// This is used by the catalog when it has OID and type modifier information
565    /// to construct the proper `SqlType` with precision/scale.
566    pub(crate) fn add_column_with_sql_type(
567        &mut self,
568        name: &str,
569        sql_type: SqlType,
570        nullable: bool,
571    ) {
572        self.columns.push(ColumnDefinition {
573            name: name.to_string(),
574            sql_type_or_name: SqlTypeOrName::SqlType(sql_type),
575            nullable,
576            collation: None,
577        });
578    }
579
580    /// Maps a Hyper type name from `pg_type` to `SqlType`.
581    #[allow(
582        dead_code,
583        reason = "helper used only by `add_column_raw`, which is itself gated on macro use"
584    )]
585    fn type_name_to_sql_type(type_name: &str) -> Option<SqlType> {
586        // Hyper uses PostgreSQL-style type names
587        match type_name.to_lowercase().as_str() {
588            "integer" | "int4" | "int" => Some(SqlType::int()),
589            "smallint" | "int2" => Some(SqlType::small_int()),
590            "bigint" | "int8" => Some(SqlType::big_int()),
591            "double precision" | "float8" => Some(SqlType::double()),
592            "real" | "float4" => Some(SqlType::float()),
593            "text" => Some(SqlType::text()),
594            "boolean" | "bool" => Some(SqlType::bool()),
595            "date" => Some(SqlType::date()),
596            "time" | "time without time zone" => Some(SqlType::time()),
597            "timestamp" | "timestamp without time zone" => Some(SqlType::timestamp()),
598            "timestamptz" | "timestamp with time zone" => Some(SqlType::timestamp_tz()),
599            "bytea" => Some(SqlType::bytes()),
600            "numeric" => Some(SqlType::numeric(38, 0)), // Default precision/scale
601            "json" => Some(SqlType::json()),
602            "geography" => Some(SqlType::tabgeography()),
603            s if s.starts_with("varchar") || s.starts_with("character varying") => {
604                Some(SqlType::varchar(Some(1000))) // Default max length
605            }
606            s if s.starts_with("char") || s.starts_with("character") => {
607                Some(SqlType::char(1)) // Default length
608            }
609            _ => None,
610        }
611    }
612
613    /// Returns the number of columns.
614    #[must_use]
615    pub fn column_count(&self) -> usize {
616        self.columns.len()
617    }
618
619    /// Returns the column definitions.
620    #[must_use]
621    pub fn columns(&self) -> &[ColumnDefinition] {
622        &self.columns
623    }
624
625    /// Returns the column at the given position.
626    ///
627    /// # Panics
628    ///
629    /// Panics if the index is out of bounds.
630    #[must_use]
631    pub fn column(&self, index: usize) -> &ColumnDefinition {
632        &self.columns[index]
633    }
634
635    /// Returns the column with the given name, if it exists.
636    #[must_use]
637    pub fn column_by_name(&self, name: &str) -> Option<&ColumnDefinition> {
638        self.columns.iter().find(|c| c.name == name)
639    }
640
641    /// Returns the position of the column with the given name.
642    #[must_use]
643    pub fn column_position_by_name(&self, name: &str) -> Option<usize> {
644        self.columns.iter().position(|c| c.name == name)
645    }
646
647    /// Returns the table name (unqualified, escaped).
648    ///
649    /// This returns just the table name portion, properly escaped for use in SQL.
650    ///
651    /// # Example
652    ///
653    /// ```
654    /// use hyperdb_api::TableDefinition;
655    ///
656    /// let table = TableDefinition::new("Extract").with_schema("Extract");
657    /// // "Extract" is quoted because it contains uppercase letters (to preserve case)
658    /// assert_eq!(table.table_name(), "\"Extract\"");
659    /// ```
660    #[must_use]
661    pub fn table_name(&self) -> String {
662        format!("{}", SqlIdentifier(&self.name))
663    }
664
665    /// Returns the schema name (escaped), if set.
666    #[must_use]
667    pub fn schema_name(&self) -> Option<String> {
668        self.schema
669            .as_ref()
670            .map(|s| format!("{}", SqlIdentifier(s)))
671    }
672
673    /// Returns the database name (escaped), if set.
674    #[must_use]
675    pub fn database_name(&self) -> Option<String> {
676        self.database
677            .as_ref()
678            .map(|s| format!("{}", SqlIdentifier(s)))
679    }
680
681    /// Returns the qualified table name (escaped).
682    ///
683    /// Format: `database.schema.table` (if all parts are set, unquoted if valid identifiers)
684    #[must_use]
685    pub fn qualified_name(&self) -> String {
686        match (&self.database, &self.schema) {
687            (Some(db), Some(schema)) => format!(
688                "{}.{}.{}",
689                SqlIdentifier(db),
690                SqlIdentifier(schema),
691                SqlIdentifier(&self.name)
692            ),
693            (None, Some(schema)) => {
694                format!("{}.{}", SqlIdentifier(schema), SqlIdentifier(&self.name))
695            }
696            (Some(db), None) => format!("{}.{}", SqlIdentifier(db), SqlIdentifier(&self.name)),
697            (None, None) => format!("{}", SqlIdentifier(&self.name)),
698        }
699    }
700
701    /// Sets the table name.
702    pub fn set_table_name(&mut self, name: impl Into<String>) {
703        self.name = name.into();
704    }
705
706    /// Converts this `TableDefinition` to a validated `TableName`.
707    ///
708    /// This method validates all name components (table, schema, database) and returns
709    /// a type-safe `TableName`. Use this when you need to ensure the names are valid.
710    ///
711    /// # Errors
712    ///
713    /// Returns an error if any name component is empty or exceeds the `PostgreSQL` identifier limit.
714    ///
715    /// # Example
716    ///
717    /// ```
718    /// use hyperdb_api::TableDefinition;
719    ///
720    /// let table = TableDefinition::new("users")
721    ///     .with_schema("public")
722    ///     .with_database("mydb");
723    ///
724    /// // Validate all names
725    /// let table_name = table.to_table_name()?;
726    /// assert_eq!(table_name.to_string(), "\"mydb\".\"public\".\"users\"");
727    /// # Ok::<(), hyperdb_api::Error>(())
728    /// ```
729    pub fn to_table_name(&self) -> Result<crate::TableName> {
730        let mut table = crate::TableName::try_new(&self.name)?;
731        if let Some(ref schema) = self.schema {
732            table = table.with_schema(schema)?;
733        }
734        if let Some(ref database) = self.database {
735            table = table.with_database(database)?;
736        }
737        Ok(table)
738    }
739
740    /// Generates CREATE TABLE SQL.
741    ///
742    /// # Arguments
743    ///
744    /// * `fail_if_exists` - If true, the statement will fail if the table exists.
745    ///   If false, uses CREATE TABLE IF NOT EXISTS.
746    ///
747    /// # Example
748    ///
749    /// ```
750    /// use hyperdb_api::{TableDefinition, Result};
751    /// use hyperdb_api_core::types::{SqlType, Nullability};
752    ///
753    /// # fn main() -> Result<()> {
754    /// let table = TableDefinition::new("users")
755    ///     .add_required_column("id", SqlType::int());
756    ///
757    /// let sql = table.to_create_sql(true)?;
758    /// assert_eq!(sql, r#"CREATE TABLE users (id INTEGER NOT NULL)"#);
759    /// # Ok(())
760    /// # }
761    /// ```
762    ///
763    /// # Errors
764    ///
765    /// Returns [`Error::Other`] with message
766    /// `"Table must have at least one column"` if this definition has no
767    /// columns.
768    pub fn to_create_sql(&self, fail_if_exists: bool) -> Result<String> {
769        if self.columns.is_empty() {
770            return Err(Error::new("Table must have at least one column"));
771        }
772
773        let mut sql = String::new();
774
775        // Handle temporary tables
776        let create_keyword = match self.persistence {
777            Persistence::Permanent => {
778                if fail_if_exists {
779                    "CREATE TABLE "
780                } else {
781                    "CREATE TABLE IF NOT EXISTS "
782                }
783            }
784            Persistence::Temporary => {
785                if fail_if_exists {
786                    "CREATE TEMPORARY TABLE "
787                } else {
788                    "CREATE TEMPORARY TABLE IF NOT EXISTS "
789                }
790            }
791        };
792
793        sql.push_str(create_keyword);
794        sql.push_str(&self.qualified_name());
795        sql.push_str(" (");
796
797        for (i, col) in self.columns.iter().enumerate() {
798            if i > 0 {
799                sql.push_str(", ");
800            }
801
802            // Always quote column names in CREATE TABLE to preserve case
803            // (PostgreSQL/Hyper case-folds unquoted identifiers to lowercase)
804            // Note: write! to String is infallible, so we can ignore the Result
805            let _ = write!(sql, "{} {}", SqlIdentifier(&col.name), col.type_name());
806
807            // Add collation if specified
808            if let Some(collation) = &col.collation {
809                let _ = write!(sql, " COLLATE {}", SqlIdentifier(collation));
810            }
811
812            if !col.nullable {
813                sql.push_str(" NOT NULL");
814            }
815        }
816
817        sql.push(')');
818
819        Ok(sql)
820    }
821
822    /// Generates DROP TABLE SQL.
823    ///
824    /// # Arguments
825    ///
826    /// * `fail_if_not_exists` - If true, the statement will fail if the table doesn't exist.
827    ///   If false, uses DROP TABLE IF EXISTS.
828    #[must_use]
829    pub fn to_drop_sql(&self, fail_if_not_exists: bool) -> String {
830        let mut sql = String::new();
831
832        if fail_if_not_exists {
833            sql.push_str("DROP TABLE ");
834        } else {
835            sql.push_str("DROP TABLE IF EXISTS ");
836        }
837
838        sql.push_str(&self.qualified_name());
839        sql
840    }
841}
842
843use hyperdb_api_core::protocol::escape::SqlIdentifier;
844use std::fmt::Write;
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849
850    #[test]
851    fn test_create_sql() {
852        let table = TableDefinition::new("users")
853            .add_required_column("id", SqlType::int())
854            .add_nullable_column("name", SqlType::text());
855
856        let sql = table.to_create_sql(true).unwrap();
857        assert_eq!(sql, r"CREATE TABLE users (id INTEGER NOT NULL, name TEXT)");
858
859        // Verify type_name accessor works
860        assert_eq!(table.columns[0].type_name(), "INTEGER");
861        assert_eq!(table.columns[1].type_name(), "TEXT");
862    }
863
864    #[test]
865    fn test_create_sql_with_numeric() {
866        let table = TableDefinition::new("products")
867            .add_required_column("id", SqlType::int())
868            .add_nullable_column("name", SqlType::text())
869            .add_nullable_column("price", SqlType::numeric(18, 2));
870
871        let sql = table.to_create_sql(true).unwrap();
872        assert_eq!(
873            sql,
874            r"CREATE TABLE products (id INTEGER NOT NULL, name TEXT, price NUMERIC(18, 2))"
875        );
876    }
877
878    #[test]
879    fn test_qualified_name() {
880        let table = TableDefinition::new("users")
881            .with_schema("public")
882            .with_database("mydb");
883        assert_eq!(table.qualified_name(), r"mydb.public.users");
884    }
885
886    #[test]
887    fn test_table_name() {
888        let table = TableDefinition::new("Extract").with_schema("Extract");
889        // "Extract" is quoted because it contains uppercase letters (to preserve case)
890        assert_eq!(table.table_name(), r#""Extract""#);
891    }
892
893    #[test]
894    fn test_drop_sql() {
895        let table = TableDefinition::new("users");
896        assert_eq!(table.to_drop_sql(true), r"DROP TABLE users");
897        assert_eq!(table.to_drop_sql(false), r"DROP TABLE IF EXISTS users");
898    }
899
900    #[test]
901    fn test_column_definition_helpers() {
902        let not_null = ColumnDefinition::not_null("id", SqlType::int());
903        assert!(!not_null.nullable);
904
905        let nullable = ColumnDefinition::nullable("name", SqlType::text());
906        assert!(nullable.nullable);
907    }
908
909    #[test]
910    fn test_persistence() {
911        let perm = TableDefinition::new("data");
912        assert_eq!(perm.get_persistence(), Persistence::Permanent);
913
914        let temp = TableDefinition::new("temp_data").with_persistence(Persistence::Temporary);
915        assert_eq!(temp.get_persistence(), Persistence::Temporary);
916    }
917
918    #[test]
919    fn test_temporary_table_sql() {
920        let table = TableDefinition::new("temp_data")
921            .with_persistence(Persistence::Temporary)
922            .add_required_column("id", SqlType::int());
923
924        let sql = table.to_create_sql(true).unwrap();
925        assert_eq!(
926            sql,
927            r"CREATE TEMPORARY TABLE temp_data (id INTEGER NOT NULL)"
928        );
929    }
930
931    #[test]
932    fn test_collation() {
933        let col = ColumnDefinition::with_collation(
934            "name",
935            SqlType::text(),
936            "en_US",
937            Nullability::Nullable,
938        );
939        assert_eq!(col.collation(), Some("en_US"));
940    }
941
942    #[test]
943    fn test_column_with_collation_sql() {
944        let table = TableDefinition::new("users").add_nullable_column_with_collation(
945            "name",
946            SqlType::text(),
947            "en_US",
948        );
949
950        let sql = table.to_create_sql(true).unwrap();
951        // "en_US" is quoted because it contains uppercase letters (to preserve case)
952        assert!(sql.contains(r#"COLLATE "en_US""#));
953    }
954
955    #[test]
956    fn test_column_lookup() {
957        let table = TableDefinition::new("users")
958            .add_required_column("id", SqlType::int())
959            .add_nullable_column("name", SqlType::text());
960
961        assert!(table.column_by_name("id").is_some());
962        assert!(table.column_by_name("nonexistent").is_none());
963        assert_eq!(table.column_position_by_name("name"), Some(1));
964    }
965
966    #[test]
967    fn test_fluent_builder_pattern() {
968        // This test demonstrates the fluent builder pattern
969        let table = TableDefinition::new("Extract")
970            .with_schema("Extract")
971            .add_required_column("Customer ID", SqlType::text())
972            .add_required_column("Customer Name", SqlType::text())
973            .add_required_column("Loyalty Reward Points", SqlType::big_int())
974            .add_required_column("Segment", SqlType::text());
975
976        assert_eq!(table.column_count(), 4);
977        assert_eq!(table.schema, Some("Extract".to_string()));
978        assert_eq!(table.name, "Extract");
979    }
980}