use geopackage_core::schema::{ColumnConstraint, ConstraintKind};
use rusqlite::types::ToSqlOutput;
use rusqlite::{CachedStatement, Connection};
use crate::{Layer, Result, Value, ValueRef as CellRef};
use super::feature_writer::ValueColumn;
pub(crate) struct ColumnConstraints<'conn> {
pub(crate) per_column: Vec<Option<ColumnConstraint>>,
pub(crate) glob: Option<CachedStatement<'conn>>,
}
impl<'conn> ColumnConstraints<'conn> {
pub(crate) fn read(
layer: &Layer<'_>,
conn: &'conn Connection,
value_columns: &[ValueColumn],
) -> Result<Self> {
if !layer.gpkg().enforces_column_constraints() {
return Ok(Self::none());
}
let described = layer.gpkg().data_columns(layer.table_name())?;
let mut per_column = Vec::with_capacity(value_columns.len());
for column in value_columns {
let constraint_name = described
.iter()
.find(|described| described.column_name == column.name)
.and_then(|described| described.constraint_name.as_deref());
per_column.push(match constraint_name {
Some(name) => layer.gpkg().column_constraint(name)?,
None => None,
});
}
let needs_glob = per_column
.iter()
.flatten()
.any(|constraint| matches!(constraint.kind, ConstraintKind::Glob(_)));
let glob = match needs_glob {
true => Some(conn.prepare_cached("SELECT ?1 GLOB ?2")?),
false => None,
};
Ok(Self { per_column, glob })
}
fn none() -> Self {
Self {
per_column: Vec::new(),
glob: None,
}
}
pub(crate) fn is_empty(&self) -> bool {
self.per_column.iter().all(Option::is_none)
}
pub(crate) fn at(&self, index: usize) -> Option<&ColumnConstraint> {
self.per_column.get(index).and_then(Option::as_ref)
}
pub(crate) fn satisfied(&mut self, index: usize, value: Checkable<'_>) -> Result<bool> {
let Self { per_column, glob } = self;
let Some(Some(constraint)) = per_column.get(index) else {
return Ok(true);
};
match (&constraint.kind, value) {
(_, Checkable::Null | Checkable::Unchecked) => Ok(true),
(ConstraintKind::Range { .. }, Checkable::Text(_)) => Ok(false),
(ConstraintKind::Range { .. }, Checkable::Integer(number)) => {
Ok(in_range(&constraint.kind, number as f64))
}
(ConstraintKind::Range { .. }, Checkable::Real(number)) => {
Ok(in_range(&constraint.kind, number))
}
(ConstraintKind::Enum(members), value) => Ok(match value {
Checkable::Text(text) => members.iter().any(|member| member == text),
Checkable::Integer(number) => members.contains(&number.to_string()),
Checkable::Real(number) => members.contains(&number.to_string()),
Checkable::Null | Checkable::Unchecked => true,
}),
(ConstraintKind::Glob(pattern), value) => {
let statement = glob
.as_mut()
.expect("a glob constraint means the statement was prepared");
let matched: i64 = match value {
Checkable::Text(text) => {
statement.query_one(rusqlite::params![text, pattern], |r| r.get(0))?
}
Checkable::Integer(number) => {
statement.query_one(rusqlite::params![number, pattern], |r| r.get(0))?
}
Checkable::Real(number) => {
statement.query_one(rusqlite::params![number, pattern], |r| r.get(0))?
}
Checkable::Null | Checkable::Unchecked => 1,
};
Ok(matched != 0)
}
}
}
}
pub(crate) fn in_range(kind: &ConstraintKind, value: f64) -> bool {
let ConstraintKind::Range {
min,
min_is_inclusive,
max,
max_is_inclusive,
} = kind
else {
return false;
};
let above = if *min_is_inclusive {
value >= *min
} else {
value > *min
};
let below = if *max_is_inclusive {
value <= *max
} else {
value < *max
};
above && below
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Checkable<'a> {
Null,
Integer(i64),
Real(f64),
Text(&'a str),
Unchecked,
}
pub(crate) trait AsCheckable {
fn as_checkable(&self) -> Checkable<'_>;
}
impl AsCheckable for Value {
fn as_checkable(&self) -> Checkable<'_> {
match self {
Self::Null => Checkable::Null,
Self::Integer(value) => Checkable::Integer(*value),
Self::Boolean(value) => Checkable::Integer(i64::from(*value)),
Self::Float(value) => Checkable::Real(*value),
Self::Text(value) => Checkable::Text(value),
Self::Blob(_) | Self::Date(_) | Self::DateTime(_) => Checkable::Unchecked,
}
}
}
impl AsCheckable for CellRef<'_> {
fn as_checkable(&self) -> Checkable<'_> {
match self {
Self::Null => Checkable::Null,
Self::Integer(value) => Checkable::Integer(*value),
Self::Boolean(value) => Checkable::Integer(i64::from(*value)),
Self::Float(value) => Checkable::Real(*value),
Self::Text(value) => Checkable::Text(value),
Self::Blob(_) | Self::Date(_) | Self::DateTime(_) => Checkable::Unchecked,
}
}
}
impl AsCheckable for ToSqlOutput<'_> {
fn as_checkable(&self) -> Checkable<'_> {
let value = match self {
Self::Borrowed(value) => *value,
Self::Owned(value) => value.into(),
_ => return Checkable::Unchecked,
};
match value {
rusqlite::types::ValueRef::Null => Checkable::Null,
rusqlite::types::ValueRef::Integer(value) => Checkable::Integer(value),
rusqlite::types::ValueRef::Real(value) => Checkable::Real(value),
rusqlite::types::ValueRef::Text(bytes) => match std::str::from_utf8(bytes) {
Ok(text) => Checkable::Text(text),
Err(_) => Checkable::Unchecked,
},
rusqlite::types::ValueRef::Blob(_) => Checkable::Unchecked,
}
}
}