1use super::reloptions::render_index_options;
12use crate::identifier::{Identifier, QualifiedName};
13use crate::ir::column::{
14 Column, Compression, GeneratedKind, Identity, IdentityKind, SequenceOptions, StorageKind,
15};
16use crate::ir::constraint::{
17 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
18};
19use crate::ir::default_expr::{DefaultExpr, LiteralValue};
20use crate::ir::index::{Index, IndexColumn, IndexColumnExpr, IndexMethod, NullsOrder, SortOrder};
21use crate::ir::schema::Schema;
22use crate::ir::sequence::{Sequence, SequenceOwner};
23use crate::ir::table::Table;
24
25pub fn create_schema(s: &Schema) -> String {
31 format!("CREATE SCHEMA {};", s.name.render_sql())
32}
33
34pub fn drop_schema(name: &Identifier) -> String {
36 format!("DROP SCHEMA {};", name.render_sql())
37}
38
39pub fn comment_on_schema(name: &Identifier, comment: Option<&str>) -> String {
41 format!(
42 "COMMENT ON SCHEMA {} IS {};",
43 name.render_sql(),
44 render_comment(comment),
45 )
46}
47
48pub fn create_table(t: &Table) -> String {
55 let mut s = String::new();
56 s.push_str("CREATE TABLE ");
57 s.push_str(&t.qname.render_sql());
58
59 if let Some(po) = &t.partition_of {
60 s.push(' ');
63 s.push_str(&crate::plan::rewrite::partitions::render_partition_of(po));
64 } else {
65 s.push_str(" (");
67 let mut first = true;
68 for col in &t.columns {
69 if !first {
70 s.push(',');
71 }
72 s.push('\n');
73 s.push_str(" ");
74 s.push_str(&column_def(col));
75 first = false;
76 }
77 for c in &t.constraints {
78 if !first {
79 s.push(',');
80 }
81 s.push('\n');
82 s.push_str(" ");
83 s.push_str(&inline_constraint(c));
84 first = false;
85 }
86 if !first {
87 s.push('\n');
88 }
89 s.push(')');
90 }
91
92 if let Some(pb) = &t.partition_by {
93 s.push(' ');
94 s.push_str(&crate::plan::rewrite::partitions::render_partition_by(pb));
95 }
96
97 if let Some(am) = &t.access_method {
100 s.push_str(" USING ");
101 s.push_str(&am.render_sql());
102 }
103
104 if let Some(ts) = &t.tablespace {
106 s.push_str(" TABLESPACE ");
107 s.push_str(&ts.render_sql());
108 }
109
110 s.push(';');
111 s
112}
113
114pub fn drop_table(qname: &QualifiedName) -> String {
116 format!("DROP TABLE {};", qname.render_sql())
117}
118
119pub fn alter_table_set_tablespace(qname: &QualifiedName, name: Option<&Identifier>) -> String {
124 let ts = name.map_or_else(|| "pg_default".to_owned(), Identifier::render_sql);
125 format!("ALTER TABLE {} SET TABLESPACE {};", qname.render_sql(), ts)
126}
127
128pub fn comment_on_table(qname: &QualifiedName, comment: Option<&str>) -> String {
130 format!(
131 "COMMENT ON TABLE {} IS {};",
132 qname.render_sql(),
133 render_comment(comment),
134 )
135}
136
137pub fn create_index(idx: &Index, concurrently: bool) -> String {
139 let mut s = String::from("CREATE ");
140 if idx.unique {
141 s.push_str("UNIQUE ");
142 }
143 s.push_str("INDEX ");
144 if concurrently {
145 s.push_str("CONCURRENTLY ");
146 }
147 s.push_str(&idx.qname.name.render_sql());
148 s.push_str(" ON ");
149 s.push_str(&idx.on.qname().render_sql());
150 s.push_str(" USING ");
151 s.push_str(index_method(idx.method));
152 s.push_str(" (");
153 s.push_str(&render_index_columns(&idx.columns));
154 s.push(')');
155 if !idx.include.is_empty() {
156 s.push_str(" INCLUDE (");
157 s.push_str(&render_idents(&idx.include));
158 s.push(')');
159 }
160 if idx.unique && idx.nulls_not_distinct {
161 s.push_str(" NULLS NOT DISTINCT");
162 }
163 if !idx.storage.is_empty() {
164 s.push_str(" WITH (");
165 s.push_str(&render_index_options(&idx.storage));
166 s.push(')');
167 }
168 if let Some(ts) = &idx.tablespace {
169 s.push_str(" TABLESPACE ");
170 s.push_str(&ts.render_sql());
171 }
172 if let Some(pred) = &idx.predicate {
173 s.push_str(" WHERE ");
174 s.push_str(&pred.canonical_text);
175 }
176 s.push(';');
177 s
178}
179
180pub fn drop_index(qname: &QualifiedName, concurrently: bool) -> String {
182 if concurrently {
183 format!("DROP INDEX CONCURRENTLY {};", qname.render_sql())
184 } else {
185 format!("DROP INDEX {};", qname.render_sql())
186 }
187}
188
189pub fn create_sequence(s: &Sequence) -> String {
191 let mut out = String::from("CREATE SEQUENCE ");
192 out.push_str(&s.qname.render_sql());
193 out.push_str(" AS ");
194 out.push_str(&s.data_type.render_sql());
195 out.push_str(&format!(" INCREMENT BY {}", s.increment));
196 if let Some(min) = s.min_value {
197 out.push_str(&format!(" MINVALUE {min}"));
198 } else {
199 out.push_str(" NO MINVALUE");
200 }
201 if let Some(max) = s.max_value {
202 out.push_str(&format!(" MAXVALUE {max}"));
203 } else {
204 out.push_str(" NO MAXVALUE");
205 }
206 out.push_str(&format!(" START WITH {}", s.start));
207 out.push_str(&format!(" CACHE {}", s.cache));
208 if s.cycle {
209 out.push_str(" CYCLE");
210 } else {
211 out.push_str(" NO CYCLE");
212 }
213 if let Some(owner) = &s.owned_by {
214 out.push_str(" OWNED BY ");
215 out.push_str(&render_owner(owner));
216 }
217 out.push(';');
218 out
219}
220
221pub fn drop_sequence(qname: &QualifiedName) -> String {
223 format!("DROP SEQUENCE {};", qname.render_sql())
224}
225
226pub fn alter_table_add_column(qname: &QualifiedName, c: &Column) -> String {
232 format!(
233 "ALTER TABLE {} ADD COLUMN {};",
234 qname.render_sql(),
235 column_def(c),
236 )
237}
238
239pub fn alter_table_drop_column(qname: &QualifiedName, name: &Identifier) -> String {
241 format!(
242 "ALTER TABLE {} DROP COLUMN {};",
243 qname.render_sql(),
244 name.render_sql(),
245 )
246}
247
248pub fn alter_column_type(
250 qname: &QualifiedName,
251 name: &Identifier,
252 to: &crate::ir::column_type::ColumnType,
253 using: Option<&crate::ir::default_expr::NormalizedExpr>,
254) -> String {
255 let mut s = format!(
256 "ALTER TABLE {} ALTER COLUMN {} TYPE {}",
257 qname.render_sql(),
258 name.render_sql(),
259 to.render_sql(),
260 );
261 if let Some(u) = using {
262 s.push_str(" USING ");
263 s.push_str(&u.canonical_text);
264 }
265 s.push(';');
266 s
267}
268
269pub fn alter_column_set_nullable(
271 qname: &QualifiedName,
272 name: &Identifier,
273 nullable: bool,
274) -> String {
275 let action = if nullable {
276 "DROP NOT NULL"
277 } else {
278 "SET NOT NULL"
279 };
280 format!(
281 "ALTER TABLE {} ALTER COLUMN {} {};",
282 qname.render_sql(),
283 name.render_sql(),
284 action,
285 )
286}
287
288pub fn alter_column_set_default(
290 qname: &QualifiedName,
291 name: &Identifier,
292 default: Option<&DefaultExpr>,
293) -> String {
294 match default {
295 Some(d) => format!(
296 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
297 qname.render_sql(),
298 name.render_sql(),
299 render_default_expr(d),
300 ),
301 None => format!(
302 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
303 qname.render_sql(),
304 name.render_sql(),
305 ),
306 }
307}
308
309pub fn alter_column_set_identity(
311 qname: &QualifiedName,
312 name: &Identifier,
313 identity: Option<&Identity>,
314) -> String {
315 match identity {
316 Some(id) => format!(
317 "ALTER TABLE {} ALTER COLUMN {} ADD GENERATED {} AS IDENTITY{};",
318 qname.render_sql(),
319 name.render_sql(),
320 identity_kind(id.kind),
321 render_sequence_options(&id.sequence),
322 ),
323 None => format!(
324 "ALTER TABLE {} ALTER COLUMN {} DROP IDENTITY;",
325 qname.render_sql(),
326 name.render_sql(),
327 ),
328 }
329}
330
331pub fn alter_column_set_generated(
339 qname: &QualifiedName,
340 name: &Identifier,
341 generated: Option<&crate::ir::column::Generated>,
342) -> String {
343 match generated {
344 None => format!(
345 "ALTER TABLE {} ALTER COLUMN {} DROP EXPRESSION;",
346 qname.render_sql(),
347 name.render_sql(),
348 ),
349 Some(g) => {
350 format!(
356 "ALTER TABLE {} ALTER COLUMN {} SET EXPRESSION AS ({}) {};",
357 qname.render_sql(),
358 name.render_sql(),
359 g.expression.canonical_text,
360 generated_kind(g.kind),
361 )
362 }
363 }
364}
365
366pub fn alter_column_set_storage(
368 qname: &QualifiedName,
369 name: &Identifier,
370 storage: StorageKind,
371) -> String {
372 let kw = match storage {
373 StorageKind::Plain => "PLAIN",
374 StorageKind::External => "EXTERNAL",
375 StorageKind::Extended => "EXTENDED",
376 StorageKind::Main => "MAIN",
377 };
378 format!(
379 "ALTER TABLE {} ALTER COLUMN {} SET STORAGE {};",
380 qname.render_sql(),
381 name.render_sql(),
382 kw,
383 )
384}
385
386pub fn alter_column_set_compression(
388 qname: &QualifiedName,
389 name: &Identifier,
390 compression: Option<Compression>,
391) -> String {
392 let kw = match compression {
393 Some(Compression::Pglz) => "pglz",
394 Some(Compression::Lz4) => "lz4",
395 None => "DEFAULT",
396 };
397 format!(
398 "ALTER TABLE {} ALTER COLUMN {} SET COMPRESSION {};",
399 qname.render_sql(),
400 name.render_sql(),
401 kw,
402 )
403}
404
405pub fn comment_on_column(qname: &QualifiedName, col: &Identifier, comment: Option<&str>) -> String {
407 format!(
408 "COMMENT ON COLUMN {}.{} IS {};",
409 qname.render_sql(),
410 col.render_sql(),
411 render_comment(comment),
412 )
413}
414
415pub fn alter_table_add_constraint(qname: &QualifiedName, c: &Constraint) -> String {
417 format!(
418 "ALTER TABLE {} ADD {};",
419 qname.render_sql(),
420 constraint_def_with_name(c),
421 )
422}
423
424pub fn alter_table_add_constraint_not_valid(qname: &QualifiedName, c: &Constraint) -> String {
426 format!(
427 "ALTER TABLE {} ADD {} NOT VALID;",
428 qname.render_sql(),
429 constraint_def_with_name(c),
430 )
431}
432
433pub fn alter_table_validate_constraint(qname: &QualifiedName, cname: &Identifier) -> String {
435 format!(
436 "ALTER TABLE {} VALIDATE CONSTRAINT {};",
437 qname.render_sql(),
438 cname.render_sql(),
439 )
440}
441
442pub fn alter_table_drop_constraint(qname: &QualifiedName, cname: &Identifier) -> String {
444 format!(
445 "ALTER TABLE {} DROP CONSTRAINT {};",
446 qname.render_sql(),
447 cname.render_sql(),
448 )
449}
450
451pub fn comment_on_constraint(
453 qname: &QualifiedName,
454 cname: &Identifier,
455 comment: Option<&str>,
456) -> String {
457 format!(
458 "COMMENT ON CONSTRAINT {} ON {} IS {};",
459 cname.render_sql(),
460 qname.render_sql(),
461 render_comment(comment),
462 )
463}
464
465pub fn comment_on_index(qname: &QualifiedName, comment: Option<&str>) -> String {
467 format!(
468 "COMMENT ON INDEX {} IS {};",
469 qname.render_sql(),
470 render_comment(comment),
471 )
472}
473
474pub fn comment_on_sequence(qname: &QualifiedName, comment: Option<&str>) -> String {
476 format!(
477 "COMMENT ON SEQUENCE {} IS {};",
478 qname.render_sql(),
479 render_comment(comment),
480 )
481}
482
483pub fn alter_sequence_increment(qname: &QualifiedName, n: i64) -> String {
489 format!("ALTER SEQUENCE {} INCREMENT BY {n};", qname.render_sql())
490}
491
492pub fn alter_sequence_min_value(qname: &QualifiedName, v: Option<i64>) -> String {
494 match v {
495 Some(n) => format!("ALTER SEQUENCE {} MINVALUE {n};", qname.render_sql()),
496 None => format!("ALTER SEQUENCE {} NO MINVALUE;", qname.render_sql()),
497 }
498}
499
500pub fn alter_sequence_max_value(qname: &QualifiedName, v: Option<i64>) -> String {
502 match v {
503 Some(n) => format!("ALTER SEQUENCE {} MAXVALUE {n};", qname.render_sql()),
504 None => format!("ALTER SEQUENCE {} NO MAXVALUE;", qname.render_sql()),
505 }
506}
507
508pub fn alter_sequence_cache(qname: &QualifiedName, n: i64) -> String {
510 format!("ALTER SEQUENCE {} CACHE {n};", qname.render_sql())
511}
512
513pub fn alter_sequence_cycle(qname: &QualifiedName, cycle: bool) -> String {
515 let kw = if cycle { "CYCLE" } else { "NO CYCLE" };
516 format!("ALTER SEQUENCE {} {kw};", qname.render_sql())
517}
518
519pub fn alter_sequence_data_type(
521 qname: &QualifiedName,
522 ty: &crate::ir::column_type::ColumnType,
523) -> String {
524 format!(
525 "ALTER SEQUENCE {} AS {};",
526 qname.render_sql(),
527 ty.render_sql(),
528 )
529}
530
531pub fn alter_sequence_owned_by(qname: &QualifiedName, owner: Option<&SequenceOwner>) -> String {
533 match owner {
534 Some(o) => format!(
535 "ALTER SEQUENCE {} OWNED BY {};",
536 qname.render_sql(),
537 render_owner(o),
538 ),
539 None => format!("ALTER SEQUENCE {} OWNED BY NONE;", qname.render_sql()),
540 }
541}
542
543pub fn column_def(c: &Column) -> String {
549 let mut s = String::new();
550 s.push_str(&c.name.render_sql());
551 s.push(' ');
552 s.push_str(&c.ty.render_sql());
553 if let Some(coll) = &c.collation {
554 s.push_str(" COLLATE ");
555 s.push_str(&coll.render_sql());
556 }
557 if let Some(compression) = c.compression {
571 let kw = match compression {
572 Compression::Pglz => "pglz",
573 Compression::Lz4 => "lz4",
574 };
575 s.push_str(" COMPRESSION ");
576 s.push_str(kw);
577 }
578 if !c.nullable {
579 s.push_str(" NOT NULL");
580 }
581 if let Some(d) = &c.default {
582 s.push_str(" DEFAULT ");
583 s.push_str(&render_default_expr(d));
584 }
585 if let Some(id) = &c.identity {
586 s.push_str(" GENERATED ");
587 s.push_str(identity_kind(id.kind));
588 s.push_str(" AS IDENTITY");
589 s.push_str(&render_sequence_options(&id.sequence));
590 }
591 if let Some(g) = &c.generated {
592 s.push_str(" GENERATED ALWAYS AS (");
593 s.push_str(&g.expression.canonical_text);
594 s.push_str(") ");
595 s.push_str(generated_kind(g.kind));
596 }
597 s
598}
599
600fn inline_constraint(c: &Constraint) -> String {
602 constraint_def_with_name(c)
603}
604
605pub fn constraint_def_with_name(c: &Constraint) -> String {
607 let mut s = format!(
608 "CONSTRAINT {} {}",
609 c.qname.name.render_sql(),
610 constraint_body(&c.kind)
611 );
612 match c.deferrable {
613 Deferrable::NotDeferrable => {}
614 Deferrable::Deferrable {
615 initially_deferred: true,
616 } => s.push_str(" DEFERRABLE INITIALLY DEFERRED"),
617 Deferrable::Deferrable {
618 initially_deferred: false,
619 } => s.push_str(" DEFERRABLE INITIALLY IMMEDIATE"),
620 }
621 s
622}
623
624fn constraint_body(k: &ConstraintKind) -> String {
625 match k {
626 ConstraintKind::PrimaryKey { columns, include } => {
627 let mut s = format!("PRIMARY KEY ({})", render_idents(columns));
628 if !include.is_empty() {
629 s.push_str(&format!(" INCLUDE ({})", render_idents(include)));
630 }
631 s
632 }
633 ConstraintKind::Unique {
634 columns,
635 include,
636 nulls_distinct,
637 } => {
638 let mut s = String::from("UNIQUE");
639 if !nulls_distinct {
640 s.push_str(" NULLS NOT DISTINCT");
641 }
642 s.push_str(&format!(" ({})", render_idents(columns)));
643 if !include.is_empty() {
644 s.push_str(&format!(" INCLUDE ({})", render_idents(include)));
645 }
646 s
647 }
648 ConstraintKind::ForeignKey(fk) => render_fk(fk),
649 ConstraintKind::Check {
650 expression,
651 no_inherit,
652 } => {
653 let mut s = format!("CHECK ({})", expression.canonical_text);
654 if *no_inherit {
655 s.push_str(" NO INHERIT");
656 }
657 s
658 }
659 }
660}
661
662fn render_fk(fk: &ForeignKey) -> String {
663 let mut s = format!(
664 "FOREIGN KEY ({}) REFERENCES {} ({})",
665 render_idents(&fk.columns),
666 fk.referenced_table.render_sql(),
667 render_idents(&fk.referenced_columns),
668 );
669 if !matches!(fk.match_type, FkMatchType::Simple) {
670 s.push_str(" MATCH ");
671 s.push_str(match fk.match_type {
672 FkMatchType::Simple => "SIMPLE",
673 FkMatchType::Full => "FULL",
674 });
675 }
676 if !matches!(fk.on_update, ReferentialAction::NoAction) {
677 s.push_str(" ON UPDATE ");
678 s.push_str(&referential_action(&fk.on_update));
679 }
680 if !matches!(fk.on_delete, ReferentialAction::NoAction) {
681 s.push_str(" ON DELETE ");
682 s.push_str(&referential_action(&fk.on_delete));
683 }
684 s
685}
686
687fn referential_action(a: &ReferentialAction) -> String {
688 match a {
689 ReferentialAction::NoAction => "NO ACTION".into(),
690 ReferentialAction::Restrict => "RESTRICT".into(),
691 ReferentialAction::Cascade => "CASCADE".into(),
692 ReferentialAction::SetNull(cols) => {
693 if cols.is_empty() {
694 "SET NULL".into()
695 } else {
696 format!("SET NULL ({})", render_idents(cols))
697 }
698 }
699 ReferentialAction::SetDefault(cols) => {
700 if cols.is_empty() {
701 "SET DEFAULT".into()
702 } else {
703 format!("SET DEFAULT ({})", render_idents(cols))
704 }
705 }
706 }
707}
708
709fn render_index_columns(cols: &[IndexColumn]) -> String {
710 let mut parts = Vec::with_capacity(cols.len());
711 for c in cols {
712 let mut s = match &c.expr {
713 IndexColumnExpr::Column(id) => id.render_sql(),
714 IndexColumnExpr::Expression(e) => format!("({})", e.canonical_text),
715 };
716 if let Some(coll) = &c.collation {
717 s.push_str(" COLLATE ");
718 s.push_str(&coll.render_sql());
719 }
720 if let Some(opc) = &c.opclass {
721 s.push(' ');
722 s.push_str(&opc.render_sql());
723 }
724 match c.sort_order {
725 SortOrder::Asc => {} SortOrder::Desc => s.push_str(" DESC"),
727 }
728 match c.nulls_order {
729 NullsOrder::NullsFirst => s.push_str(" NULLS FIRST"),
730 NullsOrder::NullsLast => {} }
732 parts.push(s);
733 }
734 parts.join(", ")
735}
736
737const fn index_method(m: IndexMethod) -> &'static str {
738 match m {
739 IndexMethod::BTree => "btree",
740 IndexMethod::Hash => "hash",
741 IndexMethod::Gin => "gin",
742 IndexMethod::Gist => "gist",
743 IndexMethod::Brin => "brin",
744 IndexMethod::Spgist => "spgist",
745 }
746}
747
748const fn identity_kind(k: IdentityKind) -> &'static str {
749 match k {
750 IdentityKind::Always => "ALWAYS",
751 IdentityKind::ByDefault => "BY DEFAULT",
752 }
753}
754
755const fn generated_kind(k: GeneratedKind) -> &'static str {
756 match k {
757 GeneratedKind::Stored => "STORED",
758 }
759}
760
761fn render_sequence_options(o: &SequenceOptions) -> String {
762 let defaults = SequenceOptions {
764 start: 1,
765 increment: 1,
766 min_value: None,
767 max_value: None,
768 cache: 1,
769 cycle: false,
770 };
771 if o == &defaults {
772 return String::new();
773 }
774 let mut parts: Vec<String> = Vec::new();
775 if o.start != defaults.start {
776 parts.push(format!("START WITH {}", o.start));
777 }
778 if o.increment != defaults.increment {
779 parts.push(format!("INCREMENT BY {}", o.increment));
780 }
781 if let Some(min) = o.min_value {
782 parts.push(format!("MINVALUE {min}"));
783 }
784 if let Some(max) = o.max_value {
785 parts.push(format!("MAXVALUE {max}"));
786 }
787 if o.cache != defaults.cache {
788 parts.push(format!("CACHE {}", o.cache));
789 }
790 if o.cycle {
791 parts.push("CYCLE".into());
792 }
793 if parts.is_empty() {
794 String::new()
795 } else {
796 format!(" ({})", parts.join(" "))
797 }
798}
799
800fn render_default_expr(d: &DefaultExpr) -> String {
801 match d {
802 DefaultExpr::Literal(LiteralValue::Bool(b)) => {
803 if *b {
804 "true".into()
805 } else {
806 "false".into()
807 }
808 }
809 DefaultExpr::Literal(LiteralValue::Integer(i)) => i.to_string(),
810 DefaultExpr::Literal(LiteralValue::Float(f)) => f.to_string(),
811 DefaultExpr::Literal(LiteralValue::Text(t)) => format!("'{}'", t.replace('\'', "''")),
812 DefaultExpr::Literal(LiteralValue::Bytea(b)) => format!("'\\x{}'", hex(b)),
813 DefaultExpr::Literal(LiteralValue::Null) => "NULL".into(),
814 DefaultExpr::Sequence(q) => format!("nextval('{}')", q.render_sql()),
815 DefaultExpr::Expr(e) => e.canonical_text.clone(),
816 }
817}
818
819fn hex(bytes: &[u8]) -> String {
820 use std::fmt::Write as _;
821 let mut s = String::with_capacity(bytes.len() * 2);
822 for b in bytes {
823 let _ = write!(s, "{b:02x}");
824 }
825 s
826}
827
828fn render_idents(v: &[Identifier]) -> String {
829 let mut s = String::new();
830 for (i, id) in v.iter().enumerate() {
831 if i > 0 {
832 s.push_str(", ");
833 }
834 s.push_str(&id.render_sql());
835 }
836 s
837}
838
839fn render_owner(o: &SequenceOwner) -> String {
840 format!("{}.{}", o.table.render_sql(), o.column.render_sql())
841}
842
843fn render_comment(comment: Option<&str>) -> String {
844 match comment {
845 Some(t) => format!("'{}'", t.replace('\'', "''")),
846 None => "NULL".into(),
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853 use crate::identifier::Identifier;
854 use crate::ir::column_type::ColumnType;
855 use crate::ir::partition::{
856 BoundDatum, PartitionBounds, PartitionBy, PartitionColumn, PartitionColumnKind,
857 PartitionOf, PartitionStrategy,
858 };
859
860 fn id(s: &str) -> Identifier {
861 Identifier::from_unquoted(s).unwrap()
862 }
863
864 fn qn(schema: &str, name: &str) -> QualifiedName {
865 QualifiedName::new(id(schema), id(name))
866 }
867
868 fn simple_col(name: &str) -> Column {
869 Column {
870 name: id(name),
871 ty: ColumnType::Text,
872 nullable: true,
873 collation: None,
874 default: None,
875 identity: None,
876 generated: None,
877 storage: None,
878 compression: None,
879 comment: None,
880 }
881 }
882
883 fn lit(s: &str) -> crate::ir::default_expr::NormalizedExpr {
884 crate::ir::default_expr::NormalizedExpr::from_text(s)
885 }
886
887 fn empty_table(qname: QualifiedName) -> Table {
888 Table {
889 qname,
890 columns: vec![],
891 constraints: vec![],
892 partition_by: None,
893 partition_of: None,
894 comment: None,
895 owner: None,
896 grants: vec![],
897 rls_enabled: false,
898 rls_forced: false,
899 policies: vec![],
900 storage: crate::ir::reloptions::TableStorageOptions::default(),
901 access_method: None,
902 tablespace: None,
903 }
904 }
905
906 #[test]
907 fn partitioned_parent_includes_partition_by() {
908 let mut t = empty_table(qn("app", "orders"));
909 t.columns = vec![simple_col("region")];
910 t.partition_by = Some(PartitionBy {
911 strategy: PartitionStrategy::List,
912 columns: vec![PartitionColumn {
913 kind: PartitionColumnKind::Column(id("region")),
914 collation: None,
915 opclass: None,
916 }],
917 });
918 let sql = create_table(&t);
919 assert!(
920 sql.ends_with("PARTITION BY LIST (region);"),
921 "expected PARTITION BY LIST (region); at end, got: {sql}"
922 );
923 assert!(
924 sql.contains("region text"),
925 "expected column def, got: {sql}"
926 );
927 }
928
929 #[test]
930 fn child_partition_emits_partition_of_no_column_list() {
931 let mut t = empty_table(qn("app", "orders_2024"));
932 t.partition_of = Some(PartitionOf {
933 parent: qn("app", "orders"),
934 bounds: PartitionBounds::Range {
935 from: vec![BoundDatum::Literal(lit("'2024-01-01'"))],
936 to: vec![BoundDatum::Literal(lit("'2025-01-01'"))],
937 },
938 });
939 let sql = create_table(&t);
940 assert_eq!(
941 sql,
942 "CREATE TABLE app.orders_2024 PARTITION OF app.orders FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');"
943 );
944 assert!(
946 !sql.contains('(') || sql.contains("FOR VALUES FROM ("),
947 "should not contain a column list opening paren, got: {sql}"
948 );
949 }
950
951 #[test]
956 fn renders_set_storage_external() {
957 let s = alter_column_set_storage(&qn("app", "t"), &id("c"), StorageKind::External);
958 assert_eq!(
959 s.trim(),
960 "ALTER TABLE app.t ALTER COLUMN c SET STORAGE EXTERNAL;"
961 );
962 }
963
964 #[test]
965 fn renders_all_storage_variants() {
966 for (storage, expected) in [
967 (StorageKind::Plain, "PLAIN"),
968 (StorageKind::External, "EXTERNAL"),
969 (StorageKind::Extended, "EXTENDED"),
970 (StorageKind::Main, "MAIN"),
971 ] {
972 let s = alter_column_set_storage(&qn("app", "t"), &id("c"), storage);
973 assert!(s.contains(&format!("SET STORAGE {expected}")), "got: {s}");
974 }
975 }
976
977 #[test]
978 fn renders_set_compression_lz4() {
979 let s = alter_column_set_compression(&qn("app", "t"), &id("c"), Some(Compression::Lz4));
980 assert_eq!(
981 s.trim(),
982 "ALTER TABLE app.t ALTER COLUMN c SET COMPRESSION lz4;"
983 );
984 }
985
986 #[test]
987 fn renders_set_compression_pglz() {
988 let s = alter_column_set_compression(&qn("app", "t"), &id("c"), Some(Compression::Pglz));
989 assert_eq!(
990 s.trim(),
991 "ALTER TABLE app.t ALTER COLUMN c SET COMPRESSION pglz;"
992 );
993 }
994
995 #[test]
996 fn renders_set_compression_default() {
997 let s = alter_column_set_compression(&qn("app", "t"), &id("c"), None);
998 assert_eq!(
999 s.trim(),
1000 "ALTER TABLE app.t ALTER COLUMN c SET COMPRESSION DEFAULT;"
1001 );
1002 }
1003
1004 #[test]
1010 fn column_def_never_renders_inline_storage() {
1011 for storage in [
1015 StorageKind::Plain,
1016 StorageKind::External,
1017 StorageKind::Extended,
1018 StorageKind::Main,
1019 ] {
1020 let mut c = simple_col("doc");
1021 c.storage = Some(storage);
1022 let s = column_def(&c);
1023 assert!(
1024 !s.contains("STORAGE"),
1025 "column_def must not emit inline STORAGE (PG 14/15 do not support it), got: {s}"
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn column_def_renders_compression_lz4() {
1032 let mut c = simple_col("blob");
1033 c.compression = Some(Compression::Lz4);
1034 let s = column_def(&c);
1035 assert!(s.contains("COMPRESSION lz4"), "got: {s}");
1036 }
1037
1038 #[test]
1039 fn column_def_renders_no_clauses_when_none() {
1040 let c = simple_col("plain");
1041 let s = column_def(&c);
1042 assert!(!s.contains("STORAGE"), "got: {s}");
1043 assert!(!s.contains("COMPRESSION"), "got: {s}");
1044 }
1045
1046 fn simple_index() -> Index {
1051 use crate::ir::index::{IndexColumn, IndexColumnExpr, IndexParent, NullsOrder, SortOrder};
1052 Index {
1053 qname: qn("app", "idx_test"),
1054 on: IndexParent::Table(qn("app", "t")),
1055 method: crate::ir::index::IndexMethod::BTree,
1056 columns: vec![IndexColumn {
1057 expr: IndexColumnExpr::Column(id("col1")),
1058 collation: None,
1059 opclass: None,
1060 sort_order: SortOrder::Asc,
1061 nulls_order: NullsOrder::NullsLast,
1062 }],
1063 include: vec![],
1064 unique: false,
1065 nulls_not_distinct: false,
1066 predicate: None,
1067 tablespace: None,
1068 comment: None,
1069 storage: crate::ir::reloptions::IndexStorageOptions::default(),
1070 }
1071 }
1072
1073 #[test]
1074 fn create_index_emits_with_clause_when_fillfactor_set() {
1075 let mut idx = simple_index();
1076 idx.storage = crate::ir::reloptions::IndexStorageOptions {
1077 fillfactor: Some(80),
1078 ..Default::default()
1079 };
1080 let sql = create_index(&idx, false);
1081 assert!(
1082 sql.contains("WITH (fillfactor = 80)"),
1083 "expected WITH (fillfactor = 80) in SQL, got: {sql}"
1084 );
1085 }
1086
1087 #[test]
1088 fn create_index_no_with_clause_for_default_storage() {
1089 let idx = simple_index();
1090 let sql = create_index(&idx, false);
1091 assert!(
1092 !sql.contains("WITH ("),
1093 "expected no WITH clause for default storage, got: {sql}"
1094 );
1095 }
1096
1097 #[test]
1102 fn create_table_with_access_method_emits_using_clause() {
1103 let mut t = empty_table(qn("app", "events"));
1104 t.columns = vec![simple_col("id")];
1105 t.access_method = Some(Identifier::from_unquoted("columnar").unwrap());
1106 let sql = create_table(&t);
1107 let paren_close = sql.find(')').expect("closing paren");
1109 let using_pos = sql
1110 .find(" USING columnar")
1111 .expect("USING columnar not found in SQL");
1112 assert!(
1113 using_pos > paren_close,
1114 "USING must come after the closing ')' of the column list; got: {sql}"
1115 );
1116 assert!(sql.ends_with(';'), "SQL must end with ';', got: {sql}");
1117 }
1118
1119 #[test]
1120 fn create_table_without_access_method_emits_no_using_clause() {
1121 let mut t = empty_table(qn("app", "events"));
1122 t.columns = vec![simple_col("id")];
1123 let sql = create_table(&t);
1125 assert!(
1126 !sql.contains("USING"),
1127 "expected no USING clause when access_method is None, got: {sql}"
1128 );
1129 }
1130
1131 #[test]
1134 fn alter_table_set_tablespace_with_name() {
1135 let qname = qn("app", "orders");
1136 let ts = id("fast");
1137 let sql = alter_table_set_tablespace(&qname, Some(&ts));
1138 assert_eq!(sql, "ALTER TABLE app.orders SET TABLESPACE fast;");
1139 }
1140
1141 #[test]
1142 fn alter_table_set_tablespace_none_uses_pg_default() {
1143 let qname = qn("app", "orders");
1144 let sql = alter_table_set_tablespace(&qname, None);
1145 assert_eq!(sql, "ALTER TABLE app.orders SET TABLESPACE pg_default;");
1146 }
1147
1148 #[test]
1149 fn create_table_with_tablespace_appends_clause() {
1150 let mut t = empty_table(qn("app", "events"));
1151 t.columns = vec![simple_col("id")];
1152 t.tablespace = Some(id("fast_ssd"));
1153 let sql = create_table(&t);
1154 assert!(
1155 sql.ends_with(" TABLESPACE fast_ssd;"),
1156 "expected TABLESPACE clause at end, got: {sql}"
1157 );
1158 }
1159
1160 #[test]
1161 fn create_table_without_tablespace_omits_clause() {
1162 let mut t = empty_table(qn("app", "events"));
1163 t.columns = vec![simple_col("id")];
1164 let sql = create_table(&t);
1166 assert!(
1167 !sql.contains("TABLESPACE"),
1168 "expected no TABLESPACE clause when tablespace is None, got: {sql}"
1169 );
1170 }
1171}