Skip to main content

drizzle_types/postgres/ddl/
sql.rs

1//! SQL generation for `PostgreSQL` DDL types
2//!
3//! This module provides SQL generation methods for DDL types, enabling
4//! unified SQL output from both compile-time and runtime schema definitions.
5
6use crate::alloc_prelude::*;
7use core::fmt::Write;
8
9use super::{
10    CheckConstraint, Column, Enum, ForeignKey, Generated, GeneratedType, Identity, IdentityType,
11    Index, IndexColumn, IndexColumnDef, Policy, PrimaryKey, Sequence, Table, UniqueConstraint,
12    View,
13};
14
15fn quote_ident(ident: &str) -> String {
16    format!("\"{}\"", ident.replace('"', "\"\""))
17}
18
19fn quote_literal(value: &str) -> String {
20    format!("'{}'", value.replace('\'', "''"))
21}
22
23fn schema_prefix(schema: &str) -> String {
24    if schema == "public" {
25        String::new()
26    } else {
27        format!("{}.", quote_ident(schema))
28    }
29}
30
31fn qualified_name(schema: &str, name: &str) -> String {
32    format!("{}{}", schema_prefix(schema), quote_ident(name))
33}
34
35/// Quote a possibly schema-qualified name (`parent`, `app.parent`) by
36/// splitting on `.` and quoting each part.
37fn quote_qualified_name(name: &str) -> String {
38    name.split('.')
39        .map(quote_ident)
40        .collect::<Vec<_>>()
41        .join(".")
42}
43
44/// Whether a custom type name needs identifier quoting to survive.
45///
46/// Lowercase single-token names (`status`, `vector`) resolve identically
47/// with or without quotes, and multi-word built-ins (`double precision`)
48/// must never be quoted — only case-sensitive or otherwise unusual
49/// single-token names require quotes.
50fn type_name_needs_quoting(base: &str) -> bool {
51    !base.contains(' ')
52        && !base
53            .chars()
54            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
55}
56
57fn index_column_sql(column: &IndexColumn) -> String {
58    let mut sql = if column.is_expression {
59        format!("({})", column.value)
60    } else {
61        quote_ident(&column.value)
62    };
63
64    if let Some(op) = column.opclass.as_ref() {
65        let _ = write!(sql, " {op}");
66    }
67
68    if !column.asc {
69        sql.push_str(" DESC");
70    }
71
72    if column.nulls_first {
73        sql.push_str(" NULLS FIRST");
74    }
75
76    sql
77}
78
79// =============================================================================
80// Table SQL Generation
81// =============================================================================
82
83/// A complete table definition with all related entities for SQL generation
84#[derive(Clone, Debug)]
85pub struct TableSql<'a> {
86    pub table: &'a Table,
87    pub columns: &'a [Column],
88    pub primary_key: Option<&'a PrimaryKey>,
89    pub foreign_keys: &'a [ForeignKey],
90    pub unique_constraints: &'a [UniqueConstraint],
91    pub check_constraints: &'a [CheckConstraint],
92    pub indexes: &'a [Index],
93    pub policies: &'a [Policy],
94}
95
96impl<'a> TableSql<'a> {
97    /// Create a new `TableSql` for SQL generation
98    #[must_use]
99    pub const fn new(table: &'a Table) -> Self {
100        Self {
101            table,
102            columns: &[],
103            primary_key: None,
104            foreign_keys: &[],
105            unique_constraints: &[],
106            check_constraints: &[],
107            indexes: &[],
108            policies: &[],
109        }
110    }
111
112    /// Set columns
113    #[must_use]
114    pub const fn columns(mut self, columns: &'a [Column]) -> Self {
115        self.columns = columns;
116        self
117    }
118
119    /// Set primary key
120    #[must_use]
121    pub const fn primary_key(mut self, pk: Option<&'a PrimaryKey>) -> Self {
122        self.primary_key = pk;
123        self
124    }
125
126    /// Set foreign keys
127    #[must_use]
128    pub const fn foreign_keys(mut self, fks: &'a [ForeignKey]) -> Self {
129        self.foreign_keys = fks;
130        self
131    }
132
133    /// Set unique constraints
134    #[must_use]
135    pub const fn unique_constraints(mut self, uniques: &'a [UniqueConstraint]) -> Self {
136        self.unique_constraints = uniques;
137        self
138    }
139
140    /// Set check constraints
141    #[must_use]
142    pub const fn check_constraints(mut self, checks: &'a [CheckConstraint]) -> Self {
143        self.check_constraints = checks;
144        self
145    }
146
147    /// Set indexes
148    #[must_use]
149    pub const fn indexes(mut self, indexes: &'a [Index]) -> Self {
150        self.indexes = indexes;
151        self
152    }
153
154    /// Set policies
155    #[must_use]
156    pub const fn policies(mut self, policies: &'a [Policy]) -> Self {
157        self.policies = policies;
158        self
159    }
160
161    /// Generate CREATE TABLE SQL
162    #[must_use]
163    pub fn create_table_sql(&self) -> String {
164        let table_kind = if self.table.is_temporary.unwrap_or(false) {
165            "TEMPORARY "
166        } else if self.table.is_unlogged.unwrap_or(false) {
167            "UNLOGGED "
168        } else {
169            ""
170        };
171        let mut sql = format!(
172            "CREATE {}TABLE {} (\n",
173            table_kind,
174            qualified_name(self.table.schema(), self.table.name())
175        );
176
177        let mut lines = Vec::new();
178
179        // Column definitions
180        for column in self.columns {
181            lines.push(format!("\t{}", column.to_column_sql()));
182        }
183
184        // Primary key
185        if let Some(pk) = &self.primary_key {
186            let cols = pk
187                .columns
188                .iter()
189                .map(|c| quote_ident(c))
190                .collect::<Vec<_>>()
191                .join(", ");
192            if pk.name_explicit {
193                lines.push(format!(
194                    "\tCONSTRAINT {} PRIMARY KEY({})",
195                    quote_ident(pk.name()),
196                    cols
197                ));
198            } else {
199                lines.push(format!("\tPRIMARY KEY({cols})"));
200            }
201        }
202
203        // Foreign keys
204        for fk in self.foreign_keys {
205            lines.push(format!("\t{}", fk.to_constraint_sql()));
206        }
207
208        // Unique constraints
209        for unique in self.unique_constraints {
210            lines.push(format!("\t{}", unique.to_constraint_sql()));
211        }
212
213        // Check constraints
214        for check in self.check_constraints {
215            lines.push(format!(
216                "\tCONSTRAINT {} CHECK ({})",
217                quote_ident(check.name()),
218                check.value
219            ));
220        }
221
222        sql.push_str(&lines.join(",\n"));
223        sql.push('\n');
224        sql.push(')');
225
226        if let Some(inherits) = self.table.inherits.as_ref() {
227            let _ = write!(sql, " INHERITS ({})", quote_qualified_name(inherits));
228        }
229
230        if let Some(tablespace) = self.table.tablespace.as_ref() {
231            let _ = write!(sql, " TABLESPACE {}", quote_ident(tablespace));
232        }
233
234        sql.push(';');
235
236        sql
237    }
238
239    /// Generate DROP TABLE SQL
240    #[must_use]
241    pub fn drop_table_sql(&self) -> String {
242        format!(
243            "DROP TABLE {};",
244            qualified_name(self.table.schema(), self.table.name())
245        )
246    }
247
248    /// Generate all related indexes
249    #[must_use]
250    pub fn create_indexes_sql(&self) -> Vec<String> {
251        self.indexes
252            .iter()
253            .map(super::index::Index::create_index_sql)
254            .collect()
255    }
256
257    /// Generate COMMENT ON statements for the table and its columns.
258    #[must_use]
259    pub fn create_comments_sql(&self) -> Vec<String> {
260        let mut comments = Vec::new();
261        let table_name = qualified_name(self.table.schema(), self.table.name());
262
263        if let Some(comment) = self.table.comment.as_ref() {
264            comments.push(format!(
265                "COMMENT ON TABLE {} IS {};",
266                table_name,
267                quote_literal(comment)
268            ));
269        }
270
271        for column in self.columns {
272            if let Some(comment) = column.comment.as_ref() {
273                comments.push(format!(
274                    "COMMENT ON COLUMN {}.{} IS {};",
275                    table_name,
276                    quote_ident(column.name()),
277                    quote_literal(comment)
278                ));
279            }
280        }
281
282        comments
283    }
284
285    /// Generate RLS enable statement if needed
286    #[must_use]
287    pub fn enable_rls_sql(&self) -> Option<String> {
288        if self.table.is_rls_enabled.unwrap_or(false) {
289            Some(format!(
290                "ALTER TABLE {} ENABLE ROW LEVEL SECURITY;",
291                qualified_name(self.table.schema(), self.table.name())
292            ))
293        } else {
294            None
295        }
296    }
297
298    /// Generate all policies
299    #[must_use]
300    pub fn create_policies_sql(&self) -> Vec<String> {
301        self.policies
302            .iter()
303            .map(super::policy::Policy::create_policy_sql)
304            .collect()
305    }
306}
307
308// =============================================================================
309// Column SQL Generation
310// =============================================================================
311
312impl Column {
313    /// Render this column's full type reference.
314    ///
315    /// Built-in types render verbatim (`integer`, `varchar(255)`). When
316    /// `type_schema` names a non-`public` schema, the custom/enum type name
317    /// is quoted and schema-qualified (`"app"."status"`). For `public` the
318    /// schema is elided and the name is only quoted when it needs it (mixed
319    /// case or other non-lowercase characters), so ordinary lowercase enum
320    /// names keep rendering exactly as before. Parenthesized type parameters
321    /// stay outside the quotes; array dimensions are appended.
322    #[must_use]
323    pub fn type_sql(&self) -> String {
324        let mut sql = match self.type_schema.as_deref() {
325            Some(type_schema) => {
326                let raw = self.sql_type();
327                let (base, params) = raw
328                    .find('(')
329                    .map_or((raw, ""), |idx| (raw[..idx].trim_end(), &raw[idx..]));
330                if type_schema == "public" {
331                    if type_name_needs_quoting(base) {
332                        format!("{}{}", quote_ident(base), params)
333                    } else {
334                        raw.to_string()
335                    }
336                } else {
337                    format!(
338                        "{}.{}{}",
339                        quote_ident(type_schema),
340                        quote_ident(base),
341                        params
342                    )
343                }
344            }
345            None => self.sql_type().to_string(),
346        };
347
348        if let Some(dimensions) = self.dimensions
349            && dimensions > 0
350        {
351            for _ in 0..dimensions {
352                sql.push_str("[]");
353            }
354        }
355
356        sql
357    }
358
359    /// Generate the column definition SQL (without leading/trailing punctuation)
360    #[must_use]
361    pub fn to_column_sql(&self) -> String {
362        let mut sql = format!("{} {}", quote_ident(self.name()), self.type_sql());
363
364        // COLLATE follows the type in the PostgreSQL grammar. Collation
365        // names are double-quoted identifiers (`COLLATE "en_US"`,
366        // `COLLATE "C"`).
367        if let Some(collate) = self.collate.as_ref() {
368            let _ = write!(sql, " COLLATE {}", quote_ident(collate));
369        }
370
371        // Handle identity columns
372        if let Some(identity) = &self.identity {
373            sql.push_str(&identity.to_sql());
374        }
375
376        // Handle generated columns
377        if let Some(generated) = &self.generated {
378            sql.push_str(&generated.to_sql());
379        }
380
381        // Default value (skip if identity or generated - PostgreSQL doesn't allow both)
382        if self.identity.is_none()
383            && self.generated.is_none()
384            && let Some(default) = self.default.as_ref()
385        {
386            let _ = write!(sql, " DEFAULT {default}");
387        }
388
389        // NOT NULL
390        if self.not_null {
391            sql.push_str(" NOT NULL");
392        }
393
394        sql
395    }
396
397    /// Generate ADD COLUMN SQL
398    #[must_use]
399    pub fn add_column_sql(&self) -> String {
400        format!(
401            "ALTER TABLE {} ADD COLUMN {};",
402            qualified_name(self.schema(), self.table()),
403            self.to_column_sql()
404        )
405    }
406
407    /// Generate DROP COLUMN SQL
408    #[must_use]
409    pub fn drop_column_sql(&self) -> String {
410        format!(
411            "ALTER TABLE {} DROP COLUMN {};",
412            qualified_name(self.schema(), self.table()),
413            quote_ident(self.name())
414        )
415    }
416}
417
418// =============================================================================
419// Identity Column SQL
420// =============================================================================
421
422impl Identity {
423    /// Generate the GENERATED AS IDENTITY clause
424    #[must_use]
425    pub fn to_sql(&self) -> String {
426        let identity_type = match self.type_ {
427            IdentityType::Always => "ALWAYS",
428            IdentityType::ByDefault => "BY DEFAULT",
429        };
430
431        let mut sql = format!(" GENERATED {identity_type} AS IDENTITY");
432
433        // Add sequence options if any are specified
434        let mut options = Vec::new();
435
436        if let Some(increment) = self.increment.as_ref() {
437            options.push(format!("INCREMENT BY {increment}"));
438        }
439        if let Some(min) = self.min_value.as_ref() {
440            options.push(format!("MINVALUE {min}"));
441        }
442        if let Some(max) = self.max_value.as_ref() {
443            options.push(format!("MAXVALUE {max}"));
444        }
445        if let Some(start) = self.start_with.as_ref() {
446            options.push(format!("START WITH {start}"));
447        }
448        if let Some(cache) = self.cache {
449            options.push(format!("CACHE {cache}"));
450        }
451        if self.cycle.unwrap_or(false) {
452            options.push("CYCLE".to_string());
453        }
454
455        if !options.is_empty() {
456            let _ = write!(sql, " ({})", options.join(" "));
457        }
458
459        sql
460    }
461}
462
463// =============================================================================
464// Generated Column SQL
465// =============================================================================
466
467impl Generated {
468    /// Generate the GENERATED clause SQL
469    #[must_use]
470    pub fn to_sql(&self) -> String {
471        let gen_type = match self.gen_type {
472            GeneratedType::Stored => "STORED",
473            GeneratedType::Virtual => "VIRTUAL",
474        };
475        format!(" GENERATED ALWAYS AS ({}) {}", self.expression, gen_type)
476    }
477}
478
479// =============================================================================
480// Foreign Key SQL Generation
481// =============================================================================
482
483impl ForeignKey {
484    /// Generate the CONSTRAINT ... FOREIGN KEY clause SQL
485    #[must_use]
486    pub fn to_constraint_sql(&self) -> String {
487        let from_cols = self
488            .columns
489            .iter()
490            .map(|c| quote_ident(c))
491            .collect::<Vec<_>>()
492            .join(", ");
493
494        let to_cols = self
495            .columns_to
496            .iter()
497            .map(|c| quote_ident(c))
498            .collect::<Vec<_>>()
499            .join(", ");
500
501        let mut sql = format!(
502            "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({})",
503            quote_ident(self.name()),
504            from_cols,
505            qualified_name(self.schema_to(), self.table_to()),
506            to_cols
507        );
508
509        if let Some(on_delete) = self.on_delete.as_ref()
510            && !on_delete.eq_ignore_ascii_case("NO ACTION")
511        {
512            let _ = write!(sql, " ON DELETE {}", on_delete.to_uppercase());
513        }
514
515        if let Some(on_update) = self.on_update.as_ref()
516            && !on_update.eq_ignore_ascii_case("NO ACTION")
517        {
518            let _ = write!(sql, " ON UPDATE {}", on_update.to_uppercase());
519        }
520
521        if self.deferrable || self.initially_deferred {
522            sql.push_str(" DEFERRABLE");
523            if self.initially_deferred {
524                sql.push_str(" INITIALLY DEFERRED");
525            }
526        }
527
528        sql
529    }
530
531    /// Generate ADD FOREIGN KEY SQL
532    #[must_use]
533    pub fn add_fk_sql(&self) -> String {
534        format!(
535            "ALTER TABLE {} ADD {};",
536            qualified_name(self.schema(), self.table()),
537            self.to_constraint_sql()
538        )
539    }
540
541    /// Generate DROP FOREIGN KEY SQL
542    #[must_use]
543    pub fn drop_fk_sql(&self) -> String {
544        format!(
545            "ALTER TABLE {} DROP CONSTRAINT {};",
546            qualified_name(self.schema(), self.table()),
547            quote_ident(self.name())
548        )
549    }
550}
551
552// =============================================================================
553// Index SQL Generation
554// =============================================================================
555
556impl Index {
557    /// Generate CREATE INDEX SQL
558    #[must_use]
559    pub fn create_index_sql(&self) -> String {
560        let unique = if self.is_unique { "UNIQUE " } else { "" };
561
562        let concurrently = if self.concurrently {
563            "CONCURRENTLY "
564        } else {
565            ""
566        };
567
568        let columns = self
569            .columns
570            .iter()
571            .map(index_column_sql)
572            .collect::<Vec<_>>()
573            .join(", ");
574
575        let using = self
576            .method
577            .as_ref()
578            .map(|m| format!(" USING {m}"))
579            .unwrap_or_default();
580
581        let mut sql = format!(
582            "CREATE {}INDEX {}{} ON {}{}({})",
583            unique,
584            concurrently,
585            quote_ident(self.name()),
586            qualified_name(self.schema(), self.table()),
587            using,
588            columns
589        );
590
591        if let Some(with) = self.with.as_ref() {
592            let _ = write!(sql, " WITH ({with})");
593        }
594
595        if let Some(where_clause) = self.where_clause.as_ref() {
596            let _ = write!(sql, " WHERE {where_clause}");
597        }
598
599        sql.push(';');
600        sql
601    }
602
603    /// Generate DROP INDEX SQL
604    #[must_use]
605    pub fn drop_index_sql(&self) -> String {
606        format!("DROP INDEX {};", qualified_name(self.schema(), self.name()))
607    }
608}
609
610impl IndexColumnDef {
611    /// Generate the column reference for an index
612    #[must_use]
613    pub fn to_sql(&self) -> String {
614        let mut sql = if self.is_expression {
615            format!("({})", self.value)
616        } else {
617            quote_ident(self.value)
618        };
619
620        if let Some(op) = self.opclass.as_ref() {
621            let _ = write!(sql, " {op}");
622        }
623
624        if !self.asc {
625            sql.push_str(" DESC");
626        }
627
628        if self.nulls_first {
629            sql.push_str(" NULLS FIRST");
630        }
631
632        sql
633    }
634}
635
636// =============================================================================
637// Enum SQL Generation
638// =============================================================================
639
640impl Enum {
641    /// Generate CREATE TYPE ... AS ENUM SQL
642    #[must_use]
643    pub fn create_enum_sql(&self) -> String {
644        let values = self
645            .values
646            .iter()
647            .map(|v| quote_literal(v))
648            .collect::<Vec<_>>()
649            .join(", ");
650        format!(
651            "CREATE TYPE {} AS ENUM ({});",
652            qualified_name(self.schema(), self.name()),
653            values
654        )
655    }
656
657    /// Generate DROP TYPE SQL
658    #[must_use]
659    pub fn drop_enum_sql(&self) -> String {
660        format!("DROP TYPE {};", qualified_name(self.schema(), self.name()))
661    }
662
663    /// Generate ALTER TYPE ... ADD VALUE SQL
664    #[must_use]
665    pub fn add_value_sql(&self, value: &str, before: Option<&str>) -> String {
666        before.map_or_else(
667            || {
668                format!(
669                    "ALTER TYPE {} ADD VALUE {};",
670                    qualified_name(self.schema(), self.name()),
671                    quote_literal(value)
672                )
673            },
674            |before_value| {
675                format!(
676                    "ALTER TYPE {} ADD VALUE {} BEFORE {};",
677                    qualified_name(self.schema(), self.name()),
678                    quote_literal(value),
679                    quote_literal(before_value)
680                )
681            },
682        )
683    }
684}
685
686// =============================================================================
687// Sequence SQL Generation
688// =============================================================================
689
690impl Sequence {
691    /// Generate CREATE SEQUENCE SQL
692    #[must_use]
693    pub fn create_sequence_sql(&self) -> String {
694        let mut sql = format!(
695            "CREATE SEQUENCE {}",
696            qualified_name(self.schema(), self.name())
697        );
698
699        if let Some(inc) = self.increment_by.as_ref() {
700            let _ = write!(sql, " INCREMENT BY {inc}");
701        }
702        if let Some(min) = self.min_value.as_ref() {
703            let _ = write!(sql, " MINVALUE {min}");
704        }
705        if let Some(max) = self.max_value.as_ref() {
706            let _ = write!(sql, " MAXVALUE {max}");
707        }
708        if let Some(start) = self.start_with.as_ref() {
709            let _ = write!(sql, " START WITH {start}");
710        }
711        if let Some(cache) = self.cache_size {
712            let _ = write!(sql, " CACHE {cache}");
713        }
714        if self.cycle.unwrap_or(false) {
715            sql.push_str(" CYCLE");
716        }
717
718        sql.push(';');
719        sql
720    }
721
722    /// Generate DROP SEQUENCE SQL
723    #[must_use]
724    pub fn drop_sequence_sql(&self) -> String {
725        format!(
726            "DROP SEQUENCE {};",
727            qualified_name(self.schema(), self.name())
728        )
729    }
730}
731
732// =============================================================================
733// View SQL Generation
734// =============================================================================
735
736/// Append WITH-options clause for `CREATE VIEW`, returning the uppercased
737/// CHECK OPTION clause (to be emitted after `AS <definition>`), if any.
738fn append_view_with_options(
739    sql: &mut String,
740    with_opts: &super::view::ViewWithOption,
741) -> Option<String> {
742    let mut options = String::new();
743    let mut has_option = false;
744
745    let mut push_option = |name: &str, value: &dyn core::fmt::Display| {
746        if has_option {
747            options.push_str(", ");
748        } else {
749            has_option = true;
750        }
751        let _ = write!(options, "{name} = {value}");
752    };
753
754    if let Some(value) = with_opts.security_barrier {
755        push_option("security_barrier", &value);
756    }
757    if let Some(value) = with_opts.security_invoker {
758        push_option("security_invoker", &value);
759    }
760    if let Some(value) = with_opts.fillfactor {
761        push_option("fillfactor", &value);
762    }
763    if let Some(value) = with_opts.toast_tuple_target {
764        push_option("toast_tuple_target", &value);
765    }
766    if let Some(value) = with_opts.parallel_workers {
767        push_option("parallel_workers", &value);
768    }
769    if let Some(value) = with_opts.autovacuum_enabled {
770        push_option("autovacuum_enabled", &value);
771    }
772    if let Some(value) = with_opts.vacuum_index_cleanup.as_ref() {
773        push_option("vacuum_index_cleanup", value);
774    }
775    if let Some(value) = with_opts.vacuum_truncate {
776        push_option("vacuum_truncate", &value);
777    }
778    if let Some(value) = with_opts.autovacuum_vacuum_threshold {
779        push_option("autovacuum_vacuum_threshold", &value);
780    }
781    if let Some(value) = with_opts.autovacuum_vacuum_scale_factor {
782        push_option("autovacuum_vacuum_scale_factor", &value);
783    }
784    if let Some(value) = with_opts.autovacuum_vacuum_cost_delay {
785        push_option("autovacuum_vacuum_cost_delay", &value);
786    }
787    if let Some(value) = with_opts.autovacuum_vacuum_cost_limit {
788        push_option("autovacuum_vacuum_cost_limit", &value);
789    }
790    if let Some(value) = with_opts.autovacuum_freeze_min_age {
791        push_option("autovacuum_freeze_min_age", &value);
792    }
793    if let Some(value) = with_opts.autovacuum_freeze_max_age {
794        push_option("autovacuum_freeze_max_age", &value);
795    }
796    if let Some(value) = with_opts.autovacuum_freeze_table_age {
797        push_option("autovacuum_freeze_table_age", &value);
798    }
799    if let Some(value) = with_opts.autovacuum_multixact_freeze_min_age {
800        push_option("autovacuum_multixact_freeze_min_age", &value);
801    }
802    if let Some(value) = with_opts.autovacuum_multixact_freeze_max_age {
803        push_option("autovacuum_multixact_freeze_max_age", &value);
804    }
805    if let Some(value) = with_opts.autovacuum_multixact_freeze_table_age {
806        push_option("autovacuum_multixact_freeze_table_age", &value);
807    }
808    if let Some(value) = with_opts.log_autovacuum_min_duration {
809        push_option("log_autovacuum_min_duration", &value);
810    }
811    if let Some(value) = with_opts.user_catalog_table {
812        push_option("user_catalog_table", &value);
813    }
814
815    if has_option {
816        let _ = write!(sql, " WITH ({options})");
817    }
818
819    with_opts
820        .check_option
821        .as_deref()
822        .map(str::to_ascii_uppercase)
823}
824
825impl View {
826    /// Generate CREATE VIEW SQL
827    #[must_use]
828    pub fn create_view_sql(&self) -> String {
829        let materialized = if self.materialized {
830            "MATERIALIZED "
831        } else {
832            ""
833        };
834
835        let Some(def) = self.definition.as_ref() else {
836            return format!(
837                "-- {}View {} has no definition",
838                materialized,
839                qualified_name(self.schema(), self.name())
840            );
841        };
842
843        let mut sql = String::with_capacity(def.len() + 64);
844        let _ = write!(
845            sql,
846            "CREATE {}VIEW {}",
847            materialized,
848            qualified_name(self.schema(), self.name()),
849        );
850
851        if let Some(using) = self.using.as_ref() {
852            let _ = write!(sql, " USING {using}");
853        }
854
855        let check_option_clause = self
856            .with
857            .as_ref()
858            .and_then(|with_opts| append_view_with_options(&mut sql, with_opts));
859
860        if let Some(tablespace) = self.tablespace.as_ref() {
861            let _ = write!(sql, " TABLESPACE {}", quote_ident(tablespace));
862        }
863
864        sql.push_str(" AS ");
865        sql.push_str(def);
866
867        if let Some(check_option) = check_option_clause {
868            let _ = write!(sql, " WITH {check_option} CHECK OPTION");
869        }
870
871        if self.materialized && matches!(self.with_no_data, Some(true)) {
872            sql.push_str(" WITH NO DATA");
873        }
874
875        sql.push(';');
876        sql
877    }
878
879    /// Generate DROP VIEW SQL
880    #[must_use]
881    pub fn drop_view_sql(&self) -> String {
882        let materialized = if self.materialized {
883            "MATERIALIZED "
884        } else {
885            ""
886        };
887        format!(
888            "DROP {}VIEW {};",
889            materialized,
890            qualified_name(self.schema(), self.name())
891        )
892    }
893}
894
895// =============================================================================
896// Policy SQL Generation
897// =============================================================================
898
899impl Policy {
900    /// Generate CREATE POLICY SQL
901    #[must_use]
902    pub fn create_policy_sql(&self) -> String {
903        let mut sql = format!(
904            "CREATE POLICY {} ON {}",
905            quote_ident(self.name()),
906            qualified_name(self.schema(), self.table())
907        );
908
909        let as_clause = self.as_clause.as_deref().unwrap_or("PERMISSIVE");
910        let _ = write!(sql, " AS {}", as_clause.to_uppercase());
911
912        if let Some(r#for) = self.for_clause.as_ref() {
913            let _ = write!(sql, " FOR {}", r#for.to_uppercase());
914        }
915
916        if let Some(to) = self.to.as_ref()
917            && !to.is_empty()
918        {
919            let to_roles = to
920                .iter()
921                .map(|r| {
922                    if r.eq_ignore_ascii_case("public") {
923                        "PUBLIC".to_string()
924                    } else {
925                        quote_ident(r)
926                    }
927                })
928                .collect::<Vec<_>>()
929                .join(", ");
930            let _ = write!(sql, " TO {to_roles}");
931        }
932
933        if let Some(using) = self.using.as_ref() {
934            let _ = write!(sql, " USING ({using})");
935        }
936
937        if let Some(with_check) = self.with_check.as_ref() {
938            let _ = write!(sql, " WITH CHECK ({with_check})");
939        }
940
941        sql.push(';');
942        sql
943    }
944
945    /// Generate DROP POLICY SQL
946    #[must_use]
947    pub fn drop_policy_sql(&self) -> String {
948        format!(
949            "DROP POLICY {} ON {};",
950            quote_ident(self.name()),
951            qualified_name(self.schema(), self.table())
952        )
953    }
954}
955
956// =============================================================================
957// Table-level utilities
958// =============================================================================
959
960impl Table {
961    /// Generate DROP TABLE SQL
962    #[must_use]
963    pub fn drop_table_sql(&self) -> String {
964        format!("DROP TABLE {};", qualified_name(self.schema(), self.name()))
965    }
966
967    /// Generate RENAME TABLE SQL
968    #[must_use]
969    pub fn rename_table_sql(&self, new_name: &str) -> String {
970        format!(
971            "ALTER TABLE {} RENAME TO {};",
972            qualified_name(self.schema(), self.name()),
973            quote_ident(new_name)
974        )
975    }
976}
977
978// =============================================================================
979// Primary Key SQL Generation
980// =============================================================================
981
982impl PrimaryKey {
983    /// Generate the PRIMARY KEY constraint clause
984    #[must_use]
985    pub fn to_constraint_sql(&self) -> String {
986        let cols = self
987            .columns
988            .iter()
989            .map(|c| quote_ident(c))
990            .collect::<Vec<_>>()
991            .join(", ");
992
993        format!(
994            "CONSTRAINT {} PRIMARY KEY({})",
995            quote_ident(self.name()),
996            cols
997        )
998    }
999
1000    /// Generate ADD PRIMARY KEY SQL
1001    #[must_use]
1002    pub fn add_pk_sql(&self) -> String {
1003        format!(
1004            "ALTER TABLE {} ADD {};",
1005            qualified_name(self.schema(), self.table()),
1006            self.to_constraint_sql()
1007        )
1008    }
1009
1010    /// Generate DROP PRIMARY KEY SQL
1011    #[must_use]
1012    pub fn drop_pk_sql(&self) -> String {
1013        format!(
1014            "ALTER TABLE {} DROP CONSTRAINT {};",
1015            qualified_name(self.schema(), self.table()),
1016            quote_ident(self.name())
1017        )
1018    }
1019}
1020
1021// =============================================================================
1022// Unique Constraint SQL Generation
1023// =============================================================================
1024
1025impl UniqueConstraint {
1026    fn constraint_sql(&self, space_before_columns: bool) -> String {
1027        let cols = self
1028            .columns
1029            .iter()
1030            .map(|c| quote_ident(c))
1031            .collect::<Vec<_>>()
1032            .join(", ");
1033
1034        let separator = if space_before_columns { " " } else { "" };
1035        let nulls_not_distinct = if self.nulls_not_distinct {
1036            // `UNIQUE NULLS NOT DISTINCT ("email")` — PostgreSQL 15+.
1037            " NULLS NOT DISTINCT"
1038        } else {
1039            ""
1040        };
1041        let mut sql = format!(
1042            "CONSTRAINT {} UNIQUE{}{}({})",
1043            quote_ident(self.name()),
1044            nulls_not_distinct,
1045            separator,
1046            cols
1047        );
1048        if self.deferrable || self.initially_deferred {
1049            sql.push_str(" DEFERRABLE");
1050            if self.initially_deferred {
1051                sql.push_str(" INITIALLY DEFERRED");
1052            }
1053        }
1054        sql
1055    }
1056
1057    /// Generate the UNIQUE constraint clause
1058    #[must_use]
1059    pub fn to_constraint_sql(&self) -> String {
1060        self.constraint_sql(false)
1061    }
1062
1063    /// Generate ADD UNIQUE SQL
1064    #[must_use]
1065    pub fn add_unique_sql(&self) -> String {
1066        format!(
1067            "ALTER TABLE {} ADD {};",
1068            qualified_name(self.schema(), self.table()),
1069            self.constraint_sql(true)
1070        )
1071    }
1072
1073    /// Generate DROP UNIQUE SQL
1074    #[must_use]
1075    pub fn drop_unique_sql(&self) -> String {
1076        format!(
1077            "ALTER TABLE {} DROP CONSTRAINT {};",
1078            qualified_name(self.schema(), self.table()),
1079            quote_ident(self.name())
1080        )
1081    }
1082}
1083
1084// =============================================================================
1085// Check Constraint SQL Generation
1086// =============================================================================
1087
1088impl CheckConstraint {
1089    /// Generate the CHECK constraint clause
1090    #[must_use]
1091    pub fn to_constraint_sql(&self) -> String {
1092        format!(
1093            "CONSTRAINT {} CHECK ({})",
1094            quote_ident(self.name()),
1095            self.value
1096        )
1097    }
1098
1099    /// Generate ADD CHECK SQL
1100    #[must_use]
1101    pub fn add_check_sql(&self) -> String {
1102        format!(
1103            "ALTER TABLE {} ADD {};",
1104            qualified_name(self.schema(), self.table()),
1105            self.to_constraint_sql()
1106        )
1107    }
1108
1109    /// Generate DROP CHECK SQL
1110    #[must_use]
1111    pub fn drop_check_sql(&self) -> String {
1112        format!(
1113            "ALTER TABLE {} DROP CONSTRAINT {};",
1114            qualified_name(self.schema(), self.table()),
1115            quote_ident(self.name())
1116        )
1117    }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use super::*;
1123    use crate::postgres::ddl::{ColumnDef, PrimaryKeyDef, TableDef};
1124    use std::borrow::Cow;
1125
1126    #[test]
1127    fn test_simple_create_table() {
1128        let table = TableDef::new("public", "users").into_table();
1129        let columns = [
1130            ColumnDef::new("public", "users", "id", "SERIAL")
1131                .not_null()
1132                .into_column(),
1133            ColumnDef::new("public", "users", "name", "TEXT")
1134                .not_null()
1135                .into_column(),
1136            ColumnDef::new("public", "users", "email", "TEXT").into_column(),
1137        ];
1138        const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
1139        let pk = PrimaryKeyDef::new("public", "users", "users_pkey")
1140            .columns(PK_COLS)
1141            .into_primary_key();
1142
1143        let sql = TableSql::new(&table)
1144            .columns(&columns)
1145            .primary_key(Some(&pk))
1146            .create_table_sql();
1147
1148        assert!(sql.contains("CREATE TABLE \"users\""));
1149        assert!(sql.contains("\"id\" SERIAL NOT NULL"));
1150        assert!(sql.contains("\"name\" TEXT NOT NULL"));
1151        assert!(sql.contains("\"email\" TEXT"));
1152    }
1153
1154    #[test]
1155    fn test_table_with_schema() {
1156        let table = TableDef::new("myschema", "users").into_table();
1157        let sql = TableSql::new(&table).create_table_sql();
1158        assert!(sql.contains("\"myschema\".\"users\""));
1159    }
1160
1161    #[test]
1162    fn test_unique_concurrently_index_word_order() {
1163        let mut index = Index::new(
1164            "public",
1165            "users",
1166            "users_email_idx",
1167            vec![IndexColumn::new("email")],
1168        )
1169        .unique();
1170        index.concurrently = true;
1171
1172        assert_eq!(
1173            index.create_index_sql(),
1174            "CREATE UNIQUE INDEX CONCURRENTLY \"users_email_idx\" ON \"users\"(\"email\");"
1175        );
1176    }
1177
1178    #[test]
1179    fn test_policy_uses_explicit_as_and_public_role() {
1180        let mut policy = Policy::new("public", "users", "users_policy");
1181        policy.to = Some(vec![Cow::Borrowed("public")]);
1182
1183        assert_eq!(
1184            policy.create_policy_sql(),
1185            "CREATE POLICY \"users_policy\" ON \"users\" AS PERMISSIVE TO PUBLIC;"
1186        );
1187    }
1188}