use std::collections::BTreeMap;
use crate::catalog::read_catalog;
use crate::data::{self, Value};
use crate::file::{FileError, PageReader};
use crate::format::ObjectType;
use crate::table;
#[derive(Debug, Clone)]
pub struct RelationshipColumn {
pub from_column: String,
pub to_column: String,
}
#[derive(Debug, Clone)]
pub struct Relationship {
pub name: String,
pub from_table: String,
pub to_table: String,
pub columns: Vec<RelationshipColumn>,
pub flags: u32,
}
pub mod relationship_flags {
pub const NO_REFERENTIAL_INTEGRITY: u32 = 0x0000_0002;
pub const CASCADE_UPDATE: u32 = 0x0000_0100;
pub const CASCADE_DELETE: u32 = 0x0000_1000;
}
pub fn read_relationships(reader: &mut PageReader) -> Result<Vec<Relationship>, FileError> {
let catalog = read_catalog(reader)?;
let rel_entry = catalog.iter().find(|e| {
e.name == "MSysRelationships"
&& matches!(e.object_type, ObjectType::Table | ObjectType::SystemTable)
});
let rel_page = match rel_entry {
Some(e) => e.table_page,
None => return Ok(Vec::new()),
};
let tdef = table::read_table_def(reader, "MSysRelationships", rel_page)?;
let result = data::read_table_rows(reader, &tdef)?;
result.warn_skipped("MSysRelationships");
let mut rel_name_idx = None;
let mut col_idx = None;
let mut obj_idx = None;
let mut ref_col_idx = None;
let mut ref_obj_idx = None;
let mut grbit_idx = None;
for (i, col) in tdef.columns.iter().enumerate() {
match col.name.as_str() {
"szRelationship" => rel_name_idx = Some(i),
"szColumn" => col_idx = Some(i),
"szObject" => obj_idx = Some(i),
"szReferencedColumn" => ref_col_idx = Some(i),
"szReferencedObject" => ref_obj_idx = Some(i),
"grbit" => grbit_idx = Some(i),
_ => {}
}
}
let rel_name_idx = rel_name_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysRelationships missing szRelationship column",
})?;
let col_idx = col_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysRelationships missing szColumn column",
})?;
let obj_idx = obj_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysRelationships missing szObject column",
})?;
let ref_col_idx = ref_col_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysRelationships missing szReferencedColumn column",
})?;
let ref_obj_idx = ref_obj_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysRelationships missing szReferencedObject column",
})?;
let grbit_idx = grbit_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysRelationships missing grbit column",
})?;
struct RawRow {
from_table: String,
to_table: String,
from_column: String,
to_column: String,
flags: u32,
}
let mut groups: BTreeMap<String, Vec<RawRow>> = BTreeMap::new();
for row in &result.rows {
let name = match row.get(rel_name_idx) {
Some(Value::Text(s)) if !s.is_empty() => s.clone(),
_ => continue,
};
let from_table = match row.get(obj_idx) {
Some(Value::Text(s)) => s.clone(),
_ => continue,
};
let to_table = match row.get(ref_obj_idx) {
Some(Value::Text(s)) => s.clone(),
_ => continue,
};
let from_column = match row.get(col_idx) {
Some(Value::Text(s)) => s.clone(),
_ => continue,
};
let to_column = match row.get(ref_col_idx) {
Some(Value::Text(s)) => s.clone(),
_ => continue,
};
let flags = match row.get(grbit_idx) {
Some(Value::Long(v)) => *v as u32,
_ => 0,
};
groups.entry(name).or_default().push(RawRow {
from_table,
to_table,
from_column,
to_column,
flags,
});
}
let mut relationships = Vec::with_capacity(groups.len());
for (name, raw_rows) in groups {
if raw_rows.is_empty() {
continue;
}
let from_table = raw_rows[0].from_table.clone();
let to_table = raw_rows[0].to_table.clone();
let flags = raw_rows[0].flags;
for raw in &raw_rows[1..] {
if raw.from_table != from_table || raw.to_table != to_table {
log::warn!(
"relationship '{}': inconsistent table references, using first row's values",
name
);
break;
}
if raw.flags != flags {
log::warn!(
"relationship '{}': inconsistent flags, using first row's values",
name
);
break;
}
}
let columns = raw_rows
.into_iter()
.map(|r| RelationshipColumn {
from_column: r.from_column,
to_column: r.to_column,
})
.collect();
relationships.push(Relationship {
name,
from_table,
to_table,
columns,
flags,
});
}
Ok(relationships)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_data_path(relative: &str) -> Option<std::path::PathBuf> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::PathBuf::from(manifest_dir)
.join("../../testdata")
.join(relative);
if path.exists() {
Some(path)
} else {
None
}
}
macro_rules! skip_if_missing {
($path:expr) => {
match test_data_path($path) {
Some(p) => p,
None => {
eprintln!("SKIP: test data not found: {}", $path);
return;
}
}
};
}
#[test]
fn jet4_relationships() {
let path = skip_if_missing!("V2003/testV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let rels = read_relationships(&mut reader).unwrap();
for rel in &rels {
assert!(
!rel.name.is_empty(),
"relationship name should not be empty"
);
assert!(!rel.from_table.is_empty(), "from_table should not be empty");
assert!(!rel.to_table.is_empty(), "to_table should not be empty");
assert!(
!rel.columns.is_empty(),
"relationship should have at least one column pair"
);
for col_pair in &rel.columns {
assert!(
!col_pair.from_column.is_empty(),
"from_column should not be empty"
);
assert!(
!col_pair.to_column.is_empty(),
"to_column should not be empty"
);
}
}
}
#[test]
fn ace12_relationships() {
let path = skip_if_missing!("V2007/testV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let rels = read_relationships(&mut reader).unwrap();
for rel in &rels {
assert!(!rel.name.is_empty());
assert!(!rel.from_table.is_empty());
assert!(!rel.to_table.is_empty());
assert!(!rel.columns.is_empty());
}
}
#[test]
fn jet3_relationships() {
let path = skip_if_missing!("V1997/testV1997.mdb");
let mut reader = PageReader::open(&path).unwrap();
let rels = read_relationships(&mut reader).unwrap();
for rel in &rels {
assert!(!rel.name.is_empty());
}
}
#[test]
fn relationship_flags_check() {
let path = skip_if_missing!("V2003/testV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let rels = read_relationships(&mut reader).unwrap();
for rel in &rels {
let _ = rel.flags & relationship_flags::NO_REFERENTIAL_INTEGRITY;
let _ = rel.flags & relationship_flags::CASCADE_UPDATE;
let _ = rel.flags & relationship_flags::CASCADE_DELETE;
}
}
}