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 table-level constraint.
67///
68/// Hyper accepts only the *assumed* constraint forms on `CREATE TABLE`. Real
69/// `PRIMARY KEY`, `UNIQUE`, and `FOREIGN KEY` clauses are rejected with
70/// `Index support is disabled`, and `CHECK` with `check constraints not
71/// implemented yet`, so they can never appear on a Hyper table in the first
72/// place. Assumed constraints are declarations the optimizer trusts but the
73/// engine does **not** enforce — inserting a duplicate key succeeds.
74///
75/// Hyper also rejects `CONSTRAINT <name> …` (`named constraints not
76/// implemented yet`), so constraints carry no user-chosen name; the engine
77/// derives one from the table and column names.
78#[derive(Debug, Clone, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum TableConstraint {
81    /// `ASSUMED PRIMARY KEY (cols…)`. Every listed column must be `NOT NULL`.
82    AssumedPrimaryKey {
83        /// Key columns, in declaration order.
84        columns: Vec<String>,
85    },
86    /// `ASSUMED UNIQUE (cols…)`.
87    AssumedUnique {
88        /// Key columns, in declaration order.
89        columns: Vec<String>,
90    },
91}
92
93impl TableConstraint {
94    /// Returns the columns the constraint covers, in declaration order.
95    #[must_use]
96    pub fn columns(&self) -> &[String] {
97        match self {
98            Self::AssumedPrimaryKey { columns } | Self::AssumedUnique { columns } => columns,
99        }
100    }
101
102    /// Renders the constraint as the SQL clause used inside `CREATE TABLE`.
103    fn to_sql(&self) -> String {
104        let keyword = match self {
105            Self::AssumedPrimaryKey { .. } => "ASSUMED PRIMARY KEY",
106            Self::AssumedUnique { .. } => "ASSUMED UNIQUE",
107        };
108        // Quoted unconditionally, for the same reason as the column list in
109        // `to_create_sql`: a constrained column may be a reserved word.
110        let cols = self
111            .columns()
112            .iter()
113            .map(|c| QuotedIdentifier(c).to_string())
114            .collect::<Vec<_>>()
115            .join(", ");
116        format!("{keyword} ({cols})")
117    }
118}
119
120/// A column definition.
121///
122/// This struct supports both string-based type names (for simplicity) and
123/// SqlType-based definitions (for type safety). Internally, it uses a single source
124/// of truth to avoid synchronization issues.
125#[derive(Debug, Clone)]
126pub struct ColumnDefinition {
127    /// Column name.
128    pub name: String,
129    /// SQL type representation (either structured `SqlType` or raw string).
130    sql_type_or_name: SqlTypeOrName,
131    /// Whether the column is nullable.
132    pub nullable: bool,
133    /// The collation for text columns (e.g., "`en_US`", "binary").
134    collation: Option<String>,
135    /// The `DEFAULT` expression, as SQL source text.
136    default_expr: Option<String>,
137}
138
139impl ColumnDefinition {
140    /// Creates a new column definition using a type name string.
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use hyperdb_api::ColumnDefinition;
146    ///
147    /// let col = ColumnDefinition::new("id", "INT", false);
148    /// assert_eq!(col.name, "id");
149    /// assert_eq!(col.type_name(), "INT");
150    /// ```
151    pub fn new(name: impl Into<String>, type_name: impl Into<String>, nullable: bool) -> Self {
152        ColumnDefinition {
153            name: name.into(),
154            sql_type_or_name: SqlTypeOrName::TypeName(type_name.into()),
155            nullable,
156            collation: None,
157            default_expr: None,
158        }
159    }
160
161    /// Creates a column definition using `SqlType`.
162    ///
163    /// # Example
164    ///
165    /// ```
166    /// use hyperdb_api::ColumnDefinition;
167    /// use hyperdb_api_core::types::{SqlType, Nullability};
168    ///
169    /// let col = ColumnDefinition::with_sql_type("id", SqlType::int(), Nullability::NotNullable);
170    /// assert_eq!(col.name, "id");
171    /// assert!(!col.nullable);
172    /// ```
173    pub fn with_sql_type(
174        name: impl Into<String>,
175        sql_type: SqlType,
176        nullability: Nullability,
177    ) -> Self {
178        ColumnDefinition {
179            name: name.into(),
180            sql_type_or_name: SqlTypeOrName::SqlType(sql_type),
181            nullable: nullability.is_nullable(),
182            collation: None,
183            default_expr: None,
184        }
185    }
186
187    /// Creates a column definition with a collation.
188    ///
189    /// The collation specifies the sorting and comparison behavior for text columns.
190    ///
191    /// # Example
192    ///
193    /// ```
194    /// use hyperdb_api::ColumnDefinition;
195    /// use hyperdb_api_core::types::{SqlType, Nullability};
196    ///
197    /// let col = ColumnDefinition::with_collation("name", SqlType::text(), "en_US", Nullability::Nullable);
198    /// assert_eq!(col.collation(), Some("en_US"));
199    /// ```
200    pub fn with_collation(
201        name: impl Into<String>,
202        sql_type: SqlType,
203        collation: impl Into<String>,
204        nullability: Nullability,
205    ) -> Self {
206        ColumnDefinition {
207            name: name.into(),
208            sql_type_or_name: SqlTypeOrName::SqlType(sql_type),
209            nullable: nullability.is_nullable(),
210            collation: Some(collation.into()),
211            default_expr: None,
212        }
213    }
214
215    /// Creates a nullable column definition using `SqlType`.
216    pub fn nullable(name: impl Into<String>, sql_type: SqlType) -> Self {
217        Self::with_sql_type(name, sql_type, Nullability::Nullable)
218    }
219
220    /// Creates a non-nullable column definition using `SqlType`.
221    pub fn not_null(name: impl Into<String>, sql_type: SqlType) -> Self {
222        Self::with_sql_type(name, sql_type, Nullability::NotNullable)
223    }
224
225    /// Returns the nullability as a Nullability enum.
226    #[must_use]
227    pub fn nullability(&self) -> Nullability {
228        if self.nullable {
229            Nullability::Nullable
230        } else {
231            Nullability::NotNullable
232        }
233    }
234
235    /// Returns the SQL type if this column was created with a structured type.
236    #[must_use]
237    pub fn sql_type(&self) -> Option<SqlType> {
238        self.sql_type_or_name.sql_type()
239    }
240
241    /// Returns the type name string representation.
242    ///
243    /// When created with `SqlType`, this is derived from it. Otherwise, it's the
244    /// string provided during construction.
245    ///
246    /// Returns `Cow<str>` to avoid allocation when the type name is already stored
247    /// as a string internally.
248    #[must_use]
249    pub fn type_name(&self) -> Cow<'_, str> {
250        self.sql_type_or_name.type_name()
251    }
252
253    /// Returns the collation if set.
254    #[must_use]
255    pub fn collation(&self) -> Option<&str> {
256        self.collation.as_deref()
257    }
258
259    /// Returns the `DEFAULT` expression, as SQL source text, if the column has one.
260    ///
261    /// The text is a SQL expression (`42`, `'n/a'`, `NOW()`), not a value — it
262    /// is rendered verbatim into `CREATE TABLE`.
263    #[must_use]
264    pub fn default_expr(&self) -> Option<&str> {
265        self.default_expr.as_deref()
266    }
267
268    /// Sets the `DEFAULT` expression from SQL source text.
269    ///
270    /// The expression is emitted verbatim, so it must be valid SQL in the
271    /// database the table is created in. See [`TableDefinition::to_create_sql`].
272    pub fn set_default_expr(&mut self, expr: impl Into<String>) {
273        self.default_expr = Some(expr.into());
274    }
275
276    /// Removes the `DEFAULT` expression.
277    pub fn clear_default_expr(&mut self) {
278        self.default_expr = None;
279    }
280
281    /// Sets the collation for this column.
282    pub fn set_collation(&mut self, collation: impl Into<String>) {
283        self.collation = Some(collation.into());
284    }
285
286    /// Sets the SQL type, replacing any previous type information.
287    ///
288    /// This replaces the internal type representation with the provided `SqlType`.
289    pub fn set_sql_type(&mut self, sql_type: SqlType) {
290        self.sql_type_or_name = SqlTypeOrName::SqlType(sql_type);
291    }
292
293    /// Converts to the hyper-types `ColumnDefinition` (if `SqlType` is set).
294    #[must_use]
295    pub fn to_types_column_definition(&self) -> Option<TypesColumnDefinition> {
296        self.sql_type()
297            .map(|sql_type| TypesColumnDefinition::new(&self.name, sql_type, self.nullability()))
298    }
299}
300
301impl From<TypesColumnDefinition> for ColumnDefinition {
302    fn from(col: TypesColumnDefinition) -> Self {
303        ColumnDefinition {
304            name: col.name.clone(),
305            sql_type_or_name: SqlTypeOrName::SqlType(col.sql_type),
306            nullable: col.nullability.is_nullable(),
307            collation: None,
308            default_expr: None,
309        }
310    }
311}
312
313/// A table definition.
314///
315/// This struct defines the schema of a table including its name, optional schema
316/// and database names, and column definitions.
317///
318/// # Example
319///
320/// Using the fluent builder pattern:
321///
322/// ```
323/// use hyperdb_api::{TableDefinition, Result};
324/// use hyperdb_api_core::types::{SqlType, Nullability};
325///
326/// # fn main() -> Result<()> {
327/// let table = TableDefinition::new("users")
328///     .add_required_column("id", SqlType::int())
329///     .add_nullable_column("name", SqlType::text());
330///
331/// let sql = table.to_create_sql(true)?;
332/// assert!(sql.contains("CREATE TABLE"));
333/// # Ok(())
334/// # }
335/// ```
336#[derive(Debug, Clone)]
337#[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"]
338pub struct TableDefinition {
339    /// Table name.
340    pub name: String,
341    /// Schema name.
342    pub schema: Option<String>,
343    /// Database name.
344    pub database: Option<String>,
345    /// Column definitions.
346    pub columns: Vec<ColumnDefinition>,
347    /// Table persistence (permanent or temporary).
348    persistence: Persistence,
349    /// Table-level constraints.
350    constraints: Vec<TableConstraint>,
351}
352
353impl Default for TableDefinition {
354    fn default() -> Self {
355        Self::new("unnamed_table")
356    }
357}
358
359impl From<&str> for TableDefinition {
360    fn from(name: &str) -> Self {
361        Self::new(name)
362    }
363}
364
365impl From<String> for TableDefinition {
366    fn from(name: String) -> Self {
367        Self::new(name)
368    }
369}
370
371impl TableDefinition {
372    /// Creates a new table definition.
373    pub fn new(name: impl Into<String>) -> Self {
374        TableDefinition {
375            name: name.into(),
376            schema: None,
377            database: None,
378            columns: Vec::new(),
379            persistence: Persistence::Permanent,
380            constraints: Vec::new(),
381        }
382    }
383
384    /// Creates a table definition from a validated `TableName`.
385    ///
386    /// This constructor uses a pre-validated `TableName`, ensuring all name components
387    /// have already passed validation (non-empty, within length limits).
388    ///
389    /// # Example
390    ///
391    /// ```
392    /// use hyperdb_api::{TableDefinition, TableName};
393    /// use hyperdb_api_core::types::{SqlType, Nullability};
394    ///
395    /// // First create a validated TableName
396    /// let table_name = TableName::try_new("users")?
397    ///     .with_schema("public")?
398    ///     .with_database("mydb")?;
399    ///
400    /// // Then create TableDefinition from it
401    /// let table = TableDefinition::from_table_name(table_name)?
402    ///     .add_required_column("id", SqlType::int());
403    ///
404    /// assert_eq!(table.name, "users");
405    /// assert_eq!(table.schema, Some("public".to_string()));
406    /// assert_eq!(table.database, Some("mydb".to_string()));
407    ///
408    /// // Direct conversion from string also works
409    /// let table2 = TableDefinition::from_table_name("public.users")?;
410    /// assert_eq!(table2.schema, Some("public".to_string()));
411    /// # Ok::<(), hyperdb_api::Error>(())
412    /// ```
413    ///
414    /// # Errors
415    ///
416    /// Returns the conversion error (typically [`Error::InvalidName`]) if
417    /// `table_name` cannot be parsed into a
418    /// [`TableName`](crate::TableName).
419    pub fn from_table_name<T>(table_name: T) -> Result<Self>
420    where
421        T: TryInto<crate::TableName>,
422        crate::Error: From<T::Error>,
423    {
424        let table_name = table_name.try_into()?;
425        Ok(TableDefinition {
426            name: table_name.table().unescaped().to_string(),
427            schema: table_name.schema().map(|s| s.unescaped().to_string()),
428            database: table_name.database().map(|d| d.unescaped().to_string()),
429            columns: Vec::new(),
430            persistence: Persistence::Permanent,
431            constraints: Vec::new(),
432        })
433    }
434
435    /// Sets the schema name (fluent builder pattern).
436    ///
437    /// # Example
438    ///
439    /// ```
440    /// use hyperdb_api::TableDefinition;
441    /// use hyperdb_api_core::types::{SqlType, Nullability};
442    ///
443    /// let table = TableDefinition::new("Extract")
444    ///     .with_schema("Extract")
445    ///     .add_required_column("id", SqlType::int());
446    /// ```
447    pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
448        self.schema = Some(schema.into());
449        self
450    }
451
452    /// Sets the database name (fluent builder pattern).
453    pub fn with_database(mut self, database: impl Into<String>) -> Self {
454        self.database = Some(database.into());
455        self
456    }
457
458    /// Sets the persistence (fluent builder pattern).
459    ///
460    /// # Example
461    ///
462    /// ```
463    /// use hyperdb_api::{TableDefinition, Persistence};
464    /// use hyperdb_api_core::types::SqlType;
465    ///
466    /// let temp_table = TableDefinition::new("temp_data")
467    ///     .with_persistence(Persistence::Temporary)
468    ///     .add_required_column("id", SqlType::int());
469    /// assert_eq!(temp_table.get_persistence(), Persistence::Temporary);
470    /// ```
471    pub fn with_persistence(mut self, persistence: Persistence) -> Self {
472        self.persistence = persistence;
473        self
474    }
475
476    /// Returns the persistence setting.
477    #[must_use]
478    pub fn get_persistence(&self) -> Persistence {
479        self.persistence
480    }
481
482    /// Sets the persistence.
483    pub fn set_persistence(&mut self, persistence: Persistence) {
484        self.persistence = persistence;
485    }
486
487    /// Adds a column to the table definition (fluent builder pattern).
488    ///
489    /// This is an internal method. Use `add_nullable_column()` or `add_required_column()` instead.
490    #[expect(
491        dead_code,
492        reason = "called from the `table!` declarative macro; not invoked by the crate itself"
493    )]
494    pub(crate) fn add_column(
495        mut self,
496        name: impl Into<String>,
497        sql_type: SqlType,
498        nullability: Nullability,
499    ) -> Self {
500        self.columns
501            .push(ColumnDefinition::with_sql_type(name, sql_type, nullability));
502        self
503    }
504
505    /// Adds a nullable column using `SqlType` (fluent builder pattern).
506    ///
507    /// # Example
508    ///
509    /// ```
510    /// use hyperdb_api::TableDefinition;
511    /// use hyperdb_api_core::types::SqlType;
512    ///
513    /// let table = TableDefinition::new("products")
514    ///     .add_nullable_column("name", SqlType::text())
515    ///     .add_nullable_column("price", SqlType::numeric(18, 2));
516    /// ```
517    pub fn add_nullable_column(mut self, name: impl Into<String>, sql_type: SqlType) -> Self {
518        self.columns.push(ColumnDefinition::with_sql_type(
519            name,
520            sql_type,
521            Nullability::Nullable,
522        ));
523        self
524    }
525
526    /// Adds a required (non-nullable) column using `SqlType` (fluent builder pattern).
527    ///
528    /// # Example
529    ///
530    /// ```
531    /// use hyperdb_api::TableDefinition;
532    /// use hyperdb_api_core::types::SqlType;
533    ///
534    /// let table = TableDefinition::new("products")
535    ///     .add_required_column("id", SqlType::int())
536    ///     .add_required_column("name", SqlType::text());
537    /// ```
538    pub fn add_required_column(mut self, name: impl Into<String>, sql_type: SqlType) -> Self {
539        self.columns.push(ColumnDefinition::with_sql_type(
540            name,
541            sql_type,
542            Nullability::NotNullable,
543        ));
544        self
545    }
546
547    /// Adds a column with a collation (fluent builder pattern).
548    ///
549    /// This is an internal method. Use `add_nullable_column_with_collation()` or `add_required_column_with_collation()` instead.
550    #[expect(
551        dead_code,
552        reason = "called from the `table!` declarative macro; not invoked by the crate itself"
553    )]
554    pub(crate) fn add_column_with_collation(
555        mut self,
556        name: impl Into<String>,
557        sql_type: SqlType,
558        collation: impl Into<String>,
559        nullability: Nullability,
560    ) -> Self {
561        self.columns.push(ColumnDefinition::with_collation(
562            name,
563            sql_type,
564            collation,
565            nullability,
566        ));
567        self
568    }
569
570    /// Adds a nullable column with a collation (fluent builder pattern).
571    ///
572    /// # Example
573    ///
574    /// ```
575    /// use hyperdb_api::TableDefinition;
576    /// use hyperdb_api_core::types::SqlType;
577    ///
578    /// let table = TableDefinition::new("products")
579    ///     .add_nullable_column_with_collation("name", SqlType::text(), "en_US");
580    /// ```
581    pub fn add_nullable_column_with_collation(
582        mut self,
583        name: impl Into<String>,
584        sql_type: SqlType,
585        collation: impl Into<String>,
586    ) -> Self {
587        self.columns.push(ColumnDefinition::with_collation(
588            name,
589            sql_type,
590            collation,
591            Nullability::Nullable,
592        ));
593        self
594    }
595
596    /// Adds a required (non-nullable) column with a collation (fluent builder pattern).
597    ///
598    /// # Example
599    ///
600    /// ```
601    /// use hyperdb_api::TableDefinition;
602    /// use hyperdb_api_core::types::SqlType;
603    ///
604    /// let table = TableDefinition::new("products")
605    ///     .add_required_column_with_collation("name", SqlType::text(), "en_US");
606    /// ```
607    pub fn add_required_column_with_collation(
608        mut self,
609        name: impl Into<String>,
610        sql_type: SqlType,
611        collation: impl Into<String>,
612    ) -> Self {
613        self.columns.push(ColumnDefinition::with_collation(
614            name,
615            sql_type,
616            collation,
617            Nullability::NotNullable,
618        ));
619        self
620    }
621
622    /// Adds a `ColumnDefinition` directly (fluent builder pattern).
623    pub fn add_column_def(mut self, column: ColumnDefinition) -> Self {
624        self.columns.push(column);
625        self
626    }
627
628    /// Adds a column with raw type string (internal use).
629    #[expect(
630        dead_code,
631        reason = "retained for catalog reflection paths that pass string type names"
632    )]
633    pub(crate) fn add_column_raw(&mut self, name: &str, type_name: &str, nullable: bool) {
634        // Map the Hyper type name to SqlType if possible
635        let sql_type_or_name = Self::type_name_to_sql_type(type_name).map_or_else(
636            || SqlTypeOrName::TypeName(type_name.to_string()),
637            SqlTypeOrName::SqlType,
638        );
639
640        self.columns.push(ColumnDefinition {
641            name: name.to_string(),
642            sql_type_or_name,
643            nullable,
644            collation: None,
645            default_expr: None,
646        });
647    }
648
649    /// Adds a column with a pre-constructed `SqlType` (internal use).
650    ///
651    /// This is used by the catalog when it has OID and type modifier information
652    /// to construct the proper `SqlType` with precision/scale.
653    pub(crate) fn add_column_with_sql_type(
654        &mut self,
655        name: &str,
656        sql_type: SqlType,
657        nullable: bool,
658    ) {
659        self.columns.push(ColumnDefinition {
660            name: name.to_string(),
661            sql_type_or_name: SqlTypeOrName::SqlType(sql_type),
662            nullable,
663            collation: None,
664            default_expr: None,
665        });
666    }
667
668    /// Maps a Hyper type name from `pg_type` to `SqlType`.
669    #[allow(
670        dead_code,
671        reason = "helper used only by `add_column_raw`, which is itself gated on macro use"
672    )]
673    fn type_name_to_sql_type(type_name: &str) -> Option<SqlType> {
674        // Hyper uses PostgreSQL-style type names
675        match type_name.to_lowercase().as_str() {
676            "integer" | "int4" | "int" => Some(SqlType::int()),
677            "smallint" | "int2" => Some(SqlType::small_int()),
678            "bigint" | "int8" => Some(SqlType::big_int()),
679            "double precision" | "float8" => Some(SqlType::double()),
680            "real" | "float4" => Some(SqlType::float()),
681            "text" => Some(SqlType::text()),
682            "boolean" | "bool" => Some(SqlType::bool()),
683            "date" => Some(SqlType::date()),
684            "time" | "time without time zone" => Some(SqlType::time()),
685            "timestamp" | "timestamp without time zone" => Some(SqlType::timestamp()),
686            "timestamptz" | "timestamp with time zone" => Some(SqlType::timestamp_tz()),
687            "bytea" => Some(SqlType::bytes()),
688            "numeric" => Some(SqlType::numeric(38, 0)), // Default precision/scale
689            "json" => Some(SqlType::json()),
690            "geography" => Some(SqlType::tabgeography()),
691            s if s.starts_with("varchar") || s.starts_with("character varying") => {
692                Some(SqlType::varchar(Some(1000))) // Default max length
693            }
694            s if s.starts_with("char") || s.starts_with("character") => {
695                Some(SqlType::char(1)) // Default length
696            }
697            _ => None,
698        }
699    }
700
701    /// Returns the table-level constraints.
702    #[must_use]
703    pub fn constraints(&self) -> &[TableConstraint] {
704        &self.constraints
705    }
706
707    /// Adds a table-level constraint in place.
708    pub fn push_constraint(&mut self, constraint: TableConstraint) {
709        self.constraints.push(constraint);
710    }
711
712    /// Replaces the table-level constraints.
713    pub fn set_constraints(&mut self, constraints: Vec<TableConstraint>) {
714        self.constraints = constraints;
715    }
716
717    /// Returns the number of columns.
718    #[must_use]
719    pub fn column_count(&self) -> usize {
720        self.columns.len()
721    }
722
723    /// Returns the column definitions.
724    #[must_use]
725    pub fn columns(&self) -> &[ColumnDefinition] {
726        &self.columns
727    }
728
729    /// Returns the column at the given position.
730    ///
731    /// # Panics
732    ///
733    /// Panics if the index is out of bounds.
734    #[must_use]
735    pub fn column(&self, index: usize) -> &ColumnDefinition {
736        &self.columns[index]
737    }
738
739    /// Returns the column with the given name, if it exists.
740    #[must_use]
741    pub fn column_by_name(&self, name: &str) -> Option<&ColumnDefinition> {
742        self.columns.iter().find(|c| c.name == name)
743    }
744
745    /// Returns the position of the column with the given name.
746    #[must_use]
747    pub fn column_position_by_name(&self, name: &str) -> Option<usize> {
748        self.columns.iter().position(|c| c.name == name)
749    }
750
751    /// Returns the table name (unqualified, escaped).
752    ///
753    /// This returns just the table name portion, properly escaped for use in SQL.
754    ///
755    /// # Example
756    ///
757    /// ```
758    /// use hyperdb_api::TableDefinition;
759    ///
760    /// let table = TableDefinition::new("Extract").with_schema("Extract");
761    /// // "Extract" is quoted because it contains uppercase letters (to preserve case)
762    /// assert_eq!(table.table_name(), "\"Extract\"");
763    /// ```
764    #[must_use]
765    pub fn table_name(&self) -> String {
766        format!("{}", SqlIdentifier(&self.name))
767    }
768
769    /// Returns the schema name (escaped), if set.
770    #[must_use]
771    pub fn schema_name(&self) -> Option<String> {
772        self.schema
773            .as_ref()
774            .map(|s| format!("{}", SqlIdentifier(s)))
775    }
776
777    /// Returns the database name (escaped), if set.
778    #[must_use]
779    pub fn database_name(&self) -> Option<String> {
780        self.database
781            .as_ref()
782            .map(|s| format!("{}", SqlIdentifier(s)))
783    }
784
785    /// Returns the qualified table name with every part quoted.
786    ///
787    /// [`qualified_name`](Self::qualified_name) leaves a name bare when it is
788    /// already a legal unquoted identifier, which is not safe for generated
789    /// DDL: the underlying check does not know the reserved word list, so a
790    /// table reflected out of the catalog as `order` would be emitted bare and
791    /// rejected. Statements this type generates use this instead.
792    fn quoted_qualified_name(&self) -> String {
793        match (&self.database, &self.schema) {
794            (Some(db), Some(schema)) => format!(
795                "{}.{}.{}",
796                QuotedIdentifier(db),
797                QuotedIdentifier(schema),
798                QuotedIdentifier(&self.name)
799            ),
800            (None, Some(schema)) => format!(
801                "{}.{}",
802                QuotedIdentifier(schema),
803                QuotedIdentifier(&self.name)
804            ),
805            (Some(db), None) => {
806                format!("{}.{}", QuotedIdentifier(db), QuotedIdentifier(&self.name))
807            }
808            (None, None) => format!("{}", QuotedIdentifier(&self.name)),
809        }
810    }
811
812    /// Returns the qualified table name (escaped).
813    ///
814    /// Format: `database.schema.table` (if all parts are set, unquoted if valid identifiers)
815    #[must_use]
816    pub fn qualified_name(&self) -> String {
817        match (&self.database, &self.schema) {
818            (Some(db), Some(schema)) => format!(
819                "{}.{}.{}",
820                SqlIdentifier(db),
821                SqlIdentifier(schema),
822                SqlIdentifier(&self.name)
823            ),
824            (None, Some(schema)) => {
825                format!("{}.{}", SqlIdentifier(schema), SqlIdentifier(&self.name))
826            }
827            (Some(db), None) => format!("{}.{}", SqlIdentifier(db), SqlIdentifier(&self.name)),
828            (None, None) => format!("{}", SqlIdentifier(&self.name)),
829        }
830    }
831
832    /// Sets the table name.
833    pub fn set_table_name(&mut self, name: impl Into<String>) {
834        self.name = name.into();
835    }
836
837    /// Converts this `TableDefinition` to a validated `TableName`.
838    ///
839    /// This method validates all name components (table, schema, database) and returns
840    /// a type-safe `TableName`. Use this when you need to ensure the names are valid.
841    ///
842    /// # Errors
843    ///
844    /// Returns an error if any name component is empty or exceeds the `PostgreSQL` identifier limit.
845    ///
846    /// # Example
847    ///
848    /// ```
849    /// use hyperdb_api::TableDefinition;
850    ///
851    /// let table = TableDefinition::new("users")
852    ///     .with_schema("public")
853    ///     .with_database("mydb");
854    ///
855    /// // Validate all names
856    /// let table_name = table.to_table_name()?;
857    /// assert_eq!(table_name.to_string(), "\"mydb\".\"public\".\"users\"");
858    /// # Ok::<(), hyperdb_api::Error>(())
859    /// ```
860    pub fn to_table_name(&self) -> Result<crate::TableName> {
861        let mut table = crate::TableName::try_new(&self.name)?;
862        if let Some(ref schema) = self.schema {
863            table = table.with_schema(schema)?;
864        }
865        if let Some(ref database) = self.database {
866            table = table.with_database(database)?;
867        }
868        Ok(table)
869    }
870
871    /// Generates CREATE TABLE SQL.
872    ///
873    /// # Arguments
874    ///
875    /// * `fail_if_exists` - If true, the statement will fail if the table exists.
876    ///   If false, uses CREATE TABLE IF NOT EXISTS.
877    ///
878    /// # Example
879    ///
880    /// ```
881    /// use hyperdb_api::{TableDefinition, Result};
882    /// use hyperdb_api_core::types::{SqlType, Nullability};
883    ///
884    /// # fn main() -> Result<()> {
885    /// let table = TableDefinition::new("users")
886    ///     .add_required_column("id", SqlType::int());
887    ///
888    /// // Identifiers are quoted unconditionally, so that a name which happens
889    /// // to be a SQL reserved word is still emitted correctly.
890    /// let sql = table.to_create_sql(true)?;
891    /// assert_eq!(sql, r#"CREATE TABLE "users" ("id" INTEGER NOT NULL)"#);
892    /// # Ok(())
893    /// # }
894    /// ```
895    ///
896    /// # Errors
897    ///
898    /// Returns [`Error::InvalidTableDefinition`] with message
899    /// `"Table must have at least one column"` if this definition has no
900    /// columns.
901    pub fn to_create_sql(&self, fail_if_exists: bool) -> Result<String> {
902        if self.columns.is_empty() {
903            return Err(Error::invalid_table_definition(
904                "Table must have at least one column",
905            ));
906        }
907
908        let mut sql = String::new();
909
910        // Handle temporary tables
911        let create_keyword = match self.persistence {
912            Persistence::Permanent => {
913                if fail_if_exists {
914                    "CREATE TABLE "
915                } else {
916                    "CREATE TABLE IF NOT EXISTS "
917                }
918            }
919            Persistence::Temporary => {
920                if fail_if_exists {
921                    "CREATE TEMPORARY TABLE "
922                } else {
923                    "CREATE TEMPORARY TABLE IF NOT EXISTS "
924                }
925            }
926        };
927
928        sql.push_str(create_keyword);
929        sql.push_str(&self.quoted_qualified_name());
930        sql.push_str(" (");
931
932        for (i, col) in self.columns.iter().enumerate() {
933            if i > 0 {
934                sql.push_str(", ");
935            }
936
937            // Quote column names unconditionally. This preserves case
938            // (Hyper case-folds unquoted identifiers to lowercase) and, just
939            // as importantly, survives names that are reserved words:
940            // `is_valid_unquoted_identifier` does not know the keyword list,
941            // so `SqlIdentifier` would emit a lowercase `select` bare and the
942            // engine would reject the statement.
943            // Note: write! to String is infallible, so we can ignore the Result
944            let _ = write!(sql, "{} {}", QuotedIdentifier(&col.name), col.type_name());
945
946            // Add collation if specified. Also always quoted — collation names
947            // are case-sensitive (`en_US`) and the engine accepts the quoted
948            // form for every name in pg_collation.
949            if let Some(collation) = &col.collation {
950                let _ = write!(sql, " COLLATE {}", QuotedIdentifier(collation));
951            }
952
953            if !col.nullable {
954                sql.push_str(" NOT NULL");
955            }
956
957            if let Some(default_expr) = &col.default_expr {
958                let _ = write!(sql, " DEFAULT {default_expr}");
959            }
960        }
961
962        for constraint in &self.constraints {
963            let _ = write!(sql, ", {}", constraint.to_sql());
964        }
965
966        sql.push(')');
967
968        Ok(sql)
969    }
970
971    /// Generates DROP TABLE SQL.
972    ///
973    /// # Arguments
974    ///
975    /// * `fail_if_not_exists` - If true, the statement will fail if the table doesn't exist.
976    ///   If false, uses DROP TABLE IF EXISTS.
977    #[must_use]
978    pub fn to_drop_sql(&self, fail_if_not_exists: bool) -> String {
979        let mut sql = String::new();
980
981        if fail_if_not_exists {
982            sql.push_str("DROP TABLE ");
983        } else {
984            sql.push_str("DROP TABLE IF EXISTS ");
985        }
986
987        sql.push_str(&self.qualified_name());
988        sql
989    }
990}
991
992use hyperdb_api_core::protocol::escape::{QuotedIdentifier, SqlIdentifier};
993use std::fmt::Write;
994
995#[cfg(test)]
996mod tests {
997    use super::*;
998
999    #[test]
1000    fn test_create_sql() {
1001        let table = TableDefinition::new("users")
1002            .add_required_column("id", SqlType::int())
1003            .add_nullable_column("name", SqlType::text());
1004
1005        // Identifiers are quoted unconditionally: the bare-identifier check
1006        // does not know the reserved word list, so a name like `order` would
1007        // otherwise be emitted bare and rejected. Quoting a name that did not
1008        // need it means the same thing.
1009        let sql = table.to_create_sql(true).unwrap();
1010        assert_eq!(
1011            sql,
1012            r#"CREATE TABLE "users" ("id" INTEGER NOT NULL, "name" TEXT)"#
1013        );
1014
1015        // Verify type_name accessor works
1016        assert_eq!(table.columns[0].type_name(), "INTEGER");
1017        assert_eq!(table.columns[1].type_name(), "TEXT");
1018    }
1019
1020    #[test]
1021    fn create_sql_quotes_reserved_words() {
1022        // `is_valid_unquoted_identifier` has no reserved word list, so these
1023        // names look like legal bare identifiers. Emitting them bare produces
1024        // `syntax error: got SELECT`, which is how a whole-database copy used
1025        // to die on a table it had faithfully reflected.
1026        let table = TableDefinition::new("order")
1027            .add_required_column("select", SqlType::int())
1028            .add_nullable_column("from", SqlType::text());
1029
1030        assert_eq!(
1031            table.to_create_sql(true).unwrap(),
1032            r#"CREATE TABLE "order" ("select" INTEGER NOT NULL, "from" TEXT)"#
1033        );
1034    }
1035
1036    #[test]
1037    fn create_sql_quotes_constraint_columns() {
1038        let mut table = TableDefinition::new("t").add_required_column("select", SqlType::int());
1039        table.push_constraint(TableConstraint::AssumedPrimaryKey {
1040            columns: vec!["select".to_string()],
1041        });
1042
1043        assert!(
1044            table
1045                .to_create_sql(true)
1046                .unwrap()
1047                .contains(r#"ASSUMED PRIMARY KEY ("select")"#)
1048        );
1049    }
1050
1051    #[test]
1052    fn test_create_sql_with_numeric() {
1053        let table = TableDefinition::new("products")
1054            .add_required_column("id", SqlType::int())
1055            .add_nullable_column("name", SqlType::text())
1056            .add_nullable_column("price", SqlType::numeric(18, 2));
1057
1058        let sql = table.to_create_sql(true).unwrap();
1059        assert_eq!(
1060            sql,
1061            r#"CREATE TABLE "products" ("id" INTEGER NOT NULL, "name" TEXT, "price" NUMERIC(18, 2))"#
1062        );
1063    }
1064
1065    #[test]
1066    fn test_qualified_name() {
1067        let table = TableDefinition::new("users")
1068            .with_schema("public")
1069            .with_database("mydb");
1070        assert_eq!(table.qualified_name(), r"mydb.public.users");
1071    }
1072
1073    #[test]
1074    fn test_table_name() {
1075        let table = TableDefinition::new("Extract").with_schema("Extract");
1076        // "Extract" is quoted because it contains uppercase letters (to preserve case)
1077        assert_eq!(table.table_name(), r#""Extract""#);
1078    }
1079
1080    #[test]
1081    fn test_drop_sql() {
1082        let table = TableDefinition::new("users");
1083        assert_eq!(table.to_drop_sql(true), r"DROP TABLE users");
1084        assert_eq!(table.to_drop_sql(false), r"DROP TABLE IF EXISTS users");
1085    }
1086
1087    #[test]
1088    fn test_column_definition_helpers() {
1089        let not_null = ColumnDefinition::not_null("id", SqlType::int());
1090        assert!(!not_null.nullable);
1091
1092        let nullable = ColumnDefinition::nullable("name", SqlType::text());
1093        assert!(nullable.nullable);
1094    }
1095
1096    #[test]
1097    fn test_persistence() {
1098        let perm = TableDefinition::new("data");
1099        assert_eq!(perm.get_persistence(), Persistence::Permanent);
1100
1101        let temp = TableDefinition::new("temp_data").with_persistence(Persistence::Temporary);
1102        assert_eq!(temp.get_persistence(), Persistence::Temporary);
1103    }
1104
1105    #[test]
1106    fn test_temporary_table_sql() {
1107        let table = TableDefinition::new("temp_data")
1108            .with_persistence(Persistence::Temporary)
1109            .add_required_column("id", SqlType::int());
1110
1111        let sql = table.to_create_sql(true).unwrap();
1112        assert_eq!(
1113            sql,
1114            r#"CREATE TEMPORARY TABLE "temp_data" ("id" INTEGER NOT NULL)"#
1115        );
1116    }
1117
1118    #[test]
1119    fn test_collation() {
1120        let col = ColumnDefinition::with_collation(
1121            "name",
1122            SqlType::text(),
1123            "en_US",
1124            Nullability::Nullable,
1125        );
1126        assert_eq!(col.collation(), Some("en_US"));
1127    }
1128
1129    #[test]
1130    fn test_column_with_collation_sql() {
1131        let table = TableDefinition::new("users").add_nullable_column_with_collation(
1132            "name",
1133            SqlType::text(),
1134            "en_US",
1135        );
1136
1137        let sql = table.to_create_sql(true).unwrap();
1138        // "en_US" is quoted because it contains uppercase letters (to preserve case)
1139        assert!(sql.contains(r#"COLLATE "en_US""#));
1140    }
1141
1142    #[test]
1143    fn test_column_lookup() {
1144        let table = TableDefinition::new("users")
1145            .add_required_column("id", SqlType::int())
1146            .add_nullable_column("name", SqlType::text());
1147
1148        assert!(table.column_by_name("id").is_some());
1149        assert!(table.column_by_name("nonexistent").is_none());
1150        assert_eq!(table.column_position_by_name("name"), Some(1));
1151    }
1152
1153    #[test]
1154    fn test_fluent_builder_pattern() {
1155        // This test demonstrates the fluent builder pattern
1156        let table = TableDefinition::new("Extract")
1157            .with_schema("Extract")
1158            .add_required_column("Customer ID", SqlType::text())
1159            .add_required_column("Customer Name", SqlType::text())
1160            .add_required_column("Loyalty Reward Points", SqlType::big_int())
1161            .add_required_column("Segment", SqlType::text());
1162
1163        assert_eq!(table.column_count(), 4);
1164        assert_eq!(table.schema, Some("Extract".to_string()));
1165        assert_eq!(table.name, "Extract");
1166    }
1167}