use std::collections::HashMap;
use crate::btree::CellValue;
use crate::error::GpkgError;
use crate::gpkg::GeoPackage;
#[derive(Debug, Clone, PartialEq)]
pub struct DataColumn {
pub table_name: String,
pub column_name: String,
pub name: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub mime_type: Option<String>,
pub constraint_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DataColumnsCatalog {
entries: Vec<DataColumn>,
by_table: HashMap<String, Vec<usize>>,
by_constraint: HashMap<String, Vec<usize>>,
}
impl DataColumnsCatalog {
pub fn load(gpkg: &GeoPackage) -> Result<Self, GpkgError> {
let rows = read_data_columns_rows(gpkg)?;
let entry_count = rows.len();
let mut by_table: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_constraint: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, entry) in rows.iter().enumerate() {
by_table
.entry(entry.table_name.clone())
.or_default()
.push(idx);
if let Some(ref cn) = entry.constraint_name {
by_constraint.entry(cn.clone()).or_default().push(idx);
}
}
let mut catalog = Self {
entries: Vec::with_capacity(entry_count),
by_table,
by_constraint,
};
catalog.entries = rows;
Ok(catalog)
}
#[must_use]
pub fn entries(&self) -> &[DataColumn] {
&self.entries
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &DataColumn> {
self.entries.iter()
}
#[must_use]
pub fn for_table(&self, table: &str) -> Vec<&DataColumn> {
self.by_table
.get(table)
.map(|indices| indices.iter().map(|&i| &self.entries[i]).collect())
.unwrap_or_default()
}
#[must_use]
pub fn for_column(&self, table: &str, column: &str) -> Option<&DataColumn> {
self.for_table(table)
.into_iter()
.find(|dc| dc.column_name == column)
}
#[must_use]
pub fn columns_using_constraint(&self, constraint_name: &str) -> Vec<&DataColumn> {
self.by_constraint
.get(constraint_name)
.map(|indices| indices.iter().map(|&i| &self.entries[i]).collect())
.unwrap_or_default()
}
}
pub fn read_data_columns_rows(gpkg: &GeoPackage) -> Result<Vec<DataColumn>, GpkgError> {
let rows = match gpkg.scan_table_by_name("gpkg_data_columns")? {
Some(r) => r,
None => return Ok(Vec::new()),
};
let mut out = Vec::with_capacity(rows.len());
for (_rowid, values) in rows {
if values.len() < 7 {
continue;
}
let table_name = cell_to_string(&values[0]);
if table_name.is_empty() {
continue;
}
let column_name = cell_to_string(&values[1]);
if column_name.is_empty() {
continue;
}
let name = cell_to_optional_string(&values[2]);
let title = cell_to_optional_string(&values[3]);
let description = cell_to_optional_string(&values[4]);
let mime_type = cell_to_optional_string(&values[5]);
let constraint_name = cell_to_optional_string(&values[6]);
out.push(DataColumn {
table_name,
column_name,
name,
title,
description,
mime_type,
constraint_name,
});
}
Ok(out)
}
fn cell_to_string(v: &CellValue) -> String {
match v {
CellValue::Text(s) => s.clone(),
CellValue::Integer(i) => i.to_string(),
CellValue::Float(f) => f.to_string(),
CellValue::Blob(b) => String::from_utf8_lossy(b).into_owned(),
CellValue::Null => String::new(),
}
}
fn cell_to_optional_string(v: &CellValue) -> Option<String> {
match v {
CellValue::Null => None,
CellValue::Text(s) if s.is_empty() => None,
other => Some(cell_to_string(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(table: &str, column: &str, constraint: Option<&str>) -> DataColumn {
DataColumn {
table_name: table.to_owned(),
column_name: column.to_owned(),
name: None,
title: None,
description: None,
mime_type: None,
constraint_name: constraint.map(str::to_owned),
}
}
#[test]
fn test_catalog_len_and_is_empty_in_memory() {
let cat = DataColumnsCatalog {
entries: Vec::new(),
by_table: HashMap::new(),
by_constraint: HashMap::new(),
};
assert!(cat.is_empty());
assert_eq!(cat.len(), 0);
}
#[test]
fn test_catalog_for_table_filters_in_memory() {
let entries = vec![
make_entry("t1", "c1", None),
make_entry("t1", "c2", Some("r1")),
make_entry("t2", "c3", Some("r1")),
];
let mut by_table: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_constraint: HashMap<String, Vec<usize>> = HashMap::new();
for (i, e) in entries.iter().enumerate() {
by_table.entry(e.table_name.clone()).or_default().push(i);
if let Some(ref cn) = e.constraint_name {
by_constraint.entry(cn.clone()).or_default().push(i);
}
}
let cat = DataColumnsCatalog {
entries,
by_table,
by_constraint,
};
assert_eq!(cat.for_table("t1").len(), 2);
assert_eq!(cat.for_table("t2").len(), 1);
assert_eq!(cat.for_table("unknown").len(), 0);
}
#[test]
fn test_catalog_for_column_in_memory() {
let entries = vec![
make_entry("t1", "c1", None),
make_entry("t1", "c2", Some("r1")),
];
let mut by_table: HashMap<String, Vec<usize>> = HashMap::new();
let by_constraint: HashMap<String, Vec<usize>> = HashMap::new();
for (i, e) in entries.iter().enumerate() {
by_table.entry(e.table_name.clone()).or_default().push(i);
}
let cat = DataColumnsCatalog {
entries,
by_table,
by_constraint,
};
assert!(cat.for_column("t1", "c1").is_some());
assert!(cat.for_column("t1", "c2").is_some());
assert!(cat.for_column("t1", "nonexistent").is_none());
}
#[test]
fn test_catalog_columns_using_constraint_in_memory() {
let entries = vec![
make_entry("t1", "c1", Some("size_rule")),
make_entry("t1", "c2", Some("size_rule")),
make_entry("t2", "c3", None),
];
let mut by_table: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_constraint: HashMap<String, Vec<usize>> = HashMap::new();
for (i, e) in entries.iter().enumerate() {
by_table.entry(e.table_name.clone()).or_default().push(i);
if let Some(ref cn) = e.constraint_name {
by_constraint.entry(cn.clone()).or_default().push(i);
}
}
let cat = DataColumnsCatalog {
entries,
by_table,
by_constraint,
};
assert_eq!(cat.columns_using_constraint("size_rule").len(), 2);
assert_eq!(cat.columns_using_constraint("absent").len(), 0);
}
#[test]
fn test_cell_to_string_variants() {
assert_eq!(cell_to_string(&CellValue::Text("hello".into())), "hello");
assert_eq!(cell_to_string(&CellValue::Integer(42)), "42");
assert_eq!(cell_to_string(&CellValue::Null), "");
assert_eq!(cell_to_string(&CellValue::Blob(b"hi".to_vec())), "hi");
}
#[test]
fn test_cell_to_optional_string_null_is_none() {
assert_eq!(cell_to_optional_string(&CellValue::Null), None);
}
#[test]
fn test_cell_to_optional_string_empty_text_is_none() {
assert_eq!(
cell_to_optional_string(&CellValue::Text(String::new())),
None
);
}
#[test]
fn test_cell_to_optional_string_nonempty_text() {
assert_eq!(
cell_to_optional_string(&CellValue::Text("image/png".into())),
Some("image/png".to_owned())
);
}
}