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}