Skip to main content

drizzle_types/postgres/ddl/
sql.rs

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