#![expect(
clippy::unwrap_used,
reason = "clippy's allow-*-in-tests covers #[test] fns but not the free helper fns in an integration-test crate; the unwraps in these helpers are the intended failure mechanism"
)]
use geopackage::core::datetime::DateTime;
use geopackage::core::metadata::{MetadataScope, ReferenceScope};
use geopackage::{
ColumnSpec, Error, GeoPackage, GeometrySpec, MetadataTarget, NewMetadata, TableSchemaBuilder,
};
use geopackage_core::types::{ColumnType, GeometryType};
use tempfile::TempDir;
fn stamp() -> DateTime {
DateTime::parse_strict("2020-01-01T00:00:00.000Z").unwrap()
}
fn gpkg() -> (TempDir, GeoPackage) {
let dir = tempfile::tempdir().unwrap();
let gpkg = GeoPackage::create(dir.path().join("m.gpkg")).unwrap();
gpkg.add_epsg_srs(4326).unwrap();
gpkg.create_layer(
&TableSchemaBuilder::new("roads")
.column(ColumnSpec::new("name", ColumnType::Text(None)))
.geometry(GeometrySpec::new(GeometryType::LineString, 4326)),
)
.unwrap();
(dir, gpkg)
}
fn iso(gpkg: &GeoPackage, body: &str) -> i64 {
gpkg.add_metadata(&NewMetadata::new(
MetadataScope::Dataset,
"http://www.isotc211.org/2005/gmd",
body,
))
.unwrap()
}
#[test]
fn a_file_without_the_extension_reads_as_empty_rather_than_failing() {
let (_dir, gpkg) = gpkg();
assert!(gpkg.metadata().unwrap().is_empty());
assert!(gpkg.metadata_references().unwrap().is_empty());
assert_eq!(gpkg.metadata_record(1).unwrap(), None);
}
#[test]
fn adding_a_record_creates_the_tables_and_registers_both() {
let (_dir, gpkg) = gpkg();
let id = iso(&gpkg, "<gmd:MD_Metadata/>");
assert_eq!(id, 1);
let rows: Vec<(String, Option<String>, String)> = gpkg
.connection()
.prepare(
"SELECT extension_name, table_name, scope FROM gpkg_extensions \
WHERE extension_name = 'gpkg_metadata' ORDER BY table_name",
)
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(
rows,
vec![
(
"gpkg_metadata".to_owned(),
Some("gpkg_metadata".to_owned()),
"read-write".to_owned()
),
(
"gpkg_metadata".to_owned(),
Some("gpkg_metadata_reference".to_owned()),
"read-write".to_owned()
),
]
);
}
#[test]
fn a_record_round_trips_including_its_payload() {
let (_dir, gpkg) = gpkg();
let body = "<gmd:MD_Metadata><gmd:fileIdentifier>x</gmd:fileIdentifier></gmd:MD_Metadata>";
let id = gpkg
.add_metadata(
&NewMetadata::new(MetadataScope::Series, "http://example.invalid/std", body)
.mime_type("application/xml"),
)
.unwrap();
let record = gpkg.metadata_record(id).unwrap().expect("the record");
assert_eq!(record.scope, MetadataScope::Series);
assert_eq!(record.standard_uri, "http://example.invalid/std");
assert_eq!(record.mime_type, "application/xml");
assert_eq!(record.metadata, body);
}
#[test]
fn an_unlisted_scope_survives_a_round_trip() {
let (_dir, gpkg) = gpkg();
let scope = MetadataScope::Other("x-acme_survey".to_owned());
let id = gpkg
.add_metadata(&NewMetadata::new(
scope.clone(),
"http://example.invalid/std",
"{}",
))
.unwrap();
assert_eq!(gpkg.metadata_record(id).unwrap().unwrap().scope, scope);
}
#[test]
fn every_reference_scope_round_trips_through_its_target() {
let (_dir, gpkg) = gpkg();
let id = iso(&gpkg, "<gmd:MD_Metadata/>");
let targets = [
MetadataTarget::GeoPackage,
MetadataTarget::Table {
table_name: "roads".to_owned(),
},
MetadataTarget::Column {
table_name: "roads".to_owned(),
column_name: "name".to_owned(),
},
MetadataTarget::Row {
table_name: "roads".to_owned(),
row_id: 7,
},
MetadataTarget::Cell {
table_name: "roads".to_owned(),
column_name: "name".to_owned(),
row_id: 7,
},
];
for target in &targets {
gpkg.add_metadata_reference(id, target, stamp(), None)
.unwrap();
}
for target in &targets {
let found = gpkg.metadata_for(target).unwrap();
assert_eq!(found.len(), 1, "{target:?}");
assert_eq!(found[0].scope, target.scope());
assert_eq!(found[0].md_file_id, id);
}
type StoredReference = (String, Option<String>, Option<String>, Option<i64>);
let stored: Vec<StoredReference> = gpkg
.connection()
.prepare(
"SELECT reference_scope, table_name, column_name, row_id_value \
FROM gpkg_metadata_reference ORDER BY reference_scope",
)
.unwrap()
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(
stored,
vec![
(
"column".to_owned(),
Some("roads".to_owned()),
Some("name".to_owned()),
None
),
("geopackage".to_owned(), None, None, None),
("row".to_owned(), Some("roads".to_owned()), None, Some(7)),
(
"row/col".to_owned(),
Some("roads".to_owned()),
Some("name".to_owned()),
Some(7)
),
("table".to_owned(), Some("roads".to_owned()), None, None),
]
);
}
#[test]
fn the_timestamp_is_written_in_the_spec_datetime_form() {
let (_dir, gpkg) = gpkg();
let id = iso(&gpkg, "<gmd:MD_Metadata/>");
gpkg.add_metadata_reference(id, &MetadataTarget::GeoPackage, stamp(), None)
.unwrap();
let stored: String = gpkg
.connection()
.query_row("SELECT timestamp FROM gpkg_metadata_reference", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(stored, "2020-01-01T00:00:00.000Z");
assert_eq!(DateTime::parse_strict(&stored).unwrap(), stamp());
}
#[test]
fn a_reference_to_an_absent_record_is_refused() {
let (_dir, gpkg) = gpkg();
match gpkg.add_metadata_reference(99, &MetadataTarget::GeoPackage, stamp(), None) {
Err(Error::NoSuchMetadata { id }) => assert_eq!(id, 99),
other => panic!("expected NoSuchMetadata, got {other:?}"),
}
}
#[test]
fn a_reference_to_a_table_outside_gpkg_contents_is_refused() {
let (_dir, gpkg) = gpkg();
let id = iso(&gpkg, "<gmd:MD_Metadata/>");
let target = MetadataTarget::Table {
table_name: "nowhere".to_owned(),
};
match gpkg.add_metadata_reference(id, &target, stamp(), None) {
Err(Error::NoSuchTable { table_name }) => assert_eq!(table_name, "nowhere"),
other => panic!("expected NoSuchTable, got {other:?}"),
}
}
#[test]
fn a_record_cannot_be_its_own_parent() {
let (_dir, gpkg) = gpkg();
let id = iso(&gpkg, "<gmd:MD_Metadata/>");
match gpkg.add_metadata_reference(id, &MetadataTarget::GeoPackage, stamp(), Some(id)) {
Err(Error::SelfParentedMetadata { md_file_id }) => assert_eq!(md_file_id, id),
other => panic!("expected SelfParentedMetadata, got {other:?}"),
}
}
#[test]
fn ancestors_walk_the_parent_chain_nearest_first() {
let (_dir, gpkg) = gpkg();
let grandparent = iso(&gpkg, "<gmd:MD_Metadata>gp</gmd:MD_Metadata>");
let parent = iso(&gpkg, "<gmd:MD_Metadata>p</gmd:MD_Metadata>");
let child = iso(&gpkg, "<gmd:MD_Metadata>c</gmd:MD_Metadata>");
gpkg.add_metadata_reference(
parent,
&MetadataTarget::Table {
table_name: "roads".to_owned(),
},
stamp(),
Some(grandparent),
)
.unwrap();
gpkg.add_metadata_reference(child, &MetadataTarget::GeoPackage, stamp(), Some(parent))
.unwrap();
let ids: Vec<i64> = gpkg
.metadata_ancestors(child)
.unwrap()
.into_iter()
.map(|record| record.id)
.collect();
assert_eq!(ids, vec![parent, grandparent]);
assert!(gpkg.metadata_ancestors(grandparent).unwrap().is_empty());
assert!(matches!(
gpkg.metadata_ancestors(404),
Err(Error::NoSuchMetadata { id: 404 })
));
}
#[test]
fn a_parent_cycle_is_reported_rather_than_looped_on() {
let (_dir, gpkg) = gpkg();
let first = iso(&gpkg, "<gmd:MD_Metadata>1</gmd:MD_Metadata>");
let second = iso(&gpkg, "<gmd:MD_Metadata>2</gmd:MD_Metadata>");
gpkg.add_metadata_reference(first, &MetadataTarget::GeoPackage, stamp(), Some(second))
.unwrap();
gpkg.connection()
.execute(
"INSERT INTO gpkg_metadata_reference \
(reference_scope, timestamp, md_file_id, md_parent_id) \
VALUES ('geopackage', '2020-01-01T00:00:00.000Z', ?1, ?2)",
[second, first],
)
.unwrap();
assert!(matches!(
gpkg.metadata_ancestors(first),
Err(Error::MetadataCycle { .. })
));
}
#[test]
fn the_gdal_written_fixture_reads() {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path().join("gdal_multilayer_1_4.gpkg");
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("gdal_multilayer_1_4.gpkg");
std::fs::copy(src, &dst).unwrap();
let gpkg = GeoPackage::open(&dst).unwrap();
let records = gpkg.metadata().unwrap();
assert!(!records.is_empty(), "the fixture contains metadata");
assert!(records.iter().all(|r| r.scope == MetadataScope::Dataset));
assert!(records.iter().all(|r| r.mime_type == "text/xml"));
let references = gpkg.metadata_references().unwrap();
assert!(!references.is_empty());
assert!(
references
.iter()
.all(|r| r.scope == ReferenceScope::Table && r.table_name.is_some())
);
let lines = gpkg
.metadata_for(&MetadataTarget::Table {
table_name: "lines".to_owned(),
})
.unwrap();
assert_eq!(lines.len(), 1);
}