1use std::collections::{BTreeMap, BTreeSet};
9
10use pg_query::NodeEnum;
11use pg_query::protobuf::CreateStmt;
12
13use crate::identifier::{Identifier, QualifiedName};
14use crate::ir::catalog::Catalog;
15use crate::ir::column::Column;
16use crate::ir::constraint::{Constraint, ConstraintKind};
17use crate::ir::index::{Index, IndexColumnExpr, IndexParent};
18use crate::ir::statistic::{Statistic, StatisticColumn};
19use crate::parse::builder::{choose_name, shared};
20use crate::parse::error::{ParseError, SourceLocation};
21
22#[derive(Debug, Clone, Copy)]
25pub struct TableLikeOptions(u32);
26
27impl TableLikeOptions {
28 const COMMENTS: u32 = 1 << 0;
29 const COMPRESSION: u32 = 1 << 1;
30 const CONSTRAINTS: u32 = 1 << 2;
31 const DEFAULTS: u32 = 1 << 3;
32 const GENERATED: u32 = 1 << 4;
33 const IDENTITY: u32 = 1 << 5;
34 const INDEXES: u32 = 1 << 6;
35 const STATISTICS: u32 = 1 << 7;
36 const STORAGE: u32 = 1 << 8;
37
38 #[must_use]
40 pub const fn new(bits: u32) -> Self {
41 Self(bits)
42 }
43 #[must_use]
45 pub const fn comments(self) -> bool {
46 self.0 & Self::COMMENTS != 0
47 }
48 #[must_use]
50 pub const fn compression(self) -> bool {
51 self.0 & Self::COMPRESSION != 0
52 }
53 #[must_use]
55 pub const fn constraints(self) -> bool {
56 self.0 & Self::CONSTRAINTS != 0
57 }
58 #[must_use]
60 pub const fn defaults(self) -> bool {
61 self.0 & Self::DEFAULTS != 0
62 }
63 #[must_use]
65 pub const fn generated(self) -> bool {
66 self.0 & Self::GENERATED != 0
67 }
68 #[must_use]
70 pub const fn identity(self) -> bool {
71 self.0 & Self::IDENTITY != 0
72 }
73 #[must_use]
75 pub const fn indexes(self) -> bool {
76 self.0 & Self::INDEXES != 0
77 }
78 #[must_use]
80 pub const fn statistics(self) -> bool {
81 self.0 & Self::STATISTICS != 0
82 }
83 #[must_use]
85 pub const fn storage(self) -> bool {
86 self.0 & Self::STORAGE != 0
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct PendingLike {
93 pub target: QualifiedName,
95 pub source: QualifiedName,
97 pub options: TableLikeOptions,
99 pub explicit_cols_before: usize,
102 pub location: SourceLocation,
104}
105
106pub fn extract_pending_likes(
109 create: &CreateStmt,
110 target: &QualifiedName,
111 default_schema: Option<&Identifier>,
112 location: &SourceLocation,
113) -> Result<Vec<PendingLike>, ParseError> {
114 let mut out = Vec::new();
115 let mut explicit_cols = 0usize;
116 for elt in &create.table_elts {
117 match elt.node.as_ref() {
118 Some(NodeEnum::ColumnDef(_)) => explicit_cols += 1,
119 Some(NodeEnum::TableLikeClause(like)) => {
120 let rv = like
121 .relation
122 .as_ref()
123 .ok_or_else(|| ParseError::Structural {
124 location: location.clone(),
125 message: "LIKE clause missing source relation".into(),
126 })?;
127 let source = shared::resolve_qname(rv, default_schema, location)?;
128 out.push(PendingLike {
129 target: target.clone(),
130 source,
131 options: TableLikeOptions::new(like.options),
132 explicit_cols_before: explicit_cols,
133 location: location.clone(),
134 });
135 }
136 _ => {}
137 }
138 }
139 Ok(out)
140}
141
142fn copy_column(src: &Column, opts: TableLikeOptions) -> Column {
146 Column {
147 name: src.name.clone(),
148 ty: src.ty.clone(),
149 nullable: src.nullable,
150 collation: src.collation.clone(),
151 default: if opts.defaults() {
152 src.default.clone()
153 } else {
154 None
155 },
156 identity: if opts.identity() {
157 src.identity.clone()
158 } else {
159 None
160 },
161 generated: if opts.generated() {
162 src.generated.clone()
163 } else {
164 None
165 },
166 storage: if opts.storage() { src.storage } else { None },
167 compression: if opts.compression() {
168 src.compression
169 } else {
170 None
171 },
172 comment: None,
175 }
176}
177
178struct LikeSnapshot {
183 explicit_cols_before: usize,
184 cols: Vec<Column>,
185 constraints: Vec<Constraint>,
186 indexes: Vec<Index>,
189 statistics: Vec<Statistic>,
192}
193
194fn snapshot_like(
201 like: &PendingLike,
202 target_name: &Identifier,
203 target_schema: &Identifier,
204 catalog: &Catalog,
205 taken: &mut choose_name::TakenNames,
206) -> Result<LikeSnapshot, ParseError> {
207 let src = catalog.tables.iter().find(|t| t.qname == like.source)
208 .ok_or_else(|| {
209 let is_view = catalog.views.iter().any(|v| v.qname == like.source);
212 let is_mv = catalog.materialized_views.iter().any(|v| v.qname == like.source);
213 let message = if is_view {
214 format!(
215 "LIKE source {} is a view; LIKE requires a base table (views are not supported as LIKE sources)",
216 like.source
217 )
218 } else if is_mv {
219 format!(
220 "LIKE source {} is a materialized view; LIKE requires a base table (materialized views are not supported as LIKE sources)",
221 like.source
222 )
223 } else {
224 format!(
225 "LIKE source table {} not found (must be a table declared in the schema)",
226 like.source
227 )
228 };
229 ParseError::Structural {
230 location: like.location.clone(),
231 message,
232 }
233 })?;
234
235 let cols: Vec<Column> = src
236 .columns
237 .iter()
238 .map(|c| copy_column(c, like.options))
239 .collect();
240 let mut constraints: Vec<Constraint> = Vec::new();
241
242 if like.options.constraints() {
246 let source_auto_name = format!("{}_check", like.source.name.as_str());
260 for c in &src.constraints {
261 if matches!(c.kind, ConstraintKind::Check { .. }) {
262 let local_name = if c.qname.name.as_str() == source_auto_name {
263 Identifier::from_unquoted(&format!("{}_check", target_name.as_str())).map_err(
265 |e| ParseError::Structural {
266 location: like.location.clone(),
267 message: format!(
268 "re-derived CHECK constraint name is not a valid identifier: {e}"
269 ),
270 },
271 )?
272 } else {
273 c.qname.name.clone()
275 };
276 constraints.push(Constraint {
277 qname: QualifiedName::new(target_schema.clone(), local_name),
278 kind: c.kind.clone(),
279 deferrable: c.deferrable,
280 comment: None,
283 });
284 }
285 }
286 }
287
288 let indexes = if like.options.indexes() {
292 copy_index_constraints(
293 &src.constraints,
294 target_name,
295 target_schema,
296 &like.location,
297 taken,
298 &mut constraints,
299 )?;
300 copy_plain_indexes(
301 &like.source,
302 target_name,
303 target_schema,
304 &like.location,
305 &catalog.indexes,
306 taken,
307 )?
308 } else {
309 Vec::new()
310 };
311
312 let statistics = if like.options.statistics() {
314 copy_statistics(
315 &like.source,
316 target_name,
317 target_schema,
318 &like.location,
319 &catalog.statistics,
320 taken,
321 )?
322 } else {
323 Vec::new()
324 };
325
326 Ok(LikeSnapshot {
327 explicit_cols_before: like.explicit_cols_before,
328 cols,
329 constraints,
330 indexes,
331 statistics,
332 })
333}
334
335fn copy_index_constraints(
338 src_constraints: &[Constraint],
339 target_name: &Identifier,
340 target_schema: &Identifier,
341 location: &crate::parse::error::SourceLocation,
342 taken: &mut choose_name::TakenNames,
343 out: &mut Vec<Constraint>,
344) -> Result<(), ParseError> {
345 for c in src_constraints {
346 match &c.kind {
347 ConstraintKind::PrimaryKey { columns, include } => {
348 let generated = choose_name::choose_index_name(
349 target_name.as_str(),
350 &[],
351 choose_name::IndexNameKind::Pkey,
352 taken,
353 );
354 let qname = QualifiedName::new(
355 target_schema.clone(),
356 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
357 location: location.clone(),
358 message: format!(
359 "generated PK constraint name {generated:?} is not a valid identifier: {e}"
360 ),
361 })?,
362 );
363 out.push(Constraint {
364 qname,
365 kind: ConstraintKind::PrimaryKey {
366 columns: columns.clone(),
367 include: include.clone(),
368 },
369 deferrable: c.deferrable,
370 comment: None,
371 });
372 }
373 ConstraintKind::Unique {
374 columns,
375 include,
376 nulls_distinct,
377 } => {
378 let col_opts: Vec<Option<&str>> =
379 columns.iter().map(|col| Some(col.as_str())).collect();
380 let generated = choose_name::choose_index_name(
381 target_name.as_str(),
382 &col_opts,
383 choose_name::IndexNameKind::Unique,
384 taken,
385 );
386 let qname = QualifiedName::new(
387 target_schema.clone(),
388 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
389 location: location.clone(),
390 message: format!(
391 "generated UNIQUE constraint name {generated:?} is not a valid identifier: {e}"
392 ),
393 })?,
394 );
395 out.push(Constraint {
396 qname,
397 kind: ConstraintKind::Unique {
398 columns: columns.clone(),
399 include: include.clone(),
400 nulls_distinct: *nulls_distinct,
401 },
402 deferrable: c.deferrable,
403 comment: None,
404 });
405 }
406 ConstraintKind::Check { .. } | ConstraintKind::ForeignKey(_) => {}
409 }
410 }
411 Ok(())
412}
413
414fn copy_plain_indexes(
424 source: &QualifiedName,
425 target_name: &Identifier,
426 target_schema: &Identifier,
427 location: &crate::parse::error::SourceLocation,
428 catalog_indexes: &[Index],
429 taken: &mut choose_name::TakenNames,
430) -> Result<Vec<Index>, ParseError> {
431 let mut out = Vec::new();
432 let source_parent = IndexParent::Table(source.clone());
433 for idx in catalog_indexes.iter().filter(|i| i.on == source_parent) {
434 let col_opts: Vec<Option<&str>> = idx
435 .columns
436 .iter()
437 .map(|col| match &col.expr {
438 IndexColumnExpr::Column(id) => Some(id.as_str()),
439 IndexColumnExpr::Expression(_) => None,
440 })
441 .collect();
442 let generated = choose_name::choose_index_name(
446 target_name.as_str(),
447 &col_opts,
448 choose_name::IndexNameKind::Plain,
449 taken,
450 );
451 let new_qname = QualifiedName::new(
452 target_schema.clone(),
453 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
454 location: location.clone(),
455 message: format!(
456 "generated index name {generated:?} is not a valid identifier: {e}"
457 ),
458 })?,
459 );
460 out.push(Index {
461 qname: new_qname,
462 on: IndexParent::Table(QualifiedName::new(
463 target_schema.clone(),
464 target_name.clone(),
465 )),
466 method: idx.method,
467 columns: idx.columns.clone(),
468 include: idx.include.clone(),
469 unique: idx.unique,
470 nulls_not_distinct: idx.nulls_not_distinct,
471 predicate: idx.predicate.clone(),
472 tablespace: idx.tablespace.clone(),
473 comment: None,
477 storage: idx.storage.clone(),
478 });
479 }
480 Ok(out)
481}
482
483fn copy_statistics(
492 source: &QualifiedName,
493 target_name: &Identifier,
494 target_schema: &Identifier,
495 location: &crate::parse::error::SourceLocation,
496 catalog_statistics: &[Statistic],
497 taken: &mut choose_name::TakenNames,
498) -> Result<Vec<Statistic>, ParseError> {
499 let clone_qname = QualifiedName::new(target_schema.clone(), target_name.clone());
500 let mut out = Vec::new();
501 for stat in catalog_statistics.iter().filter(|s| &s.target == source) {
502 let col_opts: Vec<Option<&str>> = stat
504 .columns
505 .iter()
506 .map(|col| match col {
507 StatisticColumn::Column(id) => Some(id.as_str()),
508 StatisticColumn::Expression(_) => None,
509 })
510 .collect();
511 let generated = choose_name::choose_index_name(
512 target_name.as_str(),
513 &col_opts,
514 choose_name::IndexNameKind::Stat,
515 taken,
516 );
517 let new_qname = QualifiedName::new(
518 target_schema.clone(),
519 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
520 location: location.clone(),
521 message: format!(
522 "generated statistics name {generated:?} is not a valid identifier: {e}"
523 ),
524 })?,
525 );
526 out.push(Statistic {
527 qname: new_qname,
528 target: clone_qname.clone(),
529 kinds: stat.kinds,
530 columns: stat.columns.clone(),
531 statistics_target: stat.statistics_target,
532 owner: None,
533 comment: None,
534 });
535 }
536 Ok(out)
537}
538
539fn resolve_order(
545 by_target: &BTreeMap<QualifiedName, Vec<&PendingLike>>,
546) -> Result<Vec<QualifiedName>, ParseError> {
547 let mut unresolved: BTreeSet<QualifiedName> = by_target.keys().cloned().collect();
548 let mut ordered: Vec<QualifiedName> = Vec::with_capacity(by_target.len());
549
550 while !unresolved.is_empty() {
551 let ready: Vec<QualifiedName> = unresolved
553 .iter()
554 .filter(|target| {
555 by_target[*target]
556 .iter()
557 .all(|like| !unresolved.contains(&like.source))
558 })
559 .cloned()
560 .collect();
561
562 if ready.is_empty() {
563 let involved = unresolved
565 .iter()
566 .map(ToString::to_string)
567 .collect::<Vec<_>>()
568 .join(", ");
569 let location = unresolved
571 .iter()
572 .next()
573 .and_then(|t| by_target.get(t))
574 .and_then(|likes| likes.first())
575 .map_or_else(
576 || SourceLocation::new(std::path::PathBuf::new(), 0, 0),
577 |like| like.location.clone(),
578 );
579 return Err(ParseError::Structural {
580 location,
581 message: format!("LIKE forms a cycle involving {involved}"),
582 });
583 }
584
585 for target in ready {
586 unresolved.remove(&target);
587 ordered.push(target);
588 }
589 }
590
591 Ok(ordered)
592}
593
594pub fn apply_pending_likes(
607 catalog: &mut Catalog,
608 pending: &[PendingLike],
609) -> Result<(), ParseError> {
610 let mut by_target: BTreeMap<QualifiedName, Vec<&PendingLike>> = BTreeMap::new();
613 for p in pending {
614 by_target.entry(p.target.clone()).or_default().push(p);
615 }
616
617 for target in resolve_order(&by_target)? {
618 let mut likes = by_target.remove(&target).unwrap_or_default();
620 likes.sort_by_key(|p| p.explicit_cols_before); let mut taken = choose_name::TakenNames::from_schema(catalog, &target.schema);
631
632 let mut snapshots: Vec<LikeSnapshot> = Vec::with_capacity(likes.len());
635 for like in &likes {
636 snapshots.push(snapshot_like(
637 like,
638 &target.name,
639 &target.schema,
640 catalog,
641 &mut taken,
642 )?);
643 }
644
645 let mut inserted = 0usize;
650 for snap in snapshots {
651 let n = snap.cols.len();
652 let tgt = catalog
653 .tables
654 .iter_mut()
655 .find(|t| t.qname == target)
656 .ok_or_else(|| ParseError::Structural {
657 location: likes.first().map_or_else(
658 || SourceLocation::new(std::path::PathBuf::new(), 0, 0),
659 |l| l.location.clone(),
660 ),
661 message: format!("LIKE target table {target} vanished"),
662 })?;
663 let at = (snap.explicit_cols_before + inserted).min(tgt.columns.len());
664 tgt.columns.splice(at..at, snap.cols);
665 inserted += n;
666 tgt.constraints.extend(snap.constraints);
667 catalog.indexes.extend(snap.indexes);
671 catalog.statistics.extend(snap.statistics);
675 }
676 }
677 Ok(())
678}
679
680pub fn apply_pending_like_comments(
689 catalog: &mut Catalog,
690 pending: &[PendingLike],
691) -> Result<(), ParseError> {
692 let mut by_target: BTreeMap<QualifiedName, Vec<&PendingLike>> = BTreeMap::new();
696 for p in pending {
697 if p.options.comments() {
698 by_target.entry(p.target.clone()).or_default().push(p);
699 }
700 }
701
702 if by_target.is_empty() {
703 return Ok(());
704 }
705
706 for target in resolve_order(&by_target)? {
707 let likes = by_target.remove(&target).unwrap_or_default();
708 for like in likes {
709 let (src_table_comment, src_col_comments): (
712 Option<String>,
713 Vec<(Identifier, Option<String>)>,
714 ) = {
715 let src = catalog
716 .tables
717 .iter()
718 .find(|t| t.qname == like.source)
719 .ok_or_else(|| ParseError::Structural {
720 location: like.location.clone(),
721 message: format!(
722 "LIKE source table {} not found during comment copy",
723 like.source
724 ),
725 })?;
726 let table_comment = src.comment.clone();
727 let col_comments = src
728 .columns
729 .iter()
730 .map(|c| (c.name.clone(), c.comment.clone()))
731 .collect();
732 (table_comment, col_comments)
733 };
734
735 let tgt = catalog
736 .tables
737 .iter_mut()
738 .find(|t| t.qname == target)
739 .ok_or_else(|| ParseError::Structural {
740 location: like.location.clone(),
741 message: format!("LIKE target table {target} vanished during comment copy"),
742 })?;
743
744 if tgt.comment.is_none() {
746 tgt.comment = src_table_comment;
747 }
748
749 for (col_name, col_comment) in src_col_comments {
751 if let Some(tgt_col) = tgt.columns.iter_mut().find(|c| c.name == col_name)
752 && tgt_col.comment.is_none()
753 {
754 tgt_col.comment = col_comment;
755 }
756 }
757 }
758 }
759 Ok(())
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use crate::ir::column_type::ColumnType;
766 use std::path::PathBuf;
767
768 fn loc() -> SourceLocation {
769 SourceLocation::new(PathBuf::from("t.sql"), 1, 1)
770 }
771 fn id(s: &str) -> Identifier {
772 Identifier::from_unquoted(s).unwrap()
773 }
774 fn qn(s: &str, n: &str) -> QualifiedName {
775 QualifiedName::new(id(s), id(n))
776 }
777
778 #[test]
779 fn end_to_end_bare_like_via_parse_directory() {
780 let dir = tempfile::tempdir().unwrap();
781 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
782 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
783 std::fs::write(
784 dir.path().join("pub/t.sql"),
785 "CREATE TABLE pub.base (id int PRIMARY KEY, name text);\n\
786 CREATE TABLE pub.clone (LIKE pub.base);\n",
787 )
788 .unwrap();
789 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
790 let clone = cat
791 .tables
792 .iter()
793 .find(|t| t.qname.name.as_str() == "clone")
794 .unwrap();
795 assert_eq!(
796 clone
797 .columns
798 .iter()
799 .map(|c| c.name.as_str().to_string())
800 .collect::<Vec<_>>(),
801 vec!["id".to_string(), "name".to_string()]
802 );
803 }
804
805 #[test]
806 fn bare_like_copies_columns_names_types_notnull() {
807 let dir = tempfile::tempdir().unwrap();
808 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
809 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
810 std::fs::write(
811 dir.path().join("pub/t.sql"),
812 "CREATE TABLE pub.base (id integer NOT NULL, name text);\n\
813 CREATE TABLE pub.clone (LIKE pub.base);\n",
814 )
815 .unwrap();
816 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
817 let clone = cat
818 .tables
819 .iter()
820 .find(|t| t.qname.name.as_str() == "clone")
821 .unwrap();
822 let got: Vec<_> = clone
823 .columns
824 .iter()
825 .map(|c| (c.name.as_str().to_string(), c.ty.clone(), c.nullable))
826 .collect();
827 assert_eq!(
828 got,
829 vec![
830 ("id".into(), ColumnType::Integer, false),
831 ("name".into(), ColumnType::Text, true),
832 ]
833 );
834 assert!(
835 clone.columns.iter().all(|c| c.default.is_none()),
836 "bare LIKE copies no defaults"
837 );
838 }
839
840 #[test]
841 fn like_unknown_source_errors() {
842 let pend = PendingLike {
843 target: qn("pub", "clone"),
844 source: qn("pub", "missing"),
845 options: TableLikeOptions::new(0),
846 explicit_cols_before: 0,
847 location: loc(),
848 };
849 let dir = tempfile::tempdir().unwrap();
853 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
854 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
855 std::fs::write(
856 dir.path().join("pub/t.sql"),
857 "CREATE TABLE pub.clone (id int);\n",
858 )
859 .unwrap();
860 let (mut cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
861 assert!(apply_pending_likes(&mut cat, &[pend]).is_err());
862 }
863
864 #[test]
868 fn chained_like_resolves_dependent_after_source() {
869 let dir = tempfile::tempdir().unwrap();
870 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
871 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
872 std::fs::write(
873 dir.path().join("pub/t.sql"),
874 "CREATE TABLE pub.a (id int PRIMARY KEY, name text);\n\
875 CREATE TABLE pub.z (LIKE pub.a);\n\
876 CREATE TABLE pub.x (LIKE pub.z);\n",
877 )
878 .unwrap();
879 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
880 for tname in ["a", "z", "x"] {
881 let t = cat
882 .tables
883 .iter()
884 .find(|t| t.qname.name.as_str() == tname)
885 .unwrap();
886 assert_eq!(
887 t.columns
888 .iter()
889 .map(|c| c.name.as_str().to_string())
890 .collect::<Vec<_>>(),
891 vec!["id".to_string(), "name".to_string()],
892 "table {tname} should have both copied columns",
893 );
894 }
895 }
896
897 #[test]
902 fn chained_like_resolves_when_dependent_sorts_before_source() {
903 let dir = tempfile::tempdir().unwrap();
904 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
905 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
906 std::fs::write(
907 dir.path().join("pub/t.sql"),
908 "CREATE TABLE pub.base (id int PRIMARY KEY, name text);\n\
909 CREATE TABLE pub.aaa (LIKE pub.zzz);\n\
910 CREATE TABLE pub.zzz (LIKE pub.base);\n",
911 )
912 .unwrap();
913 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
914 for tname in ["base", "aaa", "zzz"] {
915 let t = cat
916 .tables
917 .iter()
918 .find(|t| t.qname.name.as_str() == tname)
919 .unwrap();
920 assert_eq!(
921 t.columns
922 .iter()
923 .map(|c| c.name.as_str().to_string())
924 .collect::<Vec<_>>(),
925 vec!["id".to_string(), "name".to_string()],
926 "table {tname} should have both copied columns",
927 );
928 }
929 }
930
931 #[test]
934 fn self_referential_like_errors() {
935 let dir = tempfile::tempdir().unwrap();
936 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
937 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
938 std::fs::write(
939 dir.path().join("pub/t.sql"),
940 "CREATE TABLE pub.c (LIKE pub.c);\n",
941 )
942 .unwrap();
943 let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
944 assert!(
945 matches!(err, crate::parse::ParseError::Structural { .. }),
946 "got {err:?}"
947 );
948 }
949
950 #[test]
952 fn two_cycle_like_errors() {
953 let dir = tempfile::tempdir().unwrap();
954 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
955 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
956 std::fs::write(
957 dir.path().join("pub/t.sql"),
958 "CREATE TABLE pub.p (LIKE pub.q);\n\
959 CREATE TABLE pub.q (LIKE pub.p);\n",
960 )
961 .unwrap();
962 let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
963 assert!(
964 matches!(err, crate::parse::ParseError::Structural { .. }),
965 "got {err:?}"
966 );
967 }
968
969 #[test]
970 fn like_preserves_position_among_explicit_columns() {
971 let dir = tempfile::tempdir().unwrap();
972 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
973 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
974 std::fs::write(
975 dir.path().join("pub/t.sql"),
976 "CREATE TABLE pub.base (a int, b int);\n\
977 CREATE TABLE pub.c (x int, LIKE pub.base, y text);\n",
978 )
979 .unwrap();
980 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
981 let c = cat
982 .tables
983 .iter()
984 .find(|t| t.qname.name.as_str() == "c")
985 .unwrap();
986 assert_eq!(
987 c.columns
988 .iter()
989 .map(|c| c.name.as_str().to_string())
990 .collect::<Vec<_>>(),
991 vec!["x", "a", "b", "y"]
992 );
993 }
994
995 #[test]
996 fn including_defaults_and_storage_gate_attributes() {
997 let dir = tempfile::tempdir().unwrap();
998 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
999 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1000 std::fs::write(
1001 dir.path().join("pub/t.sql"),
1002 "CREATE TABLE pub.base (id int DEFAULT 7, doc text STORAGE EXTERNAL);\n\
1003 CREATE TABLE pub.bare (LIKE pub.base);\n\
1004 CREATE TABLE pub.full (LIKE pub.base INCLUDING DEFAULTS INCLUDING STORAGE);\n",
1005 )
1006 .unwrap();
1007 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1008 let bare = cat
1009 .tables
1010 .iter()
1011 .find(|t| t.qname.name.as_str() == "bare")
1012 .unwrap();
1013 assert!(bare.columns[0].default.is_none());
1014 assert!(bare.columns[1].storage.is_none());
1015 let full = cat
1016 .tables
1017 .iter()
1018 .find(|t| t.qname.name.as_str() == "full")
1019 .unwrap();
1020 assert!(
1021 full.columns[0].default.is_some(),
1022 "INCLUDING DEFAULTS copies default"
1023 );
1024 assert!(
1025 full.columns[1].storage.is_some(),
1026 "INCLUDING STORAGE copies storage"
1027 );
1028 }
1029
1030 #[test]
1031 fn multiple_like_clauses_expand_in_order() {
1032 let dir = tempfile::tempdir().unwrap();
1033 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1034 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1035 std::fs::write(
1036 dir.path().join("pub/t.sql"),
1037 "CREATE TABLE pub.l (a int);\nCREATE TABLE pub.r (b int);\n\
1038 CREATE TABLE pub.c (LIKE pub.l, mid int, LIKE pub.r);\n",
1039 )
1040 .unwrap();
1041 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1042 let c = cat
1043 .tables
1044 .iter()
1045 .find(|t| t.qname.name.as_str() == "c")
1046 .unwrap();
1047 assert_eq!(
1048 c.columns
1049 .iter()
1050 .map(|c| c.name.as_str().to_string())
1051 .collect::<Vec<_>>(),
1052 vec!["a", "mid", "b"]
1053 );
1054 }
1055
1056 #[test]
1059 fn including_comments_copies_table_comment() {
1060 let dir = tempfile::tempdir().unwrap();
1061 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1062 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1063 std::fs::write(
1064 dir.path().join("pub/t.sql"),
1065 "CREATE TABLE pub.base (id int);\n\
1066 COMMENT ON TABLE pub.base IS 'hi';\n\
1067 CREATE TABLE pub.c (LIKE pub.base INCLUDING COMMENTS);\n",
1068 )
1069 .unwrap();
1070 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1071 let c = cat
1072 .tables
1073 .iter()
1074 .find(|t| t.qname.name.as_str() == "c")
1075 .unwrap();
1076 assert_eq!(
1077 c.comment.as_deref(),
1078 Some("hi"),
1079 "INCLUDING COMMENTS should propagate the source table comment to the clone"
1080 );
1081 }
1082
1083 #[test]
1084 fn including_comments_copies_column_comment() {
1085 let dir = tempfile::tempdir().unwrap();
1086 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1087 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1088 std::fs::write(
1089 dir.path().join("pub/t.sql"),
1090 "CREATE TABLE pub.base (id int);\n\
1091 COMMENT ON COLUMN pub.base.id IS 'col';\n\
1092 CREATE TABLE pub.clone (LIKE pub.base INCLUDING COMMENTS);\n",
1093 )
1094 .unwrap();
1095 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1096 let clone = cat
1097 .tables
1098 .iter()
1099 .find(|t| t.qname.name.as_str() == "clone")
1100 .unwrap();
1101 let id_col = clone
1102 .columns
1103 .iter()
1104 .find(|c| c.name.as_str() == "id")
1105 .unwrap();
1106 assert_eq!(
1107 id_col.comment.as_deref(),
1108 Some("col"),
1109 "INCLUDING COMMENTS should propagate the source column comment to the clone"
1110 );
1111 }
1112
1113 #[test]
1114 fn bare_like_does_not_copy_comments() {
1115 let dir = tempfile::tempdir().unwrap();
1116 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1117 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1118 std::fs::write(
1119 dir.path().join("pub/t.sql"),
1120 "CREATE TABLE pub.base (id int);\n\
1121 COMMENT ON TABLE pub.base IS 'tbl';\n\
1122 COMMENT ON COLUMN pub.base.id IS 'col';\n\
1123 CREATE TABLE pub.clone (LIKE pub.base);\n",
1124 )
1125 .unwrap();
1126 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1127 let clone = cat
1128 .tables
1129 .iter()
1130 .find(|t| t.qname.name.as_str() == "clone")
1131 .unwrap();
1132 assert!(
1133 clone.comment.is_none(),
1134 "bare LIKE must not copy table comment"
1135 );
1136 assert!(
1137 clone.columns.iter().all(|c| c.comment.is_none()),
1138 "bare LIKE must not copy column comments"
1139 );
1140 }
1141
1142 #[test]
1147 fn comment_on_clone_like_derived_column_succeeds() {
1148 let dir = tempfile::tempdir().unwrap();
1149 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1150 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1151 std::fs::write(
1152 dir.path().join("pub/t.sql"),
1153 "CREATE TABLE pub.base (id int);\n\
1154 CREATE TABLE pub.clone (LIKE pub.base);\n\
1155 COMMENT ON COLUMN pub.clone.id IS 'x';\n",
1156 )
1157 .unwrap();
1158 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1159 let clone = cat
1160 .tables
1161 .iter()
1162 .find(|t| t.qname.name.as_str() == "clone")
1163 .unwrap();
1164 let id_col = clone
1165 .columns
1166 .iter()
1167 .find(|c| c.name.as_str() == "id")
1168 .unwrap();
1169 assert_eq!(
1170 id_col.comment.as_deref(),
1171 Some("x"),
1172 "COMMENT ON COLUMN clone.id must apply to the LIKE-derived column"
1173 );
1174 }
1175
1176 #[test]
1177 fn explicit_clone_comment_wins_over_including_comments() {
1178 let dir = tempfile::tempdir().unwrap();
1179 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1180 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1181 std::fs::write(
1182 dir.path().join("pub/t.sql"),
1183 "CREATE TABLE pub.base (id int);\n\
1184 COMMENT ON TABLE pub.base IS 'from_base';\n\
1185 CREATE TABLE pub.clone (LIKE pub.base INCLUDING COMMENTS);\n\
1186 COMMENT ON TABLE pub.clone IS 'explicit';\n",
1187 )
1188 .unwrap();
1189 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1190 let clone = cat
1191 .tables
1192 .iter()
1193 .find(|t| t.qname.name.as_str() == "clone")
1194 .unwrap();
1195 assert_eq!(
1196 clone.comment.as_deref(),
1197 Some("explicit"),
1198 "explicit COMMENT ON TABLE clone must win over INCLUDING COMMENTS propagation"
1199 );
1200 }
1201
1202 #[test]
1205 fn including_constraints_copies_check() {
1206 let dir = tempfile::tempdir().unwrap();
1207 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1208 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1209 std::fs::write(
1210 dir.path().join("pub/t.sql"),
1211 "CREATE TABLE pub.base (n int, CONSTRAINT n_pos CHECK (n > 0));\n\
1212 CREATE TABLE pub.bare (LIKE pub.base);\n\
1213 CREATE TABLE pub.c (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1214 )
1215 .unwrap();
1216 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1217 let bare = cat
1218 .tables
1219 .iter()
1220 .find(|t| t.qname.name.as_str() == "bare")
1221 .unwrap();
1222 assert!(
1223 bare.constraints
1224 .iter()
1225 .all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. })),
1226 "bare LIKE must not copy CHECK constraints"
1227 );
1228 let c = cat
1229 .tables
1230 .iter()
1231 .find(|t| t.qname.name.as_str() == "c")
1232 .unwrap();
1233 assert_eq!(
1234 c.constraints
1235 .iter()
1236 .filter(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1237 .count(),
1238 1,
1239 "INCLUDING CONSTRAINTS should copy exactly one CHECK constraint"
1240 );
1241 }
1242
1243 #[test]
1244 fn including_constraints_does_not_copy_pk_or_unique() {
1245 let dir = tempfile::tempdir().unwrap();
1246 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1247 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1248 std::fs::write(
1249 dir.path().join("pub/t.sql"),
1250 "CREATE TABLE pub.base (id int PRIMARY KEY, e text UNIQUE, n int CHECK (n>0));\n\
1251 CREATE TABLE pub.c (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1252 )
1253 .unwrap();
1254 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1255 let c = cat
1256 .tables
1257 .iter()
1258 .find(|t| t.qname.name.as_str() == "c")
1259 .unwrap();
1260 assert_eq!(
1261 c.constraints
1262 .iter()
1263 .filter(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1264 .count(),
1265 1,
1266 "INCLUDING CONSTRAINTS should copy the CHECK constraint"
1267 );
1268 assert!(
1269 c.constraints.iter().all(|c| !matches!(
1270 c.kind,
1271 crate::ir::constraint::ConstraintKind::PrimaryKey { .. }
1272 )),
1273 "INCLUDING CONSTRAINTS must not copy PrimaryKey (belongs to INCLUDING INDEXES)"
1274 );
1275 assert!(
1276 c.constraints
1277 .iter()
1278 .all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Unique { .. })),
1279 "INCLUDING CONSTRAINTS must not copy Unique (belongs to INCLUDING INDEXES)"
1280 );
1281 }
1282
1283 #[test]
1289 fn like_constraints_rederives_unnamed_check_name() {
1290 let dir = tempfile::tempdir().unwrap();
1291 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1292 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1293 std::fs::write(
1295 dir.path().join("pub/t.sql"),
1296 "CREATE TABLE pub.base (n int, CHECK (n > 0));\n\
1297 CREATE TABLE pub.clone (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1298 )
1299 .unwrap();
1300 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1301 let clone = cat
1302 .tables
1303 .iter()
1304 .find(|t| t.qname.name.as_str() == "clone")
1305 .unwrap();
1306 let check = clone
1307 .constraints
1308 .iter()
1309 .find(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1310 .expect("clone must have a copied CHECK constraint");
1311 assert_eq!(
1312 check.qname.name.as_str(),
1313 "clone_check",
1314 "unnamed source CHECK must be re-derived to clone_check, got {:?}",
1315 check.qname.name.as_str(),
1316 );
1317 assert_eq!(
1318 check.qname.schema.as_str(),
1319 "pub",
1320 "copied CHECK must be in clone's schema"
1321 );
1322 }
1323
1324 #[test]
1328 fn like_constraints_preserves_explicit_check_name() {
1329 let dir = tempfile::tempdir().unwrap();
1330 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1331 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1332 std::fs::write(
1334 dir.path().join("pub/t.sql"),
1335 "CREATE TABLE pub.base (n int, CONSTRAINT n_pos CHECK (n > 0));\n\
1336 CREATE TABLE pub.clone (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1337 )
1338 .unwrap();
1339 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1340 let clone = cat
1341 .tables
1342 .iter()
1343 .find(|t| t.qname.name.as_str() == "clone")
1344 .unwrap();
1345 let check = clone
1346 .constraints
1347 .iter()
1348 .find(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1349 .expect("clone must have a copied CHECK constraint");
1350 assert_eq!(
1351 check.qname.name.as_str(),
1352 "n_pos",
1353 "explicitly-named source CHECK must be preserved as n_pos, got {:?}",
1354 check.qname.name.as_str(),
1355 );
1356 assert_eq!(
1357 check.qname.schema.as_str(),
1358 "pub",
1359 "copied CHECK must be in clone's schema"
1360 );
1361 }
1362
1363 #[test]
1366 fn including_indexes_copies_pk_and_unique_with_pg_names() {
1367 use crate::ir::constraint::ConstraintKind::{PrimaryKey, Unique};
1368 let dir = tempfile::tempdir().unwrap();
1369 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1370 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1371 std::fs::write(
1372 dir.path().join("pub/t.sql"),
1373 "CREATE TABLE pub.base (id int PRIMARY KEY, email text UNIQUE);\n\
1374 CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
1375 )
1376 .unwrap();
1377 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1378 let c = cat
1379 .tables
1380 .iter()
1381 .find(|t| t.qname.name.as_str() == "c")
1382 .unwrap();
1383 let names: Vec<_> = c
1384 .constraints
1385 .iter()
1386 .map(|k| k.qname.name.as_str().to_string())
1387 .collect();
1388 assert!(names.contains(&"c_pkey".to_string()), "got {names:?}");
1389 assert!(names.contains(&"c_email_key".to_string()), "got {names:?}");
1390 assert_eq!(
1391 c.constraints
1392 .iter()
1393 .filter(|k| matches!(k.kind, PrimaryKey { .. }))
1394 .count(),
1395 1
1396 );
1397 assert_eq!(
1398 c.constraints
1399 .iter()
1400 .filter(|k| matches!(k.kind, Unique { .. }))
1401 .count(),
1402 1
1403 );
1404 }
1405
1406 #[test]
1407 fn bare_like_copies_no_pk_or_unique() {
1408 let dir = tempfile::tempdir().unwrap();
1409 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1410 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1411 std::fs::write(
1412 dir.path().join("pub/t.sql"),
1413 "CREATE TABLE pub.base (id int PRIMARY KEY, email text UNIQUE);\n\
1414 CREATE TABLE pub.d (LIKE pub.base);\n",
1415 )
1416 .unwrap();
1417 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1418 let d = cat
1419 .tables
1420 .iter()
1421 .find(|t| t.qname.name.as_str() == "d")
1422 .unwrap();
1423 assert!(
1424 d.constraints.iter().all(|c| !matches!(
1425 c.kind,
1426 crate::ir::constraint::ConstraintKind::PrimaryKey { .. }
1427 )),
1428 "bare LIKE must not copy PK"
1429 );
1430 assert!(
1431 d.constraints
1432 .iter()
1433 .all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Unique { .. })),
1434 "bare LIKE must not copy UNIQUE"
1435 );
1436 }
1437
1438 #[test]
1443 fn including_indexes_copies_plain_index() {
1444 let dir = tempfile::tempdir().unwrap();
1445 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1446 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1447 std::fs::write(
1448 dir.path().join("pub/t.sql"),
1449 "CREATE TABLE pub.base (a int, b int);\n\
1450 CREATE INDEX ON pub.base (a, b);\n\
1451 CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
1452 )
1453 .unwrap();
1454 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1455 let idx: Vec<_> = cat
1456 .indexes
1457 .iter()
1458 .filter(|i| i.on.qname().name.as_str() == "c")
1459 .map(|i| i.qname.name.as_str().to_string())
1460 .collect();
1461 assert_eq!(idx, vec!["c_a_b_idx".to_string()], "got {idx:?}");
1462 }
1463
1464 #[test]
1468 fn unique_index_keeps_idx_suffix() {
1469 let dir = tempfile::tempdir().unwrap();
1470 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1471 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1472 std::fs::write(
1473 dir.path().join("pub/t.sql"),
1474 "CREATE TABLE pub.base (a int);\n\
1475 CREATE UNIQUE INDEX ON pub.base (a);\n\
1476 CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
1477 )
1478 .unwrap();
1479 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1480 let copied: Vec<_> = cat
1481 .indexes
1482 .iter()
1483 .filter(|i| i.on.qname().name.as_str() == "c")
1484 .collect();
1485 assert_eq!(
1486 copied.len(),
1487 1,
1488 "expected exactly one copied index, got {copied:?}"
1489 );
1490 assert_eq!(
1491 copied[0].qname.name.as_str(),
1492 "c_a_idx",
1493 "unique plain index must use _idx suffix, not _key"
1494 );
1495 assert!(copied[0].unique, "copied index must preserve unique flag");
1496 }
1497
1498 #[test]
1500 fn bare_like_copies_no_indexes() {
1501 let dir = tempfile::tempdir().unwrap();
1502 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1503 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1504 std::fs::write(
1505 dir.path().join("pub/t.sql"),
1506 "CREATE TABLE pub.base (a int, b int);\n\
1507 CREATE INDEX ON pub.base (a, b);\n\
1508 CREATE TABLE pub.d (LIKE pub.base);\n",
1509 )
1510 .unwrap();
1511 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1512 let targeting_d: Vec<_> = cat
1513 .indexes
1514 .iter()
1515 .filter(|i| i.on.qname().name.as_str() == "d")
1516 .collect();
1517 assert!(
1518 targeting_d.is_empty(),
1519 "bare LIKE must not copy plain indexes, got {targeting_d:?}"
1520 );
1521 }
1522
1523 #[test]
1526 fn including_statistics_copies_stats() {
1527 let dir = tempfile::tempdir().unwrap();
1528 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1529 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1530 std::fs::write(
1531 dir.path().join("pub/t.sql"),
1532 "CREATE TABLE pub.base (a int, b int);\n\
1533 CREATE STATISTICS pub.base_stat (ndistinct) ON a, b FROM pub.base;\n\
1534 CREATE TABLE pub.c (LIKE pub.base INCLUDING STATISTICS);\n",
1535 )
1536 .unwrap();
1537 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1538 let copied: Vec<_> = cat
1539 .statistics
1540 .iter()
1541 .filter(|s| s.target.name.as_str() == "c")
1542 .collect();
1543 assert_eq!(copied.len(), 1, "expected one copied statistic");
1544 assert_eq!(
1545 copied[0].qname.name.as_str(),
1546 "c_a_b_stat",
1547 "generated statistic qname should be c_a_b_stat, got {:?}",
1548 copied[0].qname.name.as_str(),
1549 );
1550 }
1551
1552 #[test]
1553 fn bare_like_copies_no_stats() {
1554 let dir = tempfile::tempdir().unwrap();
1555 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1556 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1557 std::fs::write(
1558 dir.path().join("pub/t.sql"),
1559 "CREATE TABLE pub.base (a int, b int);\n\
1560 CREATE STATISTICS pub.base_stat (ndistinct) ON a, b FROM pub.base;\n\
1561 CREATE TABLE pub.d (LIKE pub.base);\n",
1562 )
1563 .unwrap();
1564 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1565 let targeting_d: Vec<_> = cat
1566 .statistics
1567 .iter()
1568 .filter(|s| s.target.name.as_str() == "d")
1569 .collect();
1570 assert!(
1571 targeting_d.is_empty(),
1572 "bare LIKE must not copy statistics, got {targeting_d:?}"
1573 );
1574 }
1575
1576 #[test]
1581 fn including_all_copies_everything() {
1582 use crate::ir::constraint::ConstraintKind::{Check, PrimaryKey};
1583 let dir = tempfile::tempdir().unwrap();
1584 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1585 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1586 std::fs::write(
1587 dir.path().join("pub/t.sql"),
1588 "CREATE TABLE pub.base (id int PRIMARY KEY DEFAULT 1, n int CHECK (n > 0));\n\
1589 CREATE INDEX ON pub.base (n);\n\
1590 CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
1591 )
1592 .unwrap();
1593 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1594 let c = cat
1595 .tables
1596 .iter()
1597 .find(|t| t.qname.name.as_str() == "c")
1598 .unwrap();
1599 assert!(
1601 c.columns[0].default.is_some(),
1602 "INCLUDING ALL must copy DEFAULT (DEFAULTS bit)"
1603 );
1604 assert!(
1606 c.constraints
1607 .iter()
1608 .any(|k| matches!(k.kind, PrimaryKey { .. })),
1609 "INCLUDING ALL must copy PrimaryKey constraint (INDEXES bit)"
1610 );
1611 assert!(
1613 c.constraints.iter().any(|k| matches!(k.kind, Check { .. })),
1614 "INCLUDING ALL must copy Check constraint (CONSTRAINTS bit)"
1615 );
1616 assert!(
1618 cat.indexes
1619 .iter()
1620 .any(|i| i.on.qname().name.as_str() == "c"),
1621 "INCLUDING ALL must copy plain index (INDEXES bit)"
1622 );
1623 }
1624
1625 #[test]
1627 fn like_non_table_source_errors_clearly() {
1628 let dir = tempfile::tempdir().unwrap();
1629 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1630 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1631 std::fs::write(
1632 dir.path().join("pub/t.sql"),
1633 "CREATE TABLE pub.base (id int);\n\
1634 CREATE VIEW pub.v AS SELECT id FROM pub.base;\n\
1635 CREATE TABLE pub.c (LIKE pub.v);\n",
1636 )
1637 .unwrap();
1638 let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
1639 assert!(
1640 format!("{err}").contains("LIKE source"),
1641 "error should mention 'LIKE source', got: {err}"
1642 );
1643 }
1644
1645 #[test]
1648 fn including_all_no_double_copy_of_check() {
1649 use crate::ir::constraint::ConstraintKind::{Check, PrimaryKey, Unique};
1650 let dir = tempfile::tempdir().unwrap();
1651 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1652 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1653 std::fs::write(
1654 dir.path().join("pub/t.sql"),
1655 "CREATE TABLE pub.base (n int CHECK (n > 0), id int PRIMARY KEY, e text UNIQUE);\n\
1656 CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
1657 )
1658 .unwrap();
1659 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1660 let c = cat
1661 .tables
1662 .iter()
1663 .find(|t| t.qname.name.as_str() == "c")
1664 .unwrap();
1665 let check_count = c
1666 .constraints
1667 .iter()
1668 .filter(|k| matches!(k.kind, Check { .. }))
1669 .count();
1670 let pk_count = c
1671 .constraints
1672 .iter()
1673 .filter(|k| matches!(k.kind, PrimaryKey { .. }))
1674 .count();
1675 let uq_count = c
1676 .constraints
1677 .iter()
1678 .filter(|k| matches!(k.kind, Unique { .. }))
1679 .count();
1680 assert_eq!(
1681 check_count, 1,
1682 "expected exactly 1 Check, got {check_count}"
1683 );
1684 assert_eq!(pk_count, 1, "expected exactly 1 PrimaryKey, got {pk_count}");
1685 assert_eq!(uq_count, 1, "expected exactly 1 Unique, got {uq_count}");
1686 }
1687
1688 #[test]
1692 fn constraint_and_index_share_name_namespace() {
1693 let dir = tempfile::tempdir().unwrap();
1694 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1695 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1696 std::fs::write(
1697 dir.path().join("pub/t.sql"),
1698 "CREATE TABLE pub.base (a int UNIQUE);\n\
1701 CREATE INDEX ON pub.base (a);\n\
1702 CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
1703 )
1704 .unwrap();
1705 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1706 let c = cat
1707 .tables
1708 .iter()
1709 .find(|t| t.qname.name.as_str() == "c")
1710 .unwrap();
1711 let uq_name = c
1713 .constraints
1714 .iter()
1715 .find(|k| matches!(k.kind, crate::ir::constraint::ConstraintKind::Unique { .. }))
1716 .map(|k| k.qname.name.as_str().to_string())
1717 .expect("expected a Unique constraint on c");
1718 let idx_name = cat
1720 .indexes
1721 .iter()
1722 .find(|i| i.on.qname().name.as_str() == "c")
1723 .map(|i| i.qname.name.as_str().to_string())
1724 .expect("expected a plain index on c");
1725 assert_ne!(
1727 uq_name, idx_name,
1728 "Unique constraint name and plain index name must not collide: both are {uq_name:?}"
1729 );
1730 assert_eq!(
1732 uq_name, "c_a_key",
1733 "Unique constraint should be c_a_key, got {uq_name:?}"
1734 );
1735 assert_eq!(
1736 idx_name, "c_a_idx",
1737 "Plain index should be c_a_idx, got {idx_name:?}"
1738 );
1739 }
1740
1741 #[test]
1745 fn chained_like_including_all_propagates() {
1746 use crate::ir::constraint::ConstraintKind::PrimaryKey;
1747 let dir = tempfile::tempdir().unwrap();
1748 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1749 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1750 std::fs::write(
1751 dir.path().join("pub/t.sql"),
1752 "CREATE TABLE pub.base (id int PRIMARY KEY);\n\
1753 CREATE TABLE pub.mid (LIKE pub.base INCLUDING ALL);\n\
1754 CREATE TABLE pub.leaf (LIKE pub.mid INCLUDING ALL);\n",
1755 )
1756 .unwrap();
1757 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1758 let leaf = cat
1759 .tables
1760 .iter()
1761 .find(|t| t.qname.name.as_str() == "leaf")
1762 .unwrap();
1763 assert!(
1765 leaf.columns.iter().any(|c| c.name.as_str() == "id"),
1766 "leaf must have the id column propagated via mid"
1767 );
1768 let pk = leaf
1770 .constraints
1771 .iter()
1772 .find(|k| matches!(k.kind, PrimaryKey { .. }))
1773 .expect("leaf must have a PrimaryKey constraint");
1774 assert_eq!(
1775 pk.qname.name.as_str(),
1776 "leaf_pkey",
1777 "leaf PK should be named leaf_pkey, got {:?}",
1778 pk.qname.name.as_str()
1779 );
1780 }
1781}