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 for c in &src.constraints {
252 if matches!(c.kind, ConstraintKind::Check { .. }) {
253 constraints.push(Constraint {
254 qname: QualifiedName::new(target_schema.clone(), c.qname.name.clone()),
255 kind: c.kind.clone(),
256 deferrable: c.deferrable,
257 comment: None,
258 });
259 }
260 }
261 }
262
263 let indexes = if like.options.indexes() {
267 copy_index_constraints(
268 &src.constraints,
269 target_name,
270 target_schema,
271 &like.location,
272 taken,
273 &mut constraints,
274 )?;
275 copy_plain_indexes(
276 &like.source,
277 target_name,
278 target_schema,
279 &like.location,
280 &catalog.indexes,
281 taken,
282 )?
283 } else {
284 Vec::new()
285 };
286
287 let statistics = if like.options.statistics() {
289 copy_statistics(
290 &like.source,
291 target_name,
292 target_schema,
293 &like.location,
294 &catalog.statistics,
295 taken,
296 )?
297 } else {
298 Vec::new()
299 };
300
301 Ok(LikeSnapshot {
302 explicit_cols_before: like.explicit_cols_before,
303 cols,
304 constraints,
305 indexes,
306 statistics,
307 })
308}
309
310fn copy_index_constraints(
313 src_constraints: &[Constraint],
314 target_name: &Identifier,
315 target_schema: &Identifier,
316 location: &crate::parse::error::SourceLocation,
317 taken: &mut choose_name::TakenNames,
318 out: &mut Vec<Constraint>,
319) -> Result<(), ParseError> {
320 for c in src_constraints {
321 match &c.kind {
322 ConstraintKind::PrimaryKey { columns, include } => {
323 let generated = choose_name::choose_index_name(
324 target_name.as_str(),
325 &[],
326 choose_name::IndexNameKind::Pkey,
327 taken,
328 );
329 let qname = QualifiedName::new(
330 target_schema.clone(),
331 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
332 location: location.clone(),
333 message: format!(
334 "generated PK constraint name {generated:?} is not a valid identifier: {e}"
335 ),
336 })?,
337 );
338 out.push(Constraint {
339 qname,
340 kind: ConstraintKind::PrimaryKey {
341 columns: columns.clone(),
342 include: include.clone(),
343 },
344 deferrable: c.deferrable,
345 comment: None,
346 });
347 }
348 ConstraintKind::Unique {
349 columns,
350 include,
351 nulls_distinct,
352 } => {
353 let col_opts: Vec<Option<&str>> =
354 columns.iter().map(|col| Some(col.as_str())).collect();
355 let generated = choose_name::choose_index_name(
356 target_name.as_str(),
357 &col_opts,
358 choose_name::IndexNameKind::Unique,
359 taken,
360 );
361 let qname = QualifiedName::new(
362 target_schema.clone(),
363 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
364 location: location.clone(),
365 message: format!(
366 "generated UNIQUE constraint name {generated:?} is not a valid identifier: {e}"
367 ),
368 })?,
369 );
370 out.push(Constraint {
371 qname,
372 kind: ConstraintKind::Unique {
373 columns: columns.clone(),
374 include: include.clone(),
375 nulls_distinct: *nulls_distinct,
376 },
377 deferrable: c.deferrable,
378 comment: None,
379 });
380 }
381 ConstraintKind::Check { .. } | ConstraintKind::ForeignKey(_) => {}
384 }
385 }
386 Ok(())
387}
388
389fn copy_plain_indexes(
399 source: &QualifiedName,
400 target_name: &Identifier,
401 target_schema: &Identifier,
402 location: &crate::parse::error::SourceLocation,
403 catalog_indexes: &[Index],
404 taken: &mut choose_name::TakenNames,
405) -> Result<Vec<Index>, ParseError> {
406 let mut out = Vec::new();
407 let source_parent = IndexParent::Table(source.clone());
408 for idx in catalog_indexes.iter().filter(|i| i.on == source_parent) {
409 let col_opts: Vec<Option<&str>> = idx
410 .columns
411 .iter()
412 .map(|col| match &col.expr {
413 IndexColumnExpr::Column(id) => Some(id.as_str()),
414 IndexColumnExpr::Expression(_) => None,
415 })
416 .collect();
417 let generated = choose_name::choose_index_name(
421 target_name.as_str(),
422 &col_opts,
423 choose_name::IndexNameKind::Plain,
424 taken,
425 );
426 let new_qname = QualifiedName::new(
427 target_schema.clone(),
428 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
429 location: location.clone(),
430 message: format!(
431 "generated index name {generated:?} is not a valid identifier: {e}"
432 ),
433 })?,
434 );
435 out.push(Index {
436 qname: new_qname,
437 on: IndexParent::Table(QualifiedName::new(
438 target_schema.clone(),
439 target_name.clone(),
440 )),
441 method: idx.method,
442 columns: idx.columns.clone(),
443 include: idx.include.clone(),
444 unique: idx.unique,
445 nulls_not_distinct: idx.nulls_not_distinct,
446 predicate: idx.predicate.clone(),
447 tablespace: idx.tablespace.clone(),
448 comment: None,
452 storage: idx.storage.clone(),
453 });
454 }
455 Ok(out)
456}
457
458fn copy_statistics(
467 source: &QualifiedName,
468 target_name: &Identifier,
469 target_schema: &Identifier,
470 location: &crate::parse::error::SourceLocation,
471 catalog_statistics: &[Statistic],
472 taken: &mut choose_name::TakenNames,
473) -> Result<Vec<Statistic>, ParseError> {
474 let clone_qname = QualifiedName::new(target_schema.clone(), target_name.clone());
475 let mut out = Vec::new();
476 for stat in catalog_statistics.iter().filter(|s| &s.target == source) {
477 let col_opts: Vec<Option<&str>> = stat
480 .columns
481 .iter()
482 .map(|col| match col {
483 StatisticColumn::Column(id) => Some(id.as_str()),
484 StatisticColumn::Expression(_) => None,
485 })
486 .collect();
487 let generated = choose_name::choose_index_name(
488 target_name.as_str(),
489 &col_opts,
490 choose_name::IndexNameKind::Stat,
491 taken,
492 );
493 let new_qname = QualifiedName::new(
494 target_schema.clone(),
495 Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
496 location: location.clone(),
497 message: format!(
498 "generated statistics name {generated:?} is not a valid identifier: {e}"
499 ),
500 })?,
501 );
502 out.push(Statistic {
503 qname: new_qname,
504 target: clone_qname.clone(),
505 kinds: stat.kinds,
506 columns: stat.columns.clone(),
507 statistics_target: stat.statistics_target,
508 owner: None,
509 comment: None,
510 });
511 }
512 Ok(out)
513}
514
515fn resolve_order(
521 by_target: &BTreeMap<QualifiedName, Vec<&PendingLike>>,
522) -> Result<Vec<QualifiedName>, ParseError> {
523 let mut unresolved: BTreeSet<QualifiedName> = by_target.keys().cloned().collect();
524 let mut ordered: Vec<QualifiedName> = Vec::with_capacity(by_target.len());
525
526 while !unresolved.is_empty() {
527 let ready: Vec<QualifiedName> = unresolved
529 .iter()
530 .filter(|target| {
531 by_target[*target]
532 .iter()
533 .all(|like| !unresolved.contains(&like.source))
534 })
535 .cloned()
536 .collect();
537
538 if ready.is_empty() {
539 let involved = unresolved
541 .iter()
542 .map(ToString::to_string)
543 .collect::<Vec<_>>()
544 .join(", ");
545 let location = unresolved
547 .iter()
548 .next()
549 .and_then(|t| by_target.get(t))
550 .and_then(|likes| likes.first())
551 .map_or_else(
552 || SourceLocation::new(std::path::PathBuf::new(), 0, 0),
553 |like| like.location.clone(),
554 );
555 return Err(ParseError::Structural {
556 location,
557 message: format!("LIKE forms a cycle involving {involved}"),
558 });
559 }
560
561 for target in ready {
562 unresolved.remove(&target);
563 ordered.push(target);
564 }
565 }
566
567 Ok(ordered)
568}
569
570pub fn apply_pending_likes(
583 catalog: &mut Catalog,
584 pending: &[PendingLike],
585) -> Result<(), ParseError> {
586 let mut by_target: BTreeMap<QualifiedName, Vec<&PendingLike>> = BTreeMap::new();
589 for p in pending {
590 by_target.entry(p.target.clone()).or_default().push(p);
591 }
592
593 for target in resolve_order(&by_target)? {
594 let mut likes = by_target.remove(&target).unwrap_or_default();
596 likes.sort_by_key(|p| p.explicit_cols_before); let mut taken = choose_name::TakenNames::from_schema(catalog, &target.schema);
607
608 let mut snapshots: Vec<LikeSnapshot> = Vec::with_capacity(likes.len());
611 for like in &likes {
612 snapshots.push(snapshot_like(
613 like,
614 &target.name,
615 &target.schema,
616 catalog,
617 &mut taken,
618 )?);
619 }
620
621 let mut inserted = 0usize;
626 for snap in snapshots {
627 let n = snap.cols.len();
628 let tgt = catalog
629 .tables
630 .iter_mut()
631 .find(|t| t.qname == target)
632 .ok_or_else(|| ParseError::Structural {
633 location: likes.first().map_or_else(
634 || SourceLocation::new(std::path::PathBuf::new(), 0, 0),
635 |l| l.location.clone(),
636 ),
637 message: format!("LIKE target table {target} vanished"),
638 })?;
639 let at = (snap.explicit_cols_before + inserted).min(tgt.columns.len());
640 tgt.columns.splice(at..at, snap.cols);
641 inserted += n;
642 tgt.constraints.extend(snap.constraints);
643 catalog.indexes.extend(snap.indexes);
647 catalog.statistics.extend(snap.statistics);
651 }
652 }
653 Ok(())
654}
655
656pub fn apply_pending_like_comments(
665 catalog: &mut Catalog,
666 pending: &[PendingLike],
667) -> Result<(), ParseError> {
668 let mut by_target: BTreeMap<QualifiedName, Vec<&PendingLike>> = BTreeMap::new();
672 for p in pending {
673 if p.options.comments() {
674 by_target.entry(p.target.clone()).or_default().push(p);
675 }
676 }
677
678 if by_target.is_empty() {
679 return Ok(());
680 }
681
682 for target in resolve_order(&by_target)? {
683 let likes = by_target.remove(&target).unwrap_or_default();
684 for like in likes {
685 let (src_table_comment, src_col_comments): (
688 Option<String>,
689 Vec<(Identifier, Option<String>)>,
690 ) = {
691 let src = catalog
692 .tables
693 .iter()
694 .find(|t| t.qname == like.source)
695 .ok_or_else(|| ParseError::Structural {
696 location: like.location.clone(),
697 message: format!(
698 "LIKE source table {} not found during comment copy",
699 like.source
700 ),
701 })?;
702 let table_comment = src.comment.clone();
703 let col_comments = src
704 .columns
705 .iter()
706 .map(|c| (c.name.clone(), c.comment.clone()))
707 .collect();
708 (table_comment, col_comments)
709 };
710
711 let tgt = catalog
712 .tables
713 .iter_mut()
714 .find(|t| t.qname == target)
715 .ok_or_else(|| ParseError::Structural {
716 location: like.location.clone(),
717 message: format!("LIKE target table {target} vanished during comment copy"),
718 })?;
719
720 if tgt.comment.is_none() {
722 tgt.comment = src_table_comment;
723 }
724
725 for (col_name, col_comment) in src_col_comments {
727 if let Some(tgt_col) = tgt.columns.iter_mut().find(|c| c.name == col_name)
728 && tgt_col.comment.is_none()
729 {
730 tgt_col.comment = col_comment;
731 }
732 }
733 }
734 }
735 Ok(())
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use crate::ir::column_type::ColumnType;
742 use std::path::PathBuf;
743
744 fn loc() -> SourceLocation {
745 SourceLocation::new(PathBuf::from("t.sql"), 1, 1)
746 }
747 fn id(s: &str) -> Identifier {
748 Identifier::from_unquoted(s).unwrap()
749 }
750 fn qn(s: &str, n: &str) -> QualifiedName {
751 QualifiedName::new(id(s), id(n))
752 }
753
754 #[test]
755 fn end_to_end_bare_like_via_parse_directory() {
756 let dir = tempfile::tempdir().unwrap();
757 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
758 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
759 std::fs::write(
760 dir.path().join("pub/t.sql"),
761 "CREATE TABLE pub.base (id int PRIMARY KEY, name text);\n\
762 CREATE TABLE pub.clone (LIKE pub.base);\n",
763 )
764 .unwrap();
765 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
766 let clone = cat
767 .tables
768 .iter()
769 .find(|t| t.qname.name.as_str() == "clone")
770 .unwrap();
771 assert_eq!(
772 clone
773 .columns
774 .iter()
775 .map(|c| c.name.as_str().to_string())
776 .collect::<Vec<_>>(),
777 vec!["id".to_string(), "name".to_string()]
778 );
779 }
780
781 #[test]
782 fn bare_like_copies_columns_names_types_notnull() {
783 let dir = tempfile::tempdir().unwrap();
784 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
785 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
786 std::fs::write(
787 dir.path().join("pub/t.sql"),
788 "CREATE TABLE pub.base (id integer NOT NULL, name text);\n\
789 CREATE TABLE pub.clone (LIKE pub.base);\n",
790 )
791 .unwrap();
792 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
793 let clone = cat
794 .tables
795 .iter()
796 .find(|t| t.qname.name.as_str() == "clone")
797 .unwrap();
798 let got: Vec<_> = clone
799 .columns
800 .iter()
801 .map(|c| (c.name.as_str().to_string(), c.ty.clone(), c.nullable))
802 .collect();
803 assert_eq!(
804 got,
805 vec![
806 ("id".into(), ColumnType::Integer, false),
807 ("name".into(), ColumnType::Text, true),
808 ]
809 );
810 assert!(
811 clone.columns.iter().all(|c| c.default.is_none()),
812 "bare LIKE copies no defaults"
813 );
814 }
815
816 #[test]
817 fn like_unknown_source_errors() {
818 let pend = PendingLike {
819 target: qn("pub", "clone"),
820 source: qn("pub", "missing"),
821 options: TableLikeOptions::new(0),
822 explicit_cols_before: 0,
823 location: loc(),
824 };
825 let dir = tempfile::tempdir().unwrap();
829 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
830 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
831 std::fs::write(
832 dir.path().join("pub/t.sql"),
833 "CREATE TABLE pub.clone (id int);\n",
834 )
835 .unwrap();
836 let (mut cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
837 assert!(apply_pending_likes(&mut cat, &[pend]).is_err());
838 }
839
840 #[test]
844 fn chained_like_resolves_dependent_after_source() {
845 let dir = tempfile::tempdir().unwrap();
846 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
847 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
848 std::fs::write(
849 dir.path().join("pub/t.sql"),
850 "CREATE TABLE pub.a (id int PRIMARY KEY, name text);\n\
851 CREATE TABLE pub.z (LIKE pub.a);\n\
852 CREATE TABLE pub.x (LIKE pub.z);\n",
853 )
854 .unwrap();
855 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
856 for tname in ["a", "z", "x"] {
857 let t = cat
858 .tables
859 .iter()
860 .find(|t| t.qname.name.as_str() == tname)
861 .unwrap();
862 assert_eq!(
863 t.columns
864 .iter()
865 .map(|c| c.name.as_str().to_string())
866 .collect::<Vec<_>>(),
867 vec!["id".to_string(), "name".to_string()],
868 "table {tname} should have both copied columns",
869 );
870 }
871 }
872
873 #[test]
878 fn chained_like_resolves_when_dependent_sorts_before_source() {
879 let dir = tempfile::tempdir().unwrap();
880 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
881 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
882 std::fs::write(
883 dir.path().join("pub/t.sql"),
884 "CREATE TABLE pub.base (id int PRIMARY KEY, name text);\n\
885 CREATE TABLE pub.aaa (LIKE pub.zzz);\n\
886 CREATE TABLE pub.zzz (LIKE pub.base);\n",
887 )
888 .unwrap();
889 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
890 for tname in ["base", "aaa", "zzz"] {
891 let t = cat
892 .tables
893 .iter()
894 .find(|t| t.qname.name.as_str() == tname)
895 .unwrap();
896 assert_eq!(
897 t.columns
898 .iter()
899 .map(|c| c.name.as_str().to_string())
900 .collect::<Vec<_>>(),
901 vec!["id".to_string(), "name".to_string()],
902 "table {tname} should have both copied columns",
903 );
904 }
905 }
906
907 #[test]
910 fn self_referential_like_errors() {
911 let dir = tempfile::tempdir().unwrap();
912 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
913 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
914 std::fs::write(
915 dir.path().join("pub/t.sql"),
916 "CREATE TABLE pub.c (LIKE pub.c);\n",
917 )
918 .unwrap();
919 let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
920 assert!(
921 matches!(err, crate::parse::ParseError::Structural { .. }),
922 "got {err:?}"
923 );
924 }
925
926 #[test]
928 fn two_cycle_like_errors() {
929 let dir = tempfile::tempdir().unwrap();
930 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
931 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
932 std::fs::write(
933 dir.path().join("pub/t.sql"),
934 "CREATE TABLE pub.p (LIKE pub.q);\n\
935 CREATE TABLE pub.q (LIKE pub.p);\n",
936 )
937 .unwrap();
938 let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
939 assert!(
940 matches!(err, crate::parse::ParseError::Structural { .. }),
941 "got {err:?}"
942 );
943 }
944
945 #[test]
946 fn like_preserves_position_among_explicit_columns() {
947 let dir = tempfile::tempdir().unwrap();
948 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
949 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
950 std::fs::write(
951 dir.path().join("pub/t.sql"),
952 "CREATE TABLE pub.base (a int, b int);\n\
953 CREATE TABLE pub.c (x int, LIKE pub.base, y text);\n",
954 )
955 .unwrap();
956 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
957 let c = cat
958 .tables
959 .iter()
960 .find(|t| t.qname.name.as_str() == "c")
961 .unwrap();
962 assert_eq!(
963 c.columns
964 .iter()
965 .map(|c| c.name.as_str().to_string())
966 .collect::<Vec<_>>(),
967 vec!["x", "a", "b", "y"]
968 );
969 }
970
971 #[test]
972 fn including_defaults_and_storage_gate_attributes() {
973 let dir = tempfile::tempdir().unwrap();
974 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
975 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
976 std::fs::write(
977 dir.path().join("pub/t.sql"),
978 "CREATE TABLE pub.base (id int DEFAULT 7, doc text STORAGE EXTERNAL);\n\
979 CREATE TABLE pub.bare (LIKE pub.base);\n\
980 CREATE TABLE pub.full (LIKE pub.base INCLUDING DEFAULTS INCLUDING STORAGE);\n",
981 )
982 .unwrap();
983 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
984 let bare = cat
985 .tables
986 .iter()
987 .find(|t| t.qname.name.as_str() == "bare")
988 .unwrap();
989 assert!(bare.columns[0].default.is_none());
990 assert!(bare.columns[1].storage.is_none());
991 let full = cat
992 .tables
993 .iter()
994 .find(|t| t.qname.name.as_str() == "full")
995 .unwrap();
996 assert!(
997 full.columns[0].default.is_some(),
998 "INCLUDING DEFAULTS copies default"
999 );
1000 assert!(
1001 full.columns[1].storage.is_some(),
1002 "INCLUDING STORAGE copies storage"
1003 );
1004 }
1005
1006 #[test]
1007 fn multiple_like_clauses_expand_in_order() {
1008 let dir = tempfile::tempdir().unwrap();
1009 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1010 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1011 std::fs::write(
1012 dir.path().join("pub/t.sql"),
1013 "CREATE TABLE pub.l (a int);\nCREATE TABLE pub.r (b int);\n\
1014 CREATE TABLE pub.c (LIKE pub.l, mid int, LIKE pub.r);\n",
1015 )
1016 .unwrap();
1017 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1018 let c = cat
1019 .tables
1020 .iter()
1021 .find(|t| t.qname.name.as_str() == "c")
1022 .unwrap();
1023 assert_eq!(
1024 c.columns
1025 .iter()
1026 .map(|c| c.name.as_str().to_string())
1027 .collect::<Vec<_>>(),
1028 vec!["a", "mid", "b"]
1029 );
1030 }
1031
1032 #[test]
1035 fn including_comments_copies_table_comment() {
1036 let dir = tempfile::tempdir().unwrap();
1037 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1038 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1039 std::fs::write(
1040 dir.path().join("pub/t.sql"),
1041 "CREATE TABLE pub.base (id int);\n\
1042 COMMENT ON TABLE pub.base IS 'hi';\n\
1043 CREATE TABLE pub.c (LIKE pub.base INCLUDING COMMENTS);\n",
1044 )
1045 .unwrap();
1046 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1047 let c = cat
1048 .tables
1049 .iter()
1050 .find(|t| t.qname.name.as_str() == "c")
1051 .unwrap();
1052 assert_eq!(
1053 c.comment.as_deref(),
1054 Some("hi"),
1055 "INCLUDING COMMENTS should propagate the source table comment to the clone"
1056 );
1057 }
1058
1059 #[test]
1060 fn including_comments_copies_column_comment() {
1061 let dir = tempfile::tempdir().unwrap();
1062 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1063 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1064 std::fs::write(
1065 dir.path().join("pub/t.sql"),
1066 "CREATE TABLE pub.base (id int);\n\
1067 COMMENT ON COLUMN pub.base.id IS 'col';\n\
1068 CREATE TABLE pub.clone (LIKE pub.base INCLUDING COMMENTS);\n",
1069 )
1070 .unwrap();
1071 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1072 let clone = cat
1073 .tables
1074 .iter()
1075 .find(|t| t.qname.name.as_str() == "clone")
1076 .unwrap();
1077 let id_col = clone
1078 .columns
1079 .iter()
1080 .find(|c| c.name.as_str() == "id")
1081 .unwrap();
1082 assert_eq!(
1083 id_col.comment.as_deref(),
1084 Some("col"),
1085 "INCLUDING COMMENTS should propagate the source column comment to the clone"
1086 );
1087 }
1088
1089 #[test]
1090 fn bare_like_does_not_copy_comments() {
1091 let dir = tempfile::tempdir().unwrap();
1092 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1093 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1094 std::fs::write(
1095 dir.path().join("pub/t.sql"),
1096 "CREATE TABLE pub.base (id int);\n\
1097 COMMENT ON TABLE pub.base IS 'tbl';\n\
1098 COMMENT ON COLUMN pub.base.id IS 'col';\n\
1099 CREATE TABLE pub.clone (LIKE pub.base);\n",
1100 )
1101 .unwrap();
1102 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1103 let clone = cat
1104 .tables
1105 .iter()
1106 .find(|t| t.qname.name.as_str() == "clone")
1107 .unwrap();
1108 assert!(
1109 clone.comment.is_none(),
1110 "bare LIKE must not copy table comment"
1111 );
1112 assert!(
1113 clone.columns.iter().all(|c| c.comment.is_none()),
1114 "bare LIKE must not copy column comments"
1115 );
1116 }
1117
1118 #[test]
1123 fn comment_on_clone_like_derived_column_succeeds() {
1124 let dir = tempfile::tempdir().unwrap();
1125 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1126 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1127 std::fs::write(
1128 dir.path().join("pub/t.sql"),
1129 "CREATE TABLE pub.base (id int);\n\
1130 CREATE TABLE pub.clone (LIKE pub.base);\n\
1131 COMMENT ON COLUMN pub.clone.id IS 'x';\n",
1132 )
1133 .unwrap();
1134 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1135 let clone = cat
1136 .tables
1137 .iter()
1138 .find(|t| t.qname.name.as_str() == "clone")
1139 .unwrap();
1140 let id_col = clone
1141 .columns
1142 .iter()
1143 .find(|c| c.name.as_str() == "id")
1144 .unwrap();
1145 assert_eq!(
1146 id_col.comment.as_deref(),
1147 Some("x"),
1148 "COMMENT ON COLUMN clone.id must apply to the LIKE-derived column"
1149 );
1150 }
1151
1152 #[test]
1153 fn explicit_clone_comment_wins_over_including_comments() {
1154 let dir = tempfile::tempdir().unwrap();
1155 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1156 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1157 std::fs::write(
1158 dir.path().join("pub/t.sql"),
1159 "CREATE TABLE pub.base (id int);\n\
1160 COMMENT ON TABLE pub.base IS 'from_base';\n\
1161 CREATE TABLE pub.clone (LIKE pub.base INCLUDING COMMENTS);\n\
1162 COMMENT ON TABLE pub.clone IS 'explicit';\n",
1163 )
1164 .unwrap();
1165 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1166 let clone = cat
1167 .tables
1168 .iter()
1169 .find(|t| t.qname.name.as_str() == "clone")
1170 .unwrap();
1171 assert_eq!(
1172 clone.comment.as_deref(),
1173 Some("explicit"),
1174 "explicit COMMENT ON TABLE clone must win over INCLUDING COMMENTS propagation"
1175 );
1176 }
1177
1178 #[test]
1181 fn including_constraints_copies_check() {
1182 let dir = tempfile::tempdir().unwrap();
1183 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1184 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1185 std::fs::write(
1186 dir.path().join("pub/t.sql"),
1187 "CREATE TABLE pub.base (n int, CONSTRAINT n_pos CHECK (n > 0));\n\
1188 CREATE TABLE pub.bare (LIKE pub.base);\n\
1189 CREATE TABLE pub.c (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1190 )
1191 .unwrap();
1192 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1193 let bare = cat
1194 .tables
1195 .iter()
1196 .find(|t| t.qname.name.as_str() == "bare")
1197 .unwrap();
1198 assert!(
1199 bare.constraints
1200 .iter()
1201 .all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. })),
1202 "bare LIKE must not copy CHECK constraints"
1203 );
1204 let c = cat
1205 .tables
1206 .iter()
1207 .find(|t| t.qname.name.as_str() == "c")
1208 .unwrap();
1209 assert_eq!(
1210 c.constraints
1211 .iter()
1212 .filter(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1213 .count(),
1214 1,
1215 "INCLUDING CONSTRAINTS should copy exactly one CHECK constraint"
1216 );
1217 }
1218
1219 #[test]
1220 fn including_constraints_does_not_copy_pk_or_unique() {
1221 let dir = tempfile::tempdir().unwrap();
1222 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1223 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1224 std::fs::write(
1225 dir.path().join("pub/t.sql"),
1226 "CREATE TABLE pub.base (id int PRIMARY KEY, e text UNIQUE, n int CHECK (n>0));\n\
1227 CREATE TABLE pub.c (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1228 )
1229 .unwrap();
1230 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1231 let c = cat
1232 .tables
1233 .iter()
1234 .find(|t| t.qname.name.as_str() == "c")
1235 .unwrap();
1236 assert_eq!(
1237 c.constraints
1238 .iter()
1239 .filter(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1240 .count(),
1241 1,
1242 "INCLUDING CONSTRAINTS should copy the CHECK constraint"
1243 );
1244 assert!(
1245 c.constraints.iter().all(|c| !matches!(
1246 c.kind,
1247 crate::ir::constraint::ConstraintKind::PrimaryKey { .. }
1248 )),
1249 "INCLUDING CONSTRAINTS must not copy PrimaryKey (belongs to INCLUDING INDEXES)"
1250 );
1251 assert!(
1252 c.constraints
1253 .iter()
1254 .all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Unique { .. })),
1255 "INCLUDING CONSTRAINTS must not copy Unique (belongs to INCLUDING INDEXES)"
1256 );
1257 }
1258
1259 #[test]
1265 fn like_constraints_copies_check_name_verbatim() {
1266 let dir = tempfile::tempdir().unwrap();
1267 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1268 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1269 std::fs::write(
1271 dir.path().join("pub/t.sql"),
1272 "CREATE TABLE pub.base (n int CHECK (n > 0));\n\
1273 CREATE TABLE pub.clone (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1274 )
1275 .unwrap();
1276 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1277 let clone = cat
1278 .tables
1279 .iter()
1280 .find(|t| t.qname.name.as_str() == "clone")
1281 .unwrap();
1282 let check = clone
1283 .constraints
1284 .iter()
1285 .find(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1286 .expect("clone must have a copied CHECK constraint");
1287 assert_eq!(
1288 check.qname.name.as_str(),
1289 "base_n_check",
1290 "unnamed source CHECK name must be copied verbatim (base_n_check), got {:?}",
1291 check.qname.name.as_str(),
1292 );
1293 assert_eq!(
1294 check.qname.schema.as_str(),
1295 "pub",
1296 "copied CHECK must be in clone's schema"
1297 );
1298 }
1299
1300 #[test]
1304 fn like_constraints_preserves_explicit_check_name() {
1305 let dir = tempfile::tempdir().unwrap();
1306 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1307 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1308 std::fs::write(
1310 dir.path().join("pub/t.sql"),
1311 "CREATE TABLE pub.base (n int, CONSTRAINT n_pos CHECK (n > 0));\n\
1312 CREATE TABLE pub.clone (LIKE pub.base INCLUDING CONSTRAINTS);\n",
1313 )
1314 .unwrap();
1315 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1316 let clone = cat
1317 .tables
1318 .iter()
1319 .find(|t| t.qname.name.as_str() == "clone")
1320 .unwrap();
1321 let check = clone
1322 .constraints
1323 .iter()
1324 .find(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
1325 .expect("clone must have a copied CHECK constraint");
1326 assert_eq!(
1327 check.qname.name.as_str(),
1328 "n_pos",
1329 "explicitly-named source CHECK must be preserved as n_pos, got {:?}",
1330 check.qname.name.as_str(),
1331 );
1332 assert_eq!(
1333 check.qname.schema.as_str(),
1334 "pub",
1335 "copied CHECK must be in clone's schema"
1336 );
1337 }
1338
1339 #[test]
1342 fn including_indexes_copies_pk_and_unique_with_pg_names() {
1343 use crate::ir::constraint::ConstraintKind::{PrimaryKey, Unique};
1344 let dir = tempfile::tempdir().unwrap();
1345 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1346 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1347 std::fs::write(
1348 dir.path().join("pub/t.sql"),
1349 "CREATE TABLE pub.base (id int PRIMARY KEY, email text UNIQUE);\n\
1350 CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
1351 )
1352 .unwrap();
1353 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1354 let c = cat
1355 .tables
1356 .iter()
1357 .find(|t| t.qname.name.as_str() == "c")
1358 .unwrap();
1359 let names: Vec<_> = c
1360 .constraints
1361 .iter()
1362 .map(|k| k.qname.name.as_str().to_string())
1363 .collect();
1364 assert!(names.contains(&"c_pkey".to_string()), "got {names:?}");
1365 assert!(names.contains(&"c_email_key".to_string()), "got {names:?}");
1366 assert_eq!(
1367 c.constraints
1368 .iter()
1369 .filter(|k| matches!(k.kind, PrimaryKey { .. }))
1370 .count(),
1371 1
1372 );
1373 assert_eq!(
1374 c.constraints
1375 .iter()
1376 .filter(|k| matches!(k.kind, Unique { .. }))
1377 .count(),
1378 1
1379 );
1380 }
1381
1382 #[test]
1383 fn bare_like_copies_no_pk_or_unique() {
1384 let dir = tempfile::tempdir().unwrap();
1385 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1386 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1387 std::fs::write(
1388 dir.path().join("pub/t.sql"),
1389 "CREATE TABLE pub.base (id int PRIMARY KEY, email text UNIQUE);\n\
1390 CREATE TABLE pub.d (LIKE pub.base);\n",
1391 )
1392 .unwrap();
1393 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1394 let d = cat
1395 .tables
1396 .iter()
1397 .find(|t| t.qname.name.as_str() == "d")
1398 .unwrap();
1399 assert!(
1400 d.constraints.iter().all(|c| !matches!(
1401 c.kind,
1402 crate::ir::constraint::ConstraintKind::PrimaryKey { .. }
1403 )),
1404 "bare LIKE must not copy PK"
1405 );
1406 assert!(
1407 d.constraints
1408 .iter()
1409 .all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Unique { .. })),
1410 "bare LIKE must not copy UNIQUE"
1411 );
1412 }
1413
1414 #[test]
1419 fn including_indexes_copies_plain_index() {
1420 let dir = tempfile::tempdir().unwrap();
1421 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1422 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1423 std::fs::write(
1424 dir.path().join("pub/t.sql"),
1425 "CREATE TABLE pub.base (a int, b int);\n\
1426 CREATE INDEX ON pub.base (a, b);\n\
1427 CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
1428 )
1429 .unwrap();
1430 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1431 let idx: Vec<_> = cat
1432 .indexes
1433 .iter()
1434 .filter(|i| i.on.qname().name.as_str() == "c")
1435 .map(|i| i.qname.name.as_str().to_string())
1436 .collect();
1437 assert_eq!(idx, vec!["c_a_b_idx".to_string()], "got {idx:?}");
1438 }
1439
1440 #[test]
1444 fn unique_index_keeps_idx_suffix() {
1445 let dir = tempfile::tempdir().unwrap();
1446 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1447 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1448 std::fs::write(
1449 dir.path().join("pub/t.sql"),
1450 "CREATE TABLE pub.base (a int);\n\
1451 CREATE UNIQUE INDEX ON pub.base (a);\n\
1452 CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
1453 )
1454 .unwrap();
1455 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1456 let copied: Vec<_> = cat
1457 .indexes
1458 .iter()
1459 .filter(|i| i.on.qname().name.as_str() == "c")
1460 .collect();
1461 assert_eq!(
1462 copied.len(),
1463 1,
1464 "expected exactly one copied index, got {copied:?}"
1465 );
1466 assert_eq!(
1467 copied[0].qname.name.as_str(),
1468 "c_a_idx",
1469 "unique plain index must use _idx suffix, not _key"
1470 );
1471 assert!(copied[0].unique, "copied index must preserve unique flag");
1472 }
1473
1474 #[test]
1476 fn bare_like_copies_no_indexes() {
1477 let dir = tempfile::tempdir().unwrap();
1478 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1479 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1480 std::fs::write(
1481 dir.path().join("pub/t.sql"),
1482 "CREATE TABLE pub.base (a int, b int);\n\
1483 CREATE INDEX ON pub.base (a, b);\n\
1484 CREATE TABLE pub.d (LIKE pub.base);\n",
1485 )
1486 .unwrap();
1487 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1488 let targeting_d: Vec<_> = cat
1489 .indexes
1490 .iter()
1491 .filter(|i| i.on.qname().name.as_str() == "d")
1492 .collect();
1493 assert!(
1494 targeting_d.is_empty(),
1495 "bare LIKE must not copy plain indexes, got {targeting_d:?}"
1496 );
1497 }
1498
1499 #[test]
1502 fn including_statistics_copies_stats() {
1503 let dir = tempfile::tempdir().unwrap();
1504 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1505 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1506 std::fs::write(
1507 dir.path().join("pub/t.sql"),
1508 "CREATE TABLE pub.base (a int, b int);\n\
1509 CREATE STATISTICS pub.base_stat (ndistinct) ON a, b FROM pub.base;\n\
1510 CREATE TABLE pub.c (LIKE pub.base INCLUDING STATISTICS);\n",
1511 )
1512 .unwrap();
1513 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1514 let copied: Vec<_> = cat
1515 .statistics
1516 .iter()
1517 .filter(|s| s.target.name.as_str() == "c")
1518 .collect();
1519 assert_eq!(copied.len(), 1, "expected one copied statistic");
1520 assert_eq!(
1521 copied[0].qname.name.as_str(),
1522 "c_a_b_stat",
1523 "generated statistic qname should be c_a_b_stat, got {:?}",
1524 copied[0].qname.name.as_str(),
1525 );
1526 }
1527
1528 #[test]
1529 fn bare_like_copies_no_stats() {
1530 let dir = tempfile::tempdir().unwrap();
1531 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1532 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1533 std::fs::write(
1534 dir.path().join("pub/t.sql"),
1535 "CREATE TABLE pub.base (a int, b int);\n\
1536 CREATE STATISTICS pub.base_stat (ndistinct) ON a, b FROM pub.base;\n\
1537 CREATE TABLE pub.d (LIKE pub.base);\n",
1538 )
1539 .unwrap();
1540 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1541 let targeting_d: Vec<_> = cat
1542 .statistics
1543 .iter()
1544 .filter(|s| s.target.name.as_str() == "d")
1545 .collect();
1546 assert!(
1547 targeting_d.is_empty(),
1548 "bare LIKE must not copy statistics, got {targeting_d:?}"
1549 );
1550 }
1551
1552 #[test]
1557 fn including_all_copies_everything() {
1558 use crate::ir::constraint::ConstraintKind::{Check, PrimaryKey};
1559 let dir = tempfile::tempdir().unwrap();
1560 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1561 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1562 std::fs::write(
1563 dir.path().join("pub/t.sql"),
1564 "CREATE TABLE pub.base (id int PRIMARY KEY DEFAULT 1, n int CHECK (n > 0));\n\
1565 CREATE INDEX ON pub.base (n);\n\
1566 CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
1567 )
1568 .unwrap();
1569 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1570 let c = cat
1571 .tables
1572 .iter()
1573 .find(|t| t.qname.name.as_str() == "c")
1574 .unwrap();
1575 assert!(
1577 c.columns[0].default.is_some(),
1578 "INCLUDING ALL must copy DEFAULT (DEFAULTS bit)"
1579 );
1580 assert!(
1582 c.constraints
1583 .iter()
1584 .any(|k| matches!(k.kind, PrimaryKey { .. })),
1585 "INCLUDING ALL must copy PrimaryKey constraint (INDEXES bit)"
1586 );
1587 assert!(
1589 c.constraints.iter().any(|k| matches!(k.kind, Check { .. })),
1590 "INCLUDING ALL must copy Check constraint (CONSTRAINTS bit)"
1591 );
1592 assert!(
1594 cat.indexes
1595 .iter()
1596 .any(|i| i.on.qname().name.as_str() == "c"),
1597 "INCLUDING ALL must copy plain index (INDEXES bit)"
1598 );
1599 }
1600
1601 #[test]
1603 fn like_non_table_source_errors_clearly() {
1604 let dir = tempfile::tempdir().unwrap();
1605 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1606 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1607 std::fs::write(
1608 dir.path().join("pub/t.sql"),
1609 "CREATE TABLE pub.base (id int);\n\
1610 CREATE VIEW pub.v AS SELECT id FROM pub.base;\n\
1611 CREATE TABLE pub.c (LIKE pub.v);\n",
1612 )
1613 .unwrap();
1614 let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
1615 assert!(
1616 format!("{err}").contains("LIKE source"),
1617 "error should mention 'LIKE source', got: {err}"
1618 );
1619 }
1620
1621 #[test]
1624 fn including_all_no_double_copy_of_check() {
1625 use crate::ir::constraint::ConstraintKind::{Check, PrimaryKey, Unique};
1626 let dir = tempfile::tempdir().unwrap();
1627 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1628 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1629 std::fs::write(
1630 dir.path().join("pub/t.sql"),
1631 "CREATE TABLE pub.base (n int CHECK (n > 0), id int PRIMARY KEY, e text UNIQUE);\n\
1632 CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
1633 )
1634 .unwrap();
1635 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1636 let c = cat
1637 .tables
1638 .iter()
1639 .find(|t| t.qname.name.as_str() == "c")
1640 .unwrap();
1641 let check_count = c
1642 .constraints
1643 .iter()
1644 .filter(|k| matches!(k.kind, Check { .. }))
1645 .count();
1646 let pk_count = c
1647 .constraints
1648 .iter()
1649 .filter(|k| matches!(k.kind, PrimaryKey { .. }))
1650 .count();
1651 let uq_count = c
1652 .constraints
1653 .iter()
1654 .filter(|k| matches!(k.kind, Unique { .. }))
1655 .count();
1656 assert_eq!(
1657 check_count, 1,
1658 "expected exactly 1 Check, got {check_count}"
1659 );
1660 assert_eq!(pk_count, 1, "expected exactly 1 PrimaryKey, got {pk_count}");
1661 assert_eq!(uq_count, 1, "expected exactly 1 Unique, got {uq_count}");
1662 }
1663
1664 #[test]
1668 fn constraint_and_index_share_name_namespace() {
1669 let dir = tempfile::tempdir().unwrap();
1670 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1671 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1672 std::fs::write(
1673 dir.path().join("pub/t.sql"),
1674 "CREATE TABLE pub.base (a int UNIQUE);\n\
1677 CREATE INDEX ON pub.base (a);\n\
1678 CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
1679 )
1680 .unwrap();
1681 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1682 let c = cat
1683 .tables
1684 .iter()
1685 .find(|t| t.qname.name.as_str() == "c")
1686 .unwrap();
1687 let uq_name = c
1689 .constraints
1690 .iter()
1691 .find(|k| matches!(k.kind, crate::ir::constraint::ConstraintKind::Unique { .. }))
1692 .map(|k| k.qname.name.as_str().to_string())
1693 .expect("expected a Unique constraint on c");
1694 let idx_name = cat
1696 .indexes
1697 .iter()
1698 .find(|i| i.on.qname().name.as_str() == "c")
1699 .map(|i| i.qname.name.as_str().to_string())
1700 .expect("expected a plain index on c");
1701 assert_ne!(
1703 uq_name, idx_name,
1704 "Unique constraint name and plain index name must not collide: both are {uq_name:?}"
1705 );
1706 assert_eq!(
1708 uq_name, "c_a_key",
1709 "Unique constraint should be c_a_key, got {uq_name:?}"
1710 );
1711 assert_eq!(
1712 idx_name, "c_a_idx",
1713 "Plain index should be c_a_idx, got {idx_name:?}"
1714 );
1715 }
1716
1717 #[test]
1721 fn chained_like_including_all_propagates() {
1722 use crate::ir::constraint::ConstraintKind::PrimaryKey;
1723 let dir = tempfile::tempdir().unwrap();
1724 std::fs::create_dir_all(dir.path().join("pub")).unwrap();
1725 std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
1726 std::fs::write(
1727 dir.path().join("pub/t.sql"),
1728 "CREATE TABLE pub.base (id int PRIMARY KEY);\n\
1729 CREATE TABLE pub.mid (LIKE pub.base INCLUDING ALL);\n\
1730 CREATE TABLE pub.leaf (LIKE pub.mid INCLUDING ALL);\n",
1731 )
1732 .unwrap();
1733 let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
1734 let leaf = cat
1735 .tables
1736 .iter()
1737 .find(|t| t.qname.name.as_str() == "leaf")
1738 .unwrap();
1739 assert!(
1741 leaf.columns.iter().any(|c| c.name.as_str() == "id"),
1742 "leaf must have the id column propagated via mid"
1743 );
1744 let pk = leaf
1746 .constraints
1747 .iter()
1748 .find(|k| matches!(k.kind, PrimaryKey { .. }))
1749 .expect("leaf must have a PrimaryKey constraint");
1750 assert_eq!(
1751 pk.qname.name.as_str(),
1752 "leaf_pkey",
1753 "leaf PK should be named leaf_pkey, got {:?}",
1754 pk.qname.name.as_str()
1755 );
1756 }
1757}