geopackage-core 0.11.2

No-IO core for the OGC GeoPackage format: GeoPackage Binary (GPB) codec, table DDL, RTree trigger definitions, and validation rules
Documentation
//! The `gpkg_schema` extension's model (Annex F.9): column descriptions and
//! the constraints on their values.
//!
//! The extension has two tables. `gpkg_data_columns` describes a column: a
//! human-readable name, a title, a description, a MIME type for a BLOB column,
//! and optionally the name of a constraint. `gpkg_data_column_constraints`
//! stores the constraints, keyed by that name, in three forms: a numeric
//! `range`, an `enum` of allowed values, or a `glob` pattern.
//!
//! This module is only the model of what a file says. Checking whether a value
//! satisfies a constraint belongs to the `geopackage` crate, because one of the
//! three forms cannot be checked here. A `glob` is a pattern in SQLite's `GLOB`
//! syntax, which is defined by whatever the engine that opens the file
//! implements. The check therefore asks that engine rather than keeping a copy
//! of its rules here. See
//! `geopackage::OpenOptions::enforce_column_constraints`.
//!
//! The spec states explicitly that these constraints are advisory as far as the
//! file format goes: "These restrictions MAY be enforced by SQL triggers or by
//! code in applications that update GeoPackage data values."

use std::fmt;

/// Registered extension name for column descriptions and value constraints
/// (Annex F.9).
pub const EXTENSION_NAME: &str = "gpkg_schema";
/// `gpkg_extensions.definition` value for [`EXTENSION_NAME`].
pub const EXTENSION_DEFINITION: &str = "http://www.geopackage.org/spec140/#extension_schema";
/// `gpkg_extensions.scope` value for [`EXTENSION_NAME`].
pub const EXTENSION_SCOPE: &str = "read-write";

/// A row of `gpkg_data_columns`: what one column of one table means.
///
/// Every field but the column's own name is optional. A row with nothing but
/// a `name` is legal and common, because the extension supplements
/// `sqlite_master` and `PRAGMA table_info` rather than replacing them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataColumn {
    /// The column this describes.
    pub column_name: String,
    /// A human-readable identifier, such as a short name.
    pub name: Option<String>,
    /// A human-readable formal title.
    pub title: Option<String>,
    /// A human-readable description.
    pub description: Option<String>,
    /// The MIME type of the column's content, for a BLOB column.
    pub mime_type: Option<String>,
    /// The name of the constraint the column's values are subject to, which
    /// [`ColumnConstraint::name`] matches (Requirement 106).
    pub constraint_name: Option<String>,
}

/// A constraint on a column's values, assembled from the
/// `gpkg_data_column_constraints` rows sharing one `constraint_name`.
///
/// An `enum` occupies one row per member, and a `range` or `glob` exactly one
/// row (Requirement 109). This type therefore models a constraint, not a row.
#[derive(Debug, Clone, PartialEq)]
pub struct ColumnConstraint {
    /// The name that rows use to reference this constraint. It is lowercase,
    /// per the column description in the spec's table definition.
    pub name: String,
    /// What the constraint allows.
    pub kind: ConstraintKind,
    /// The constraint's description, or for an `enum`, the description of
    /// whichever member row had one.
    pub description: Option<String>,
}

/// The three constraint forms Requirement 108 allows.
#[derive(Debug, Clone, PartialEq)]
pub enum ConstraintKind {
    /// `range`: a numeric interval, each end inclusive or exclusive.
    ///
    /// Requirement 111 makes both ends NOT NULL with `min` less than `max`,
    /// and Requirement 112 makes both inclusivity flags 0 or 1.
    Range {
        /// The lower bound.
        min: f64,
        /// Whether a value equal to `min` satisfies the constraint.
        min_is_inclusive: bool,
        /// The upper bound.
        max: f64,
        /// Whether a value equal to `max` satisfies the constraint.
        max_is_inclusive: bool,
    },
    /// `enum`: the set of allowed values, one per row, compared as text
    /// (Requirement 114 makes each row's `value` NOT NULL).
    ///
    /// The members are in the order the rows came back in, and the order has
    /// no meaning. The spec calls this a set, and round-tripping a file through
    /// GDAL reorders the members. If the file has been through another
    /// implementation, compare two enums as sets rather than with
    /// [`PartialEq`].
    Enum(Vec<String>),
    /// `glob`: a pattern the value has to match, in SQLite's `GLOB` syntax.
    Glob(String),
}

impl ConstraintKind {
    /// Returns the `constraint_type` column value.
    pub fn type_name(&self) -> &'static str {
        match self {
            Self::Range { .. } => "range",
            Self::Enum(_) => "enum",
            Self::Glob(_) => "glob",
        }
    }
}

impl fmt::Display for ConstraintKind {
    /// Formats the constraint on one line, for an error message that names
    /// what was violated.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Range {
                min,
                min_is_inclusive,
                max,
                max_is_inclusive,
            } => write!(
                f,
                "range {}{min}, {max}{}",
                if *min_is_inclusive { '[' } else { '(' },
                if *max_is_inclusive { ']' } else { ')' }
            ),
            Self::Enum(members) => write!(f, "enum of {} value(s)", members.len()),
            Self::Glob(pattern) => write!(f, "glob {pattern:?}"),
        }
    }
}