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