use std::collections::{BTreeMap, BTreeSet};
use pg_query::NodeEnum;
use pg_query::protobuf::CreateStmt;
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::catalog::Catalog;
use crate::ir::column::Column;
use crate::ir::constraint::{Constraint, ConstraintKind};
use crate::ir::index::{Index, IndexColumnExpr, IndexParent};
use crate::ir::statistic::{Statistic, StatisticColumn};
use crate::parse::builder::{choose_name, shared};
use crate::parse::error::{ParseError, SourceLocation};
#[derive(Debug, Clone, Copy)]
pub struct TableLikeOptions(u32);
impl TableLikeOptions {
const COMMENTS: u32 = 1 << 0;
const COMPRESSION: u32 = 1 << 1;
const CONSTRAINTS: u32 = 1 << 2;
const DEFAULTS: u32 = 1 << 3;
const GENERATED: u32 = 1 << 4;
const IDENTITY: u32 = 1 << 5;
const INDEXES: u32 = 1 << 6;
const STATISTICS: u32 = 1 << 7;
const STORAGE: u32 = 1 << 8;
#[must_use]
pub const fn new(bits: u32) -> Self {
Self(bits)
}
#[must_use]
pub const fn comments(self) -> bool {
self.0 & Self::COMMENTS != 0
}
#[must_use]
pub const fn compression(self) -> bool {
self.0 & Self::COMPRESSION != 0
}
#[must_use]
pub const fn constraints(self) -> bool {
self.0 & Self::CONSTRAINTS != 0
}
#[must_use]
pub const fn defaults(self) -> bool {
self.0 & Self::DEFAULTS != 0
}
#[must_use]
pub const fn generated(self) -> bool {
self.0 & Self::GENERATED != 0
}
#[must_use]
pub const fn identity(self) -> bool {
self.0 & Self::IDENTITY != 0
}
#[must_use]
pub const fn indexes(self) -> bool {
self.0 & Self::INDEXES != 0
}
#[must_use]
pub const fn statistics(self) -> bool {
self.0 & Self::STATISTICS != 0
}
#[must_use]
pub const fn storage(self) -> bool {
self.0 & Self::STORAGE != 0
}
}
#[derive(Debug, Clone)]
pub struct PendingLike {
pub target: QualifiedName,
pub source: QualifiedName,
pub options: TableLikeOptions,
pub explicit_cols_before: usize,
pub location: SourceLocation,
}
pub fn extract_pending_likes(
create: &CreateStmt,
target: &QualifiedName,
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<Vec<PendingLike>, ParseError> {
let mut out = Vec::new();
let mut explicit_cols = 0usize;
for elt in &create.table_elts {
match elt.node.as_ref() {
Some(NodeEnum::ColumnDef(_)) => explicit_cols += 1,
Some(NodeEnum::TableLikeClause(like)) => {
let rv = like
.relation
.as_ref()
.ok_or_else(|| ParseError::Structural {
location: location.clone(),
message: "LIKE clause missing source relation".into(),
})?;
let source = shared::resolve_qname(rv, default_schema, location)?;
out.push(PendingLike {
target: target.clone(),
source,
options: TableLikeOptions::new(like.options),
explicit_cols_before: explicit_cols,
location: location.clone(),
});
}
_ => {}
}
}
Ok(out)
}
fn copy_column(src: &Column, opts: TableLikeOptions) -> Column {
Column {
name: src.name.clone(),
ty: src.ty.clone(),
nullable: src.nullable,
collation: src.collation.clone(),
default: if opts.defaults() {
src.default.clone()
} else {
None
},
identity: if opts.identity() {
src.identity.clone()
} else {
None
},
generated: if opts.generated() {
src.generated.clone()
} else {
None
},
storage: if opts.storage() { src.storage } else { None },
compression: if opts.compression() {
src.compression
} else {
None
},
comment: None,
}
}
struct LikeSnapshot {
explicit_cols_before: usize,
cols: Vec<Column>,
constraints: Vec<Constraint>,
indexes: Vec<Index>,
statistics: Vec<Statistic>,
}
fn snapshot_like(
like: &PendingLike,
target_name: &Identifier,
target_schema: &Identifier,
catalog: &Catalog,
taken: &mut choose_name::TakenNames,
) -> Result<LikeSnapshot, ParseError> {
let src = catalog.tables.iter().find(|t| t.qname == like.source)
.ok_or_else(|| {
let is_view = catalog.views.iter().any(|v| v.qname == like.source);
let is_mv = catalog.materialized_views.iter().any(|v| v.qname == like.source);
let message = if is_view {
format!(
"LIKE source {} is a view; LIKE requires a base table (views are not supported as LIKE sources)",
like.source
)
} else if is_mv {
format!(
"LIKE source {} is a materialized view; LIKE requires a base table (materialized views are not supported as LIKE sources)",
like.source
)
} else {
format!(
"LIKE source table {} not found (must be a table declared in the schema)",
like.source
)
};
ParseError::Structural {
location: like.location.clone(),
message,
}
})?;
let cols: Vec<Column> = src
.columns
.iter()
.map(|c| copy_column(c, like.options))
.collect();
let mut constraints: Vec<Constraint> = Vec::new();
if like.options.constraints() {
for c in &src.constraints {
if matches!(c.kind, ConstraintKind::Check { .. }) {
constraints.push(Constraint {
qname: QualifiedName::new(target_schema.clone(), c.qname.name.clone()),
kind: c.kind.clone(),
deferrable: c.deferrable,
comment: None,
});
}
}
}
let indexes = if like.options.indexes() {
copy_index_constraints(
&src.constraints,
target_name,
target_schema,
&like.location,
taken,
&mut constraints,
)?;
copy_plain_indexes(
&like.source,
target_name,
target_schema,
&like.location,
&catalog.indexes,
taken,
)?
} else {
Vec::new()
};
let statistics = if like.options.statistics() {
copy_statistics(
&like.source,
target_name,
target_schema,
&like.location,
&catalog.statistics,
taken,
)?
} else {
Vec::new()
};
Ok(LikeSnapshot {
explicit_cols_before: like.explicit_cols_before,
cols,
constraints,
indexes,
statistics,
})
}
fn copy_index_constraints(
src_constraints: &[Constraint],
target_name: &Identifier,
target_schema: &Identifier,
location: &crate::parse::error::SourceLocation,
taken: &mut choose_name::TakenNames,
out: &mut Vec<Constraint>,
) -> Result<(), ParseError> {
for c in src_constraints {
match &c.kind {
ConstraintKind::PrimaryKey { columns, include } => {
let generated = choose_name::choose_index_name(
target_name.as_str(),
&[],
choose_name::IndexNameKind::Pkey,
taken,
);
let qname = QualifiedName::new(
target_schema.clone(),
Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
location: location.clone(),
message: format!(
"generated PK constraint name {generated:?} is not a valid identifier: {e}"
),
})?,
);
out.push(Constraint {
qname,
kind: ConstraintKind::PrimaryKey {
columns: columns.clone(),
include: include.clone(),
},
deferrable: c.deferrable,
comment: None,
});
}
ConstraintKind::Unique {
columns,
include,
nulls_distinct,
} => {
let col_opts: Vec<Option<&str>> =
columns.iter().map(|col| Some(col.as_str())).collect();
let generated = choose_name::choose_index_name(
target_name.as_str(),
&col_opts,
choose_name::IndexNameKind::Unique,
taken,
);
let qname = QualifiedName::new(
target_schema.clone(),
Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
location: location.clone(),
message: format!(
"generated UNIQUE constraint name {generated:?} is not a valid identifier: {e}"
),
})?,
);
out.push(Constraint {
qname,
kind: ConstraintKind::Unique {
columns: columns.clone(),
include: include.clone(),
nulls_distinct: *nulls_distinct,
},
deferrable: c.deferrable,
comment: None,
});
}
ConstraintKind::Check { .. } | ConstraintKind::ForeignKey(_) => {}
}
}
Ok(())
}
fn copy_plain_indexes(
source: &QualifiedName,
target_name: &Identifier,
target_schema: &Identifier,
location: &crate::parse::error::SourceLocation,
catalog_indexes: &[Index],
taken: &mut choose_name::TakenNames,
) -> Result<Vec<Index>, ParseError> {
let mut out = Vec::new();
let source_parent = IndexParent::Table(source.clone());
for idx in catalog_indexes.iter().filter(|i| i.on == source_parent) {
let col_opts: Vec<Option<&str>> = idx
.columns
.iter()
.map(|col| match &col.expr {
IndexColumnExpr::Column(id) => Some(id.as_str()),
IndexColumnExpr::Expression(_) => None,
})
.collect();
let generated = choose_name::choose_index_name(
target_name.as_str(),
&col_opts,
choose_name::IndexNameKind::Plain,
taken,
);
let new_qname = QualifiedName::new(
target_schema.clone(),
Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
location: location.clone(),
message: format!(
"generated index name {generated:?} is not a valid identifier: {e}"
),
})?,
);
out.push(Index {
qname: new_qname,
on: IndexParent::Table(QualifiedName::new(
target_schema.clone(),
target_name.clone(),
)),
method: idx.method,
columns: idx.columns.clone(),
include: idx.include.clone(),
unique: idx.unique,
nulls_not_distinct: idx.nulls_not_distinct,
predicate: idx.predicate.clone(),
tablespace: idx.tablespace.clone(),
comment: None,
storage: idx.storage.clone(),
});
}
Ok(out)
}
fn copy_statistics(
source: &QualifiedName,
target_name: &Identifier,
target_schema: &Identifier,
location: &crate::parse::error::SourceLocation,
catalog_statistics: &[Statistic],
taken: &mut choose_name::TakenNames,
) -> Result<Vec<Statistic>, ParseError> {
let clone_qname = QualifiedName::new(target_schema.clone(), target_name.clone());
let mut out = Vec::new();
for stat in catalog_statistics.iter().filter(|s| &s.target == source) {
let col_opts: Vec<Option<&str>> = stat
.columns
.iter()
.map(|col| match col {
StatisticColumn::Column(id) => Some(id.as_str()),
StatisticColumn::Expression(_) => None,
})
.collect();
let generated = choose_name::choose_index_name(
target_name.as_str(),
&col_opts,
choose_name::IndexNameKind::Stat,
taken,
);
let new_qname = QualifiedName::new(
target_schema.clone(),
Identifier::from_unquoted(&generated).map_err(|e| ParseError::Structural {
location: location.clone(),
message: format!(
"generated statistics name {generated:?} is not a valid identifier: {e}"
),
})?,
);
out.push(Statistic {
qname: new_qname,
target: clone_qname.clone(),
kinds: stat.kinds,
columns: stat.columns.clone(),
statistics_target: stat.statistics_target,
owner: None,
comment: None,
});
}
Ok(out)
}
fn resolve_order(
by_target: &BTreeMap<QualifiedName, Vec<&PendingLike>>,
) -> Result<Vec<QualifiedName>, ParseError> {
let mut unresolved: BTreeSet<QualifiedName> = by_target.keys().cloned().collect();
let mut ordered: Vec<QualifiedName> = Vec::with_capacity(by_target.len());
while !unresolved.is_empty() {
let ready: Vec<QualifiedName> = unresolved
.iter()
.filter(|target| {
by_target[*target]
.iter()
.all(|like| !unresolved.contains(&like.source))
})
.cloned()
.collect();
if ready.is_empty() {
let involved = unresolved
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
let location = unresolved
.iter()
.next()
.and_then(|t| by_target.get(t))
.and_then(|likes| likes.first())
.map_or_else(
|| SourceLocation::new(std::path::PathBuf::new(), 0, 0),
|like| like.location.clone(),
);
return Err(ParseError::Structural {
location,
message: format!("LIKE forms a cycle involving {involved}"),
});
}
for target in ready {
unresolved.remove(&target);
ordered.push(target);
}
}
Ok(ordered)
}
pub fn apply_pending_likes(
catalog: &mut Catalog,
pending: &[PendingLike],
) -> Result<(), ParseError> {
let mut by_target: BTreeMap<QualifiedName, Vec<&PendingLike>> = BTreeMap::new();
for p in pending {
by_target.entry(p.target.clone()).or_default().push(p);
}
for target in resolve_order(&by_target)? {
let mut likes = by_target.remove(&target).unwrap_or_default();
likes.sort_by_key(|p| p.explicit_cols_before);
let mut taken = choose_name::TakenNames::from_schema(catalog, &target.schema);
let mut snapshots: Vec<LikeSnapshot> = Vec::with_capacity(likes.len());
for like in &likes {
snapshots.push(snapshot_like(
like,
&target.name,
&target.schema,
catalog,
&mut taken,
)?);
}
let mut inserted = 0usize;
for snap in snapshots {
let n = snap.cols.len();
let tgt = catalog
.tables
.iter_mut()
.find(|t| t.qname == target)
.ok_or_else(|| ParseError::Structural {
location: likes.first().map_or_else(
|| SourceLocation::new(std::path::PathBuf::new(), 0, 0),
|l| l.location.clone(),
),
message: format!("LIKE target table {target} vanished"),
})?;
let at = (snap.explicit_cols_before + inserted).min(tgt.columns.len());
tgt.columns.splice(at..at, snap.cols);
inserted += n;
tgt.constraints.extend(snap.constraints);
catalog.indexes.extend(snap.indexes);
catalog.statistics.extend(snap.statistics);
}
}
Ok(())
}
pub fn apply_pending_like_comments(
catalog: &mut Catalog,
pending: &[PendingLike],
) -> Result<(), ParseError> {
let mut by_target: BTreeMap<QualifiedName, Vec<&PendingLike>> = BTreeMap::new();
for p in pending {
if p.options.comments() {
by_target.entry(p.target.clone()).or_default().push(p);
}
}
if by_target.is_empty() {
return Ok(());
}
for target in resolve_order(&by_target)? {
let likes = by_target.remove(&target).unwrap_or_default();
for like in likes {
let (src_table_comment, src_col_comments): (
Option<String>,
Vec<(Identifier, Option<String>)>,
) = {
let src = catalog
.tables
.iter()
.find(|t| t.qname == like.source)
.ok_or_else(|| ParseError::Structural {
location: like.location.clone(),
message: format!(
"LIKE source table {} not found during comment copy",
like.source
),
})?;
let table_comment = src.comment.clone();
let col_comments = src
.columns
.iter()
.map(|c| (c.name.clone(), c.comment.clone()))
.collect();
(table_comment, col_comments)
};
let tgt = catalog
.tables
.iter_mut()
.find(|t| t.qname == target)
.ok_or_else(|| ParseError::Structural {
location: like.location.clone(),
message: format!("LIKE target table {target} vanished during comment copy"),
})?;
if tgt.comment.is_none() {
tgt.comment = src_table_comment;
}
for (col_name, col_comment) in src_col_comments {
if let Some(tgt_col) = tgt.columns.iter_mut().find(|c| c.name == col_name)
&& tgt_col.comment.is_none()
{
tgt_col.comment = col_comment;
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::column_type::ColumnType;
use std::path::PathBuf;
fn loc() -> SourceLocation {
SourceLocation::new(PathBuf::from("t.sql"), 1, 1)
}
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(s: &str, n: &str) -> QualifiedName {
QualifiedName::new(id(s), id(n))
}
#[test]
fn end_to_end_bare_like_via_parse_directory() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY, name text);\n\
CREATE TABLE pub.clone (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
assert_eq!(
clone
.columns
.iter()
.map(|c| c.name.as_str().to_string())
.collect::<Vec<_>>(),
vec!["id".to_string(), "name".to_string()]
);
}
#[test]
fn bare_like_copies_columns_names_types_notnull() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id integer NOT NULL, name text);\n\
CREATE TABLE pub.clone (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
let got: Vec<_> = clone
.columns
.iter()
.map(|c| (c.name.as_str().to_string(), c.ty.clone(), c.nullable))
.collect();
assert_eq!(
got,
vec![
("id".into(), ColumnType::Integer, false),
("name".into(), ColumnType::Text, true),
]
);
assert!(
clone.columns.iter().all(|c| c.default.is_none()),
"bare LIKE copies no defaults"
);
}
#[test]
fn like_unknown_source_errors() {
let pend = PendingLike {
target: qn("pub", "clone"),
source: qn("pub", "missing"),
options: TableLikeOptions::new(0),
explicit_cols_before: 0,
location: loc(),
};
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.clone (id int);\n",
)
.unwrap();
let (mut cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
assert!(apply_pending_likes(&mut cat, &[pend]).is_err());
}
#[test]
fn chained_like_resolves_dependent_after_source() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.a (id int PRIMARY KEY, name text);\n\
CREATE TABLE pub.z (LIKE pub.a);\n\
CREATE TABLE pub.x (LIKE pub.z);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
for tname in ["a", "z", "x"] {
let t = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == tname)
.unwrap();
assert_eq!(
t.columns
.iter()
.map(|c| c.name.as_str().to_string())
.collect::<Vec<_>>(),
vec!["id".to_string(), "name".to_string()],
"table {tname} should have both copied columns",
);
}
}
#[test]
fn chained_like_resolves_when_dependent_sorts_before_source() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY, name text);\n\
CREATE TABLE pub.aaa (LIKE pub.zzz);\n\
CREATE TABLE pub.zzz (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
for tname in ["base", "aaa", "zzz"] {
let t = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == tname)
.unwrap();
assert_eq!(
t.columns
.iter()
.map(|c| c.name.as_str().to_string())
.collect::<Vec<_>>(),
vec!["id".to_string(), "name".to_string()],
"table {tname} should have both copied columns",
);
}
}
#[test]
fn self_referential_like_errors() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.c (LIKE pub.c);\n",
)
.unwrap();
let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
assert!(
matches!(err, crate::parse::ParseError::Structural { .. }),
"got {err:?}"
);
}
#[test]
fn two_cycle_like_errors() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.p (LIKE pub.q);\n\
CREATE TABLE pub.q (LIKE pub.p);\n",
)
.unwrap();
let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
assert!(
matches!(err, crate::parse::ParseError::Structural { .. }),
"got {err:?}"
);
}
#[test]
fn like_preserves_position_among_explicit_columns() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int, b int);\n\
CREATE TABLE pub.c (x int, LIKE pub.base, y text);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
assert_eq!(
c.columns
.iter()
.map(|c| c.name.as_str().to_string())
.collect::<Vec<_>>(),
vec!["x", "a", "b", "y"]
);
}
#[test]
fn including_defaults_and_storage_gate_attributes() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int DEFAULT 7, doc text STORAGE EXTERNAL);\n\
CREATE TABLE pub.bare (LIKE pub.base);\n\
CREATE TABLE pub.full (LIKE pub.base INCLUDING DEFAULTS INCLUDING STORAGE);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let bare = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "bare")
.unwrap();
assert!(bare.columns[0].default.is_none());
assert!(bare.columns[1].storage.is_none());
let full = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "full")
.unwrap();
assert!(
full.columns[0].default.is_some(),
"INCLUDING DEFAULTS copies default"
);
assert!(
full.columns[1].storage.is_some(),
"INCLUDING STORAGE copies storage"
);
}
#[test]
fn multiple_like_clauses_expand_in_order() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.l (a int);\nCREATE TABLE pub.r (b int);\n\
CREATE TABLE pub.c (LIKE pub.l, mid int, LIKE pub.r);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
assert_eq!(
c.columns
.iter()
.map(|c| c.name.as_str().to_string())
.collect::<Vec<_>>(),
vec!["a", "mid", "b"]
);
}
#[test]
fn including_comments_copies_table_comment() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int);\n\
COMMENT ON TABLE pub.base IS 'hi';\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING COMMENTS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
assert_eq!(
c.comment.as_deref(),
Some("hi"),
"INCLUDING COMMENTS should propagate the source table comment to the clone"
);
}
#[test]
fn including_comments_copies_column_comment() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int);\n\
COMMENT ON COLUMN pub.base.id IS 'col';\n\
CREATE TABLE pub.clone (LIKE pub.base INCLUDING COMMENTS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
let id_col = clone
.columns
.iter()
.find(|c| c.name.as_str() == "id")
.unwrap();
assert_eq!(
id_col.comment.as_deref(),
Some("col"),
"INCLUDING COMMENTS should propagate the source column comment to the clone"
);
}
#[test]
fn bare_like_does_not_copy_comments() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int);\n\
COMMENT ON TABLE pub.base IS 'tbl';\n\
COMMENT ON COLUMN pub.base.id IS 'col';\n\
CREATE TABLE pub.clone (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
assert!(
clone.comment.is_none(),
"bare LIKE must not copy table comment"
);
assert!(
clone.columns.iter().all(|c| c.comment.is_none()),
"bare LIKE must not copy column comments"
);
}
#[test]
fn comment_on_clone_like_derived_column_succeeds() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int);\n\
CREATE TABLE pub.clone (LIKE pub.base);\n\
COMMENT ON COLUMN pub.clone.id IS 'x';\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
let id_col = clone
.columns
.iter()
.find(|c| c.name.as_str() == "id")
.unwrap();
assert_eq!(
id_col.comment.as_deref(),
Some("x"),
"COMMENT ON COLUMN clone.id must apply to the LIKE-derived column"
);
}
#[test]
fn explicit_clone_comment_wins_over_including_comments() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int);\n\
COMMENT ON TABLE pub.base IS 'from_base';\n\
CREATE TABLE pub.clone (LIKE pub.base INCLUDING COMMENTS);\n\
COMMENT ON TABLE pub.clone IS 'explicit';\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
assert_eq!(
clone.comment.as_deref(),
Some("explicit"),
"explicit COMMENT ON TABLE clone must win over INCLUDING COMMENTS propagation"
);
}
#[test]
fn including_constraints_copies_check() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (n int, CONSTRAINT n_pos CHECK (n > 0));\n\
CREATE TABLE pub.bare (LIKE pub.base);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING CONSTRAINTS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let bare = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "bare")
.unwrap();
assert!(
bare.constraints
.iter()
.all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. })),
"bare LIKE must not copy CHECK constraints"
);
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
assert_eq!(
c.constraints
.iter()
.filter(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
.count(),
1,
"INCLUDING CONSTRAINTS should copy exactly one CHECK constraint"
);
}
#[test]
fn including_constraints_does_not_copy_pk_or_unique() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY, e text UNIQUE, n int CHECK (n>0));\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING CONSTRAINTS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
assert_eq!(
c.constraints
.iter()
.filter(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
.count(),
1,
"INCLUDING CONSTRAINTS should copy the CHECK constraint"
);
assert!(
c.constraints.iter().all(|c| !matches!(
c.kind,
crate::ir::constraint::ConstraintKind::PrimaryKey { .. }
)),
"INCLUDING CONSTRAINTS must not copy PrimaryKey (belongs to INCLUDING INDEXES)"
);
assert!(
c.constraints
.iter()
.all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Unique { .. })),
"INCLUDING CONSTRAINTS must not copy Unique (belongs to INCLUDING INDEXES)"
);
}
#[test]
fn like_constraints_copies_check_name_verbatim() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (n int CHECK (n > 0));\n\
CREATE TABLE pub.clone (LIKE pub.base INCLUDING CONSTRAINTS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
let check = clone
.constraints
.iter()
.find(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
.expect("clone must have a copied CHECK constraint");
assert_eq!(
check.qname.name.as_str(),
"base_n_check",
"unnamed source CHECK name must be copied verbatim (base_n_check), got {:?}",
check.qname.name.as_str(),
);
assert_eq!(
check.qname.schema.as_str(),
"pub",
"copied CHECK must be in clone's schema"
);
}
#[test]
fn like_constraints_preserves_explicit_check_name() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (n int, CONSTRAINT n_pos CHECK (n > 0));\n\
CREATE TABLE pub.clone (LIKE pub.base INCLUDING CONSTRAINTS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let clone = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "clone")
.unwrap();
let check = clone
.constraints
.iter()
.find(|c| matches!(c.kind, crate::ir::constraint::ConstraintKind::Check { .. }))
.expect("clone must have a copied CHECK constraint");
assert_eq!(
check.qname.name.as_str(),
"n_pos",
"explicitly-named source CHECK must be preserved as n_pos, got {:?}",
check.qname.name.as_str(),
);
assert_eq!(
check.qname.schema.as_str(),
"pub",
"copied CHECK must be in clone's schema"
);
}
#[test]
fn including_indexes_copies_pk_and_unique_with_pg_names() {
use crate::ir::constraint::ConstraintKind::{PrimaryKey, Unique};
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY, email text UNIQUE);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
let names: Vec<_> = c
.constraints
.iter()
.map(|k| k.qname.name.as_str().to_string())
.collect();
assert!(names.contains(&"c_pkey".to_string()), "got {names:?}");
assert!(names.contains(&"c_email_key".to_string()), "got {names:?}");
assert_eq!(
c.constraints
.iter()
.filter(|k| matches!(k.kind, PrimaryKey { .. }))
.count(),
1
);
assert_eq!(
c.constraints
.iter()
.filter(|k| matches!(k.kind, Unique { .. }))
.count(),
1
);
}
#[test]
fn bare_like_copies_no_pk_or_unique() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY, email text UNIQUE);\n\
CREATE TABLE pub.d (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let d = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "d")
.unwrap();
assert!(
d.constraints.iter().all(|c| !matches!(
c.kind,
crate::ir::constraint::ConstraintKind::PrimaryKey { .. }
)),
"bare LIKE must not copy PK"
);
assert!(
d.constraints
.iter()
.all(|c| !matches!(c.kind, crate::ir::constraint::ConstraintKind::Unique { .. })),
"bare LIKE must not copy UNIQUE"
);
}
#[test]
fn including_indexes_copies_plain_index() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int, b int);\n\
CREATE INDEX ON pub.base (a, b);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let idx: Vec<_> = cat
.indexes
.iter()
.filter(|i| i.on.qname().name.as_str() == "c")
.map(|i| i.qname.name.as_str().to_string())
.collect();
assert_eq!(idx, vec!["c_a_b_idx".to_string()], "got {idx:?}");
}
#[test]
fn unique_index_keeps_idx_suffix() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int);\n\
CREATE UNIQUE INDEX ON pub.base (a);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING INDEXES);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let copied: Vec<_> = cat
.indexes
.iter()
.filter(|i| i.on.qname().name.as_str() == "c")
.collect();
assert_eq!(
copied.len(),
1,
"expected exactly one copied index, got {copied:?}"
);
assert_eq!(
copied[0].qname.name.as_str(),
"c_a_idx",
"unique plain index must use _idx suffix, not _key"
);
assert!(copied[0].unique, "copied index must preserve unique flag");
}
#[test]
fn bare_like_copies_no_indexes() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int, b int);\n\
CREATE INDEX ON pub.base (a, b);\n\
CREATE TABLE pub.d (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let targeting_d: Vec<_> = cat
.indexes
.iter()
.filter(|i| i.on.qname().name.as_str() == "d")
.collect();
assert!(
targeting_d.is_empty(),
"bare LIKE must not copy plain indexes, got {targeting_d:?}"
);
}
#[test]
fn including_statistics_copies_stats() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int, b int);\n\
CREATE STATISTICS pub.base_stat (ndistinct) ON a, b FROM pub.base;\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING STATISTICS);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let copied: Vec<_> = cat
.statistics
.iter()
.filter(|s| s.target.name.as_str() == "c")
.collect();
assert_eq!(copied.len(), 1, "expected one copied statistic");
assert_eq!(
copied[0].qname.name.as_str(),
"c_a_b_stat",
"generated statistic qname should be c_a_b_stat, got {:?}",
copied[0].qname.name.as_str(),
);
}
#[test]
fn bare_like_copies_no_stats() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int, b int);\n\
CREATE STATISTICS pub.base_stat (ndistinct) ON a, b FROM pub.base;\n\
CREATE TABLE pub.d (LIKE pub.base);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let targeting_d: Vec<_> = cat
.statistics
.iter()
.filter(|s| s.target.name.as_str() == "d")
.collect();
assert!(
targeting_d.is_empty(),
"bare LIKE must not copy statistics, got {targeting_d:?}"
);
}
#[test]
fn including_all_copies_everything() {
use crate::ir::constraint::ConstraintKind::{Check, PrimaryKey};
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY DEFAULT 1, n int CHECK (n > 0));\n\
CREATE INDEX ON pub.base (n);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
assert!(
c.columns[0].default.is_some(),
"INCLUDING ALL must copy DEFAULT (DEFAULTS bit)"
);
assert!(
c.constraints
.iter()
.any(|k| matches!(k.kind, PrimaryKey { .. })),
"INCLUDING ALL must copy PrimaryKey constraint (INDEXES bit)"
);
assert!(
c.constraints.iter().any(|k| matches!(k.kind, Check { .. })),
"INCLUDING ALL must copy Check constraint (CONSTRAINTS bit)"
);
assert!(
cat.indexes
.iter()
.any(|i| i.on.qname().name.as_str() == "c"),
"INCLUDING ALL must copy plain index (INDEXES bit)"
);
}
#[test]
fn like_non_table_source_errors_clearly() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int);\n\
CREATE VIEW pub.v AS SELECT id FROM pub.base;\n\
CREATE TABLE pub.c (LIKE pub.v);\n",
)
.unwrap();
let err = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap_err();
assert!(
format!("{err}").contains("LIKE source"),
"error should mention 'LIKE source', got: {err}"
);
}
#[test]
fn including_all_no_double_copy_of_check() {
use crate::ir::constraint::ConstraintKind::{Check, PrimaryKey, Unique};
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (n int CHECK (n > 0), id int PRIMARY KEY, e text UNIQUE);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
let check_count = c
.constraints
.iter()
.filter(|k| matches!(k.kind, Check { .. }))
.count();
let pk_count = c
.constraints
.iter()
.filter(|k| matches!(k.kind, PrimaryKey { .. }))
.count();
let uq_count = c
.constraints
.iter()
.filter(|k| matches!(k.kind, Unique { .. }))
.count();
assert_eq!(
check_count, 1,
"expected exactly 1 Check, got {check_count}"
);
assert_eq!(pk_count, 1, "expected exactly 1 PrimaryKey, got {pk_count}");
assert_eq!(uq_count, 1, "expected exactly 1 Unique, got {uq_count}");
}
#[test]
fn constraint_and_index_share_name_namespace() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (a int UNIQUE);\n\
CREATE INDEX ON pub.base (a);\n\
CREATE TABLE pub.c (LIKE pub.base INCLUDING ALL);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let c = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "c")
.unwrap();
let uq_name = c
.constraints
.iter()
.find(|k| matches!(k.kind, crate::ir::constraint::ConstraintKind::Unique { .. }))
.map(|k| k.qname.name.as_str().to_string())
.expect("expected a Unique constraint on c");
let idx_name = cat
.indexes
.iter()
.find(|i| i.on.qname().name.as_str() == "c")
.map(|i| i.qname.name.as_str().to_string())
.expect("expected a plain index on c");
assert_ne!(
uq_name, idx_name,
"Unique constraint name and plain index name must not collide: both are {uq_name:?}"
);
assert_eq!(
uq_name, "c_a_key",
"Unique constraint should be c_a_key, got {uq_name:?}"
);
assert_eq!(
idx_name, "c_a_idx",
"Plain index should be c_a_idx, got {idx_name:?}"
);
}
#[test]
fn chained_like_including_all_propagates() {
use crate::ir::constraint::ConstraintKind::PrimaryKey;
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("pub")).unwrap();
std::fs::write(dir.path().join("pub/_schema.sql"), "CREATE SCHEMA pub;\n").unwrap();
std::fs::write(
dir.path().join("pub/t.sql"),
"CREATE TABLE pub.base (id int PRIMARY KEY);\n\
CREATE TABLE pub.mid (LIKE pub.base INCLUDING ALL);\n\
CREATE TABLE pub.leaf (LIKE pub.mid INCLUDING ALL);\n",
)
.unwrap();
let (cat, _) = crate::parse::parse_directory_with_locations(dir.path(), &[]).unwrap();
let leaf = cat
.tables
.iter()
.find(|t| t.qname.name.as_str() == "leaf")
.unwrap();
assert!(
leaf.columns.iter().any(|c| c.name.as_str() == "id"),
"leaf must have the id column propagated via mid"
);
let pk = leaf
.constraints
.iter()
.find(|k| matches!(k.kind, PrimaryKey { .. }))
.expect("leaf must have a PrimaryKey constraint");
assert_eq!(
pk.qname.name.as_str(),
"leaf_pkey",
"leaf PK should be named leaf_pkey, got {:?}",
pk.qname.name.as_str()
);
}
}