1use crate::identifier::{Identifier, QualifiedName};
12use crate::ir::column::{
13 Column, Compression, GeneratedKind, Identity, IdentityKind, SequenceOptions, StorageKind,
14};
15use crate::ir::constraint::{
16 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
17};
18use crate::ir::default_expr::{DefaultExpr, LiteralValue};
19use crate::ir::index::{Index, IndexColumn, IndexColumnExpr, IndexMethod, NullsOrder, SortOrder};
20use crate::ir::schema::Schema;
21use crate::ir::sequence::{Sequence, SequenceOwner};
22use crate::ir::table::Table;
23
24pub fn create_schema(s: &Schema) -> String {
30 format!("CREATE SCHEMA {};", s.name.render_sql())
31}
32
33pub fn drop_schema(name: &Identifier) -> String {
35 format!("DROP SCHEMA {};", name.render_sql())
36}
37
38pub fn comment_on_schema(name: &Identifier, comment: Option<&str>) -> String {
40 format!(
41 "COMMENT ON SCHEMA {} IS {};",
42 name.render_sql(),
43 render_comment(comment),
44 )
45}
46
47pub fn create_table(t: &Table) -> String {
54 let mut s = String::new();
55 s.push_str("CREATE TABLE ");
56 s.push_str(&t.qname.render_sql());
57
58 if let Some(po) = &t.partition_of {
59 s.push(' ');
62 s.push_str(&crate::plan::rewrite::partitions::render_partition_of(po));
63 } else {
64 s.push_str(" (");
66 let mut first = true;
67 for col in &t.columns {
68 if !first {
69 s.push(',');
70 }
71 s.push('\n');
72 s.push_str(" ");
73 s.push_str(&column_def(col));
74 first = false;
75 }
76 for c in &t.constraints {
77 if !first {
78 s.push(',');
79 }
80 s.push('\n');
81 s.push_str(" ");
82 s.push_str(&inline_constraint(c));
83 first = false;
84 }
85 if !first {
86 s.push('\n');
87 }
88 s.push(')');
89 }
90
91 if let Some(pb) = &t.partition_by {
92 s.push(' ');
93 s.push_str(&crate::plan::rewrite::partitions::render_partition_by(pb));
94 }
95
96 s.push(';');
97 s
98}
99
100pub fn drop_table(qname: &QualifiedName) -> String {
102 format!("DROP TABLE {};", qname.render_sql())
103}
104
105pub fn comment_on_table(qname: &QualifiedName, comment: Option<&str>) -> String {
107 format!(
108 "COMMENT ON TABLE {} IS {};",
109 qname.render_sql(),
110 render_comment(comment),
111 )
112}
113
114pub fn create_index(idx: &Index, concurrently: bool) -> String {
116 let mut s = String::from("CREATE ");
117 if idx.unique {
118 s.push_str("UNIQUE ");
119 }
120 s.push_str("INDEX ");
121 if concurrently {
122 s.push_str("CONCURRENTLY ");
123 }
124 s.push_str(&idx.qname.name.render_sql());
125 s.push_str(" ON ");
126 s.push_str(&idx.on.qname().render_sql());
127 s.push_str(" USING ");
128 s.push_str(index_method(idx.method));
129 s.push_str(" (");
130 s.push_str(&render_index_columns(&idx.columns));
131 s.push(')');
132 if !idx.include.is_empty() {
133 s.push_str(" INCLUDE (");
134 s.push_str(&render_idents(&idx.include));
135 s.push(')');
136 }
137 if idx.unique && idx.nulls_not_distinct {
138 s.push_str(" NULLS NOT DISTINCT");
139 }
140 if let Some(ts) = &idx.tablespace {
141 s.push_str(" TABLESPACE ");
142 s.push_str(&ts.render_sql());
143 }
144 if let Some(pred) = &idx.predicate {
145 s.push_str(" WHERE ");
146 s.push_str(&pred.canonical_text);
147 }
148 s.push(';');
149 s
150}
151
152pub fn drop_index(qname: &QualifiedName, concurrently: bool) -> String {
154 if concurrently {
155 format!("DROP INDEX CONCURRENTLY {};", qname.render_sql())
156 } else {
157 format!("DROP INDEX {};", qname.render_sql())
158 }
159}
160
161pub fn create_sequence(s: &Sequence) -> String {
163 let mut out = String::from("CREATE SEQUENCE ");
164 out.push_str(&s.qname.render_sql());
165 out.push_str(" AS ");
166 out.push_str(&s.data_type.render_sql());
167 out.push_str(&format!(" INCREMENT BY {}", s.increment));
168 if let Some(min) = s.min_value {
169 out.push_str(&format!(" MINVALUE {min}"));
170 } else {
171 out.push_str(" NO MINVALUE");
172 }
173 if let Some(max) = s.max_value {
174 out.push_str(&format!(" MAXVALUE {max}"));
175 } else {
176 out.push_str(" NO MAXVALUE");
177 }
178 out.push_str(&format!(" START WITH {}", s.start));
179 out.push_str(&format!(" CACHE {}", s.cache));
180 if s.cycle {
181 out.push_str(" CYCLE");
182 } else {
183 out.push_str(" NO CYCLE");
184 }
185 if let Some(owner) = &s.owned_by {
186 out.push_str(" OWNED BY ");
187 out.push_str(&render_owner(owner));
188 }
189 out.push(';');
190 out
191}
192
193pub fn drop_sequence(qname: &QualifiedName) -> String {
195 format!("DROP SEQUENCE {};", qname.render_sql())
196}
197
198pub fn alter_table_add_column(qname: &QualifiedName, c: &Column) -> String {
204 format!(
205 "ALTER TABLE {} ADD COLUMN {};",
206 qname.render_sql(),
207 column_def(c),
208 )
209}
210
211pub fn alter_table_drop_column(qname: &QualifiedName, name: &Identifier) -> String {
213 format!(
214 "ALTER TABLE {} DROP COLUMN {};",
215 qname.render_sql(),
216 name.render_sql(),
217 )
218}
219
220pub fn alter_column_type(
222 qname: &QualifiedName,
223 name: &Identifier,
224 to: &crate::ir::column_type::ColumnType,
225 using: Option<&crate::ir::default_expr::NormalizedExpr>,
226) -> String {
227 let mut s = format!(
228 "ALTER TABLE {} ALTER COLUMN {} TYPE {}",
229 qname.render_sql(),
230 name.render_sql(),
231 to.render_sql(),
232 );
233 if let Some(u) = using {
234 s.push_str(" USING ");
235 s.push_str(&u.canonical_text);
236 }
237 s.push(';');
238 s
239}
240
241pub fn alter_column_set_nullable(
243 qname: &QualifiedName,
244 name: &Identifier,
245 nullable: bool,
246) -> String {
247 let action = if nullable {
248 "DROP NOT NULL"
249 } else {
250 "SET NOT NULL"
251 };
252 format!(
253 "ALTER TABLE {} ALTER COLUMN {} {};",
254 qname.render_sql(),
255 name.render_sql(),
256 action,
257 )
258}
259
260pub fn alter_column_set_default(
262 qname: &QualifiedName,
263 name: &Identifier,
264 default: Option<&DefaultExpr>,
265) -> String {
266 match default {
267 Some(d) => format!(
268 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
269 qname.render_sql(),
270 name.render_sql(),
271 render_default_expr(d),
272 ),
273 None => format!(
274 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
275 qname.render_sql(),
276 name.render_sql(),
277 ),
278 }
279}
280
281pub fn alter_column_set_identity(
283 qname: &QualifiedName,
284 name: &Identifier,
285 identity: Option<&Identity>,
286) -> String {
287 match identity {
288 Some(id) => format!(
289 "ALTER TABLE {} ALTER COLUMN {} ADD GENERATED {} AS IDENTITY{};",
290 qname.render_sql(),
291 name.render_sql(),
292 identity_kind(id.kind),
293 render_sequence_options(&id.sequence),
294 ),
295 None => format!(
296 "ALTER TABLE {} ALTER COLUMN {} DROP IDENTITY;",
297 qname.render_sql(),
298 name.render_sql(),
299 ),
300 }
301}
302
303pub fn alter_column_set_generated(
311 qname: &QualifiedName,
312 name: &Identifier,
313 generated: Option<&crate::ir::column::Generated>,
314) -> String {
315 match generated {
316 None => format!(
317 "ALTER TABLE {} ALTER COLUMN {} DROP EXPRESSION;",
318 qname.render_sql(),
319 name.render_sql(),
320 ),
321 Some(g) => {
322 format!(
328 "ALTER TABLE {} ALTER COLUMN {} SET EXPRESSION AS ({}) {};",
329 qname.render_sql(),
330 name.render_sql(),
331 g.expression.canonical_text,
332 generated_kind(g.kind),
333 )
334 }
335 }
336}
337
338pub fn alter_column_set_storage(
340 qname: &QualifiedName,
341 name: &Identifier,
342 storage: StorageKind,
343) -> String {
344 let kw = match storage {
345 StorageKind::Plain => "PLAIN",
346 StorageKind::External => "EXTERNAL",
347 StorageKind::Extended => "EXTENDED",
348 StorageKind::Main => "MAIN",
349 };
350 format!(
351 "ALTER TABLE {} ALTER COLUMN {} SET STORAGE {};",
352 qname.render_sql(),
353 name.render_sql(),
354 kw,
355 )
356}
357
358pub fn alter_column_set_compression(
360 qname: &QualifiedName,
361 name: &Identifier,
362 compression: Option<Compression>,
363) -> String {
364 let kw = match compression {
365 Some(Compression::Pglz) => "pglz",
366 Some(Compression::Lz4) => "lz4",
367 None => "DEFAULT",
368 };
369 format!(
370 "ALTER TABLE {} ALTER COLUMN {} SET COMPRESSION {};",
371 qname.render_sql(),
372 name.render_sql(),
373 kw,
374 )
375}
376
377pub fn comment_on_column(qname: &QualifiedName, col: &Identifier, comment: Option<&str>) -> String {
379 format!(
380 "COMMENT ON COLUMN {}.{} IS {};",
381 qname.render_sql(),
382 col.render_sql(),
383 render_comment(comment),
384 )
385}
386
387pub fn alter_table_add_constraint(qname: &QualifiedName, c: &Constraint) -> String {
389 format!(
390 "ALTER TABLE {} ADD {};",
391 qname.render_sql(),
392 constraint_def_with_name(c),
393 )
394}
395
396pub fn alter_table_add_constraint_not_valid(qname: &QualifiedName, c: &Constraint) -> String {
398 format!(
399 "ALTER TABLE {} ADD {} NOT VALID;",
400 qname.render_sql(),
401 constraint_def_with_name(c),
402 )
403}
404
405pub fn alter_table_validate_constraint(qname: &QualifiedName, cname: &Identifier) -> String {
407 format!(
408 "ALTER TABLE {} VALIDATE CONSTRAINT {};",
409 qname.render_sql(),
410 cname.render_sql(),
411 )
412}
413
414pub fn alter_table_drop_constraint(qname: &QualifiedName, cname: &Identifier) -> String {
416 format!(
417 "ALTER TABLE {} DROP CONSTRAINT {};",
418 qname.render_sql(),
419 cname.render_sql(),
420 )
421}
422
423pub fn comment_on_constraint(
425 qname: &QualifiedName,
426 cname: &Identifier,
427 comment: Option<&str>,
428) -> String {
429 format!(
430 "COMMENT ON CONSTRAINT {} ON {} IS {};",
431 cname.render_sql(),
432 qname.render_sql(),
433 render_comment(comment),
434 )
435}
436
437pub fn comment_on_index(qname: &QualifiedName, comment: Option<&str>) -> String {
439 format!(
440 "COMMENT ON INDEX {} IS {};",
441 qname.render_sql(),
442 render_comment(comment),
443 )
444}
445
446pub fn comment_on_sequence(qname: &QualifiedName, comment: Option<&str>) -> String {
448 format!(
449 "COMMENT ON SEQUENCE {} IS {};",
450 qname.render_sql(),
451 render_comment(comment),
452 )
453}
454
455pub fn alter_sequence_increment(qname: &QualifiedName, n: i64) -> String {
461 format!("ALTER SEQUENCE {} INCREMENT BY {n};", qname.render_sql())
462}
463
464pub fn alter_sequence_min_value(qname: &QualifiedName, v: Option<i64>) -> String {
466 match v {
467 Some(n) => format!("ALTER SEQUENCE {} MINVALUE {n};", qname.render_sql()),
468 None => format!("ALTER SEQUENCE {} NO MINVALUE;", qname.render_sql()),
469 }
470}
471
472pub fn alter_sequence_max_value(qname: &QualifiedName, v: Option<i64>) -> String {
474 match v {
475 Some(n) => format!("ALTER SEQUENCE {} MAXVALUE {n};", qname.render_sql()),
476 None => format!("ALTER SEQUENCE {} NO MAXVALUE;", qname.render_sql()),
477 }
478}
479
480pub fn alter_sequence_cache(qname: &QualifiedName, n: i64) -> String {
482 format!("ALTER SEQUENCE {} CACHE {n};", qname.render_sql())
483}
484
485pub fn alter_sequence_cycle(qname: &QualifiedName, cycle: bool) -> String {
487 let kw = if cycle { "CYCLE" } else { "NO CYCLE" };
488 format!("ALTER SEQUENCE {} {kw};", qname.render_sql())
489}
490
491pub fn alter_sequence_data_type(
493 qname: &QualifiedName,
494 ty: &crate::ir::column_type::ColumnType,
495) -> String {
496 format!(
497 "ALTER SEQUENCE {} AS {};",
498 qname.render_sql(),
499 ty.render_sql(),
500 )
501}
502
503pub fn alter_sequence_owned_by(qname: &QualifiedName, owner: Option<&SequenceOwner>) -> String {
505 match owner {
506 Some(o) => format!(
507 "ALTER SEQUENCE {} OWNED BY {};",
508 qname.render_sql(),
509 render_owner(o),
510 ),
511 None => format!("ALTER SEQUENCE {} OWNED BY NONE;", qname.render_sql()),
512 }
513}
514
515pub fn column_def(c: &Column) -> String {
521 let mut s = String::new();
522 s.push_str(&c.name.render_sql());
523 s.push(' ');
524 s.push_str(&c.ty.render_sql());
525 if let Some(coll) = &c.collation {
526 s.push_str(" COLLATE ");
527 s.push_str(&coll.render_sql());
528 }
529 if let Some(storage) = c.storage {
536 let kw = match storage {
537 StorageKind::Plain => "PLAIN",
538 StorageKind::External => "EXTERNAL",
539 StorageKind::Extended => "EXTENDED",
540 StorageKind::Main => "MAIN",
541 };
542 s.push_str(" STORAGE ");
543 s.push_str(kw);
544 }
545 if let Some(compression) = c.compression {
546 let kw = match compression {
547 Compression::Pglz => "pglz",
548 Compression::Lz4 => "lz4",
549 };
550 s.push_str(" COMPRESSION ");
551 s.push_str(kw);
552 }
553 if !c.nullable {
554 s.push_str(" NOT NULL");
555 }
556 if let Some(d) = &c.default {
557 s.push_str(" DEFAULT ");
558 s.push_str(&render_default_expr(d));
559 }
560 if let Some(id) = &c.identity {
561 s.push_str(" GENERATED ");
562 s.push_str(identity_kind(id.kind));
563 s.push_str(" AS IDENTITY");
564 s.push_str(&render_sequence_options(&id.sequence));
565 }
566 if let Some(g) = &c.generated {
567 s.push_str(" GENERATED ALWAYS AS (");
568 s.push_str(&g.expression.canonical_text);
569 s.push_str(") ");
570 s.push_str(generated_kind(g.kind));
571 }
572 s
573}
574
575fn inline_constraint(c: &Constraint) -> String {
577 constraint_def_with_name(c)
578}
579
580pub fn constraint_def_with_name(c: &Constraint) -> String {
582 let mut s = format!(
583 "CONSTRAINT {} {}",
584 c.qname.name.render_sql(),
585 constraint_body(&c.kind)
586 );
587 match c.deferrable {
588 Deferrable::NotDeferrable => {}
589 Deferrable::Deferrable {
590 initially_deferred: true,
591 } => s.push_str(" DEFERRABLE INITIALLY DEFERRED"),
592 Deferrable::Deferrable {
593 initially_deferred: false,
594 } => s.push_str(" DEFERRABLE INITIALLY IMMEDIATE"),
595 }
596 s
597}
598
599fn constraint_body(k: &ConstraintKind) -> String {
600 match k {
601 ConstraintKind::PrimaryKey { columns, include } => {
602 let mut s = format!("PRIMARY KEY ({})", render_idents(columns));
603 if !include.is_empty() {
604 s.push_str(&format!(" INCLUDE ({})", render_idents(include)));
605 }
606 s
607 }
608 ConstraintKind::Unique {
609 columns,
610 include,
611 nulls_distinct,
612 } => {
613 let mut s = String::from("UNIQUE");
614 if !nulls_distinct {
615 s.push_str(" NULLS NOT DISTINCT");
616 }
617 s.push_str(&format!(" ({})", render_idents(columns)));
618 if !include.is_empty() {
619 s.push_str(&format!(" INCLUDE ({})", render_idents(include)));
620 }
621 s
622 }
623 ConstraintKind::ForeignKey(fk) => render_fk(fk),
624 ConstraintKind::Check {
625 expression,
626 no_inherit,
627 } => {
628 let mut s = format!("CHECK ({})", expression.canonical_text);
629 if *no_inherit {
630 s.push_str(" NO INHERIT");
631 }
632 s
633 }
634 }
635}
636
637fn render_fk(fk: &ForeignKey) -> String {
638 let mut s = format!(
639 "FOREIGN KEY ({}) REFERENCES {} ({})",
640 render_idents(&fk.columns),
641 fk.referenced_table.render_sql(),
642 render_idents(&fk.referenced_columns),
643 );
644 if !matches!(fk.match_type, FkMatchType::Simple) {
645 s.push_str(" MATCH ");
646 s.push_str(match fk.match_type {
647 FkMatchType::Simple => "SIMPLE",
648 FkMatchType::Full => "FULL",
649 });
650 }
651 if !matches!(fk.on_update, ReferentialAction::NoAction) {
652 s.push_str(" ON UPDATE ");
653 s.push_str(&referential_action(&fk.on_update));
654 }
655 if !matches!(fk.on_delete, ReferentialAction::NoAction) {
656 s.push_str(" ON DELETE ");
657 s.push_str(&referential_action(&fk.on_delete));
658 }
659 s
660}
661
662fn referential_action(a: &ReferentialAction) -> String {
663 match a {
664 ReferentialAction::NoAction => "NO ACTION".into(),
665 ReferentialAction::Restrict => "RESTRICT".into(),
666 ReferentialAction::Cascade => "CASCADE".into(),
667 ReferentialAction::SetNull(cols) => {
668 if cols.is_empty() {
669 "SET NULL".into()
670 } else {
671 format!("SET NULL ({})", render_idents(cols))
672 }
673 }
674 ReferentialAction::SetDefault(cols) => {
675 if cols.is_empty() {
676 "SET DEFAULT".into()
677 } else {
678 format!("SET DEFAULT ({})", render_idents(cols))
679 }
680 }
681 }
682}
683
684fn render_index_columns(cols: &[IndexColumn]) -> String {
685 let mut parts = Vec::with_capacity(cols.len());
686 for c in cols {
687 let mut s = match &c.expr {
688 IndexColumnExpr::Column(id) => id.render_sql(),
689 IndexColumnExpr::Expression(e) => format!("({})", e.canonical_text),
690 };
691 if let Some(coll) = &c.collation {
692 s.push_str(" COLLATE ");
693 s.push_str(&coll.render_sql());
694 }
695 if let Some(opc) = &c.opclass {
696 s.push(' ');
697 s.push_str(&opc.render_sql());
698 }
699 match c.sort_order {
700 SortOrder::Asc => {} SortOrder::Desc => s.push_str(" DESC"),
702 }
703 match c.nulls_order {
704 NullsOrder::NullsFirst => s.push_str(" NULLS FIRST"),
705 NullsOrder::NullsLast => {} }
707 parts.push(s);
708 }
709 parts.join(", ")
710}
711
712const fn index_method(m: IndexMethod) -> &'static str {
713 match m {
714 IndexMethod::BTree => "btree",
715 IndexMethod::Hash => "hash",
716 IndexMethod::Gin => "gin",
717 IndexMethod::Gist => "gist",
718 IndexMethod::Brin => "brin",
719 IndexMethod::Spgist => "spgist",
720 }
721}
722
723const fn identity_kind(k: IdentityKind) -> &'static str {
724 match k {
725 IdentityKind::Always => "ALWAYS",
726 IdentityKind::ByDefault => "BY DEFAULT",
727 }
728}
729
730const fn generated_kind(k: GeneratedKind) -> &'static str {
731 match k {
732 GeneratedKind::Stored => "STORED",
733 }
734}
735
736fn render_sequence_options(o: &SequenceOptions) -> String {
737 let defaults = SequenceOptions {
739 start: 1,
740 increment: 1,
741 min_value: None,
742 max_value: None,
743 cache: 1,
744 cycle: false,
745 };
746 if o == &defaults {
747 return String::new();
748 }
749 let mut parts: Vec<String> = Vec::new();
750 if o.start != defaults.start {
751 parts.push(format!("START WITH {}", o.start));
752 }
753 if o.increment != defaults.increment {
754 parts.push(format!("INCREMENT BY {}", o.increment));
755 }
756 if let Some(min) = o.min_value {
757 parts.push(format!("MINVALUE {min}"));
758 }
759 if let Some(max) = o.max_value {
760 parts.push(format!("MAXVALUE {max}"));
761 }
762 if o.cache != defaults.cache {
763 parts.push(format!("CACHE {}", o.cache));
764 }
765 if o.cycle {
766 parts.push("CYCLE".into());
767 }
768 if parts.is_empty() {
769 String::new()
770 } else {
771 format!(" ({})", parts.join(" "))
772 }
773}
774
775fn render_default_expr(d: &DefaultExpr) -> String {
776 match d {
777 DefaultExpr::Literal(LiteralValue::Bool(b)) => {
778 if *b {
779 "true".into()
780 } else {
781 "false".into()
782 }
783 }
784 DefaultExpr::Literal(LiteralValue::Integer(i)) => i.to_string(),
785 DefaultExpr::Literal(LiteralValue::Float(f)) => f.to_string(),
786 DefaultExpr::Literal(LiteralValue::Text(t)) => format!("'{}'", t.replace('\'', "''")),
787 DefaultExpr::Literal(LiteralValue::Bytea(b)) => format!("'\\x{}'", hex(b)),
788 DefaultExpr::Literal(LiteralValue::Null) => "NULL".into(),
789 DefaultExpr::Sequence(q) => format!("nextval('{}')", q.render_sql()),
790 DefaultExpr::Expr(e) => e.canonical_text.clone(),
791 }
792}
793
794fn hex(bytes: &[u8]) -> String {
795 use std::fmt::Write as _;
796 let mut s = String::with_capacity(bytes.len() * 2);
797 for b in bytes {
798 let _ = write!(s, "{b:02x}");
799 }
800 s
801}
802
803fn render_idents(v: &[Identifier]) -> String {
804 let mut s = String::new();
805 for (i, id) in v.iter().enumerate() {
806 if i > 0 {
807 s.push_str(", ");
808 }
809 s.push_str(&id.render_sql());
810 }
811 s
812}
813
814fn render_owner(o: &SequenceOwner) -> String {
815 format!("{}.{}", o.table.render_sql(), o.column.render_sql())
816}
817
818fn render_comment(comment: Option<&str>) -> String {
819 match comment {
820 Some(t) => format!("'{}'", t.replace('\'', "''")),
821 None => "NULL".into(),
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use crate::identifier::Identifier;
829 use crate::ir::column_type::ColumnType;
830 use crate::ir::partition::{
831 BoundDatum, PartitionBounds, PartitionBy, PartitionColumn, PartitionColumnKind,
832 PartitionOf, PartitionStrategy,
833 };
834
835 fn id(s: &str) -> Identifier {
836 Identifier::from_unquoted(s).unwrap()
837 }
838
839 fn qn(schema: &str, name: &str) -> QualifiedName {
840 QualifiedName::new(id(schema), id(name))
841 }
842
843 fn simple_col(name: &str) -> Column {
844 Column {
845 name: id(name),
846 ty: ColumnType::Text,
847 nullable: true,
848 collation: None,
849 default: None,
850 identity: None,
851 generated: None,
852 storage: None,
853 compression: None,
854 comment: None,
855 }
856 }
857
858 fn lit(s: &str) -> crate::ir::default_expr::NormalizedExpr {
859 crate::ir::default_expr::NormalizedExpr::from_text(s)
860 }
861
862 fn empty_table(qname: QualifiedName) -> Table {
863 Table {
864 qname,
865 columns: vec![],
866 constraints: vec![],
867 partition_by: None,
868 partition_of: None,
869 comment: None,
870 owner: None,
871 grants: vec![],
872 rls_enabled: false,
873 rls_forced: false,
874 policies: vec![],
875 storage: crate::ir::reloptions::TableStorageOptions::default(),
876 }
877 }
878
879 #[test]
880 fn partitioned_parent_includes_partition_by() {
881 let mut t = empty_table(qn("app", "orders"));
882 t.columns = vec![simple_col("region")];
883 t.partition_by = Some(PartitionBy {
884 strategy: PartitionStrategy::List,
885 columns: vec![PartitionColumn {
886 kind: PartitionColumnKind::Column(id("region")),
887 collation: None,
888 opclass: None,
889 }],
890 });
891 let sql = create_table(&t);
892 assert!(
893 sql.ends_with("PARTITION BY LIST (region);"),
894 "expected PARTITION BY LIST (region); at end, got: {sql}"
895 );
896 assert!(
897 sql.contains("region text"),
898 "expected column def, got: {sql}"
899 );
900 }
901
902 #[test]
903 fn child_partition_emits_partition_of_no_column_list() {
904 let mut t = empty_table(qn("app", "orders_2024"));
905 t.partition_of = Some(PartitionOf {
906 parent: qn("app", "orders"),
907 bounds: PartitionBounds::Range {
908 from: vec![BoundDatum::Literal(lit("'2024-01-01'"))],
909 to: vec![BoundDatum::Literal(lit("'2025-01-01'"))],
910 },
911 });
912 let sql = create_table(&t);
913 assert_eq!(
914 sql,
915 "CREATE TABLE app.orders_2024 PARTITION OF app.orders FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');"
916 );
917 assert!(
919 !sql.contains('(') || sql.contains("FOR VALUES FROM ("),
920 "should not contain a column list opening paren, got: {sql}"
921 );
922 }
923
924 #[test]
929 fn renders_set_storage_external() {
930 let s = alter_column_set_storage(&qn("app", "t"), &id("c"), StorageKind::External);
931 assert_eq!(
932 s.trim(),
933 "ALTER TABLE app.t ALTER COLUMN c SET STORAGE EXTERNAL;"
934 );
935 }
936
937 #[test]
938 fn renders_all_storage_variants() {
939 for (storage, expected) in [
940 (StorageKind::Plain, "PLAIN"),
941 (StorageKind::External, "EXTERNAL"),
942 (StorageKind::Extended, "EXTENDED"),
943 (StorageKind::Main, "MAIN"),
944 ] {
945 let s = alter_column_set_storage(&qn("app", "t"), &id("c"), storage);
946 assert!(s.contains(&format!("SET STORAGE {expected}")), "got: {s}");
947 }
948 }
949
950 #[test]
951 fn renders_set_compression_lz4() {
952 let s = alter_column_set_compression(&qn("app", "t"), &id("c"), Some(Compression::Lz4));
953 assert_eq!(
954 s.trim(),
955 "ALTER TABLE app.t ALTER COLUMN c SET COMPRESSION lz4;"
956 );
957 }
958
959 #[test]
960 fn renders_set_compression_pglz() {
961 let s = alter_column_set_compression(&qn("app", "t"), &id("c"), Some(Compression::Pglz));
962 assert_eq!(
963 s.trim(),
964 "ALTER TABLE app.t ALTER COLUMN c SET COMPRESSION pglz;"
965 );
966 }
967
968 #[test]
969 fn renders_set_compression_default() {
970 let s = alter_column_set_compression(&qn("app", "t"), &id("c"), None);
971 assert_eq!(
972 s.trim(),
973 "ALTER TABLE app.t ALTER COLUMN c SET COMPRESSION DEFAULT;"
974 );
975 }
976
977 #[test]
982 fn column_def_renders_storage_external() {
983 let mut c = simple_col("doc");
984 c.storage = Some(StorageKind::External);
985 let s = column_def(&c);
986 assert!(s.contains("STORAGE EXTERNAL"), "got: {s}");
987 }
988
989 #[test]
990 fn column_def_renders_compression_lz4() {
991 let mut c = simple_col("blob");
992 c.compression = Some(Compression::Lz4);
993 let s = column_def(&c);
994 assert!(s.contains("COMPRESSION lz4"), "got: {s}");
995 }
996
997 #[test]
998 fn column_def_renders_no_clauses_when_none() {
999 let c = simple_col("plain");
1000 let s = column_def(&c);
1001 assert!(!s.contains("STORAGE"), "got: {s}");
1002 assert!(!s.contains("COMPRESSION"), "got: {s}");
1003 }
1004}