use std::fmt;
use crate::ident;
pub const EXTENSION_NAME: &str = "gpkg_related_tables";
pub const EXTENSION_DEFINITION: &str = "http://www.geopackage.org/18-000.html";
pub const EXTENSION_SCOPE: &str = "read-write";
pub const RELATIONS_TABLE: &str = "gpkgext_relations";
pub const CREATE_GPKGEXT_RELATIONS: &str = "\
CREATE TABLE 'gpkgext_relations' (
id INTEGER PRIMARY KEY AUTOINCREMENT,
base_table_name TEXT NOT NULL,
base_primary_column TEXT NOT NULL DEFAULT 'id',
related_table_name TEXT NOT NULL,
related_primary_column TEXT NOT NULL DEFAULT 'id',
relation_name TEXT NOT NULL,
mapping_table_name TEXT NOT NULL UNIQUE
)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relation {
pub id: i64,
pub base_table_name: String,
pub base_primary_column: String,
pub related_table_name: String,
pub related_primary_column: String,
pub relation_name: RelationName,
pub mapping_table_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RelationName {
Media,
SimpleAttributes,
Features,
Attributes,
Tiles,
Extended {
author: String,
name: String,
},
Other(String),
}
impl RelationName {
pub fn parse(value: &str) -> Self {
match value {
"media" => return Self::Media,
"simple_attributes" => return Self::SimpleAttributes,
"features" => return Self::Features,
"attributes" => return Self::Attributes,
"tiles" => return Self::Tiles,
_ => {}
}
if let Some(rest) = value.strip_prefix("x-")
&& let Some((author, name)) = rest.split_once('_')
&& !author.is_empty()
&& !name.is_empty()
{
return Self::Extended {
author: author.to_owned(),
name: name.to_owned(),
};
}
Self::Other(value.to_owned())
}
pub fn as_string(&self) -> String {
match self {
Self::Media => "media".to_owned(),
Self::SimpleAttributes => "simple_attributes".to_owned(),
Self::Features => "features".to_owned(),
Self::Attributes => "attributes".to_owned(),
Self::Tiles => "tiles".to_owned(),
Self::Extended { author, name } => format!("x-{author}_{name}"),
Self::Other(value) => value.clone(),
}
}
pub fn is_conformant(&self) -> bool {
!matches!(self, Self::Other(_))
}
}
impl fmt::Display for RelationName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_string())
}
}
pub fn create_mapping_table_sql(table_name: &str) -> Result<String, crate::Error> {
Ok(format!(
"CREATE TABLE {} (\n base_id INTEGER NOT NULL,\n related_id INTEGER NOT NULL\n)",
ident::quote(table_name)?
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_five_requirements_classes_round_trip() {
for name in [
RelationName::Media,
RelationName::SimpleAttributes,
RelationName::Features,
RelationName::Attributes,
RelationName::Tiles,
] {
assert_eq!(RelationName::parse(&name.as_string()), name);
assert!(name.is_conformant());
}
}
#[test]
fn the_extended_form_round_trips_and_splits_author_from_name() {
let parsed = RelationName::parse("x-acme_inspections");
assert_eq!(
parsed,
RelationName::Extended {
author: "acme".to_owned(),
name: "inspections".to_owned(),
}
);
assert_eq!(parsed.as_string(), "x-acme_inspections");
assert!(parsed.is_conformant());
assert_eq!(
RelationName::parse("x-acme_site_visits").as_string(),
"x-acme_site_visits"
);
}
#[test]
fn a_value_requirement_8_rejects_is_kept_rather_than_lost() {
for value in ["photos", "x-", "x-acme", "x-_name", "x-acme_"] {
let parsed = RelationName::parse(value);
assert_eq!(parsed, RelationName::Other(value.to_owned()), "{value}");
assert_eq!(parsed.as_string(), value);
assert!(!parsed.is_conformant(), "{value}");
}
}
#[test]
fn mapping_table_sql_quotes_its_name_and_writes_both_columns() {
let sql = create_mapping_table_sql("odd\"name").unwrap();
assert!(sql.starts_with("CREATE TABLE \"odd\"\"name\" ("), "{sql}");
assert!(sql.contains("base_id INTEGER NOT NULL"));
assert!(sql.contains("related_id INTEGER NOT NULL"));
}
}