use crate::{Error, GeoPackage, Result};
use geopackage_core::datetime::{Date, DateTime};
use geopackage_core::ident;
use geopackage_core::types::ColumnType;
use rusqlite::types::ValueRef as SqlValueRef;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Value {
Null,
Boolean(bool),
Integer(i64),
Float(f64),
Text(String),
Blob(Vec<u8>),
Date(Date),
DateTime(DateTime),
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ValueRef<'a> {
Null,
Boolean(bool),
Integer(i64),
Float(f64),
Text(&'a str),
Blob(&'a [u8]),
Date(Date),
DateTime(DateTime),
}
impl<'a> ValueRef<'a> {
#[must_use]
pub fn to_value(&self) -> Value {
Value::from(*self)
}
#[must_use]
pub fn as_str(&self) -> Option<&'a str> {
match *self {
ValueRef::Text(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_blob(&self) -> Option<&'a [u8]> {
match *self {
ValueRef::Blob(b) => Some(b),
_ => None,
}
}
#[must_use]
pub fn is_null(&self) -> bool {
matches!(*self, ValueRef::Null)
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match *self {
ValueRef::Boolean(b) => Some(b),
_ => None,
}
}
#[must_use]
pub fn as_i64(&self) -> Option<i64> {
match *self {
ValueRef::Integer(i) => Some(i),
_ => None,
}
}
#[must_use]
pub fn as_f64(&self) -> Option<f64> {
match *self {
ValueRef::Float(f) => Some(f),
_ => None,
}
}
#[must_use]
pub fn as_date(&self) -> Option<Date> {
match *self {
ValueRef::Date(d) => Some(d),
_ => None,
}
}
#[must_use]
pub fn as_datetime(&self) -> Option<DateTime> {
match *self {
ValueRef::DateTime(dt) => Some(dt),
_ => None,
}
}
}
impl From<ValueRef<'_>> for Value {
fn from(value: ValueRef<'_>) -> Self {
match value {
ValueRef::Null => Value::Null,
ValueRef::Boolean(b) => Value::Boolean(b),
ValueRef::Integer(i) => Value::Integer(i),
ValueRef::Float(f) => Value::Float(f),
ValueRef::Text(s) => Value::Text(s.to_owned()),
ValueRef::Blob(b) => Value::Blob(b.to_vec()),
ValueRef::Date(d) => Value::Date(d),
ValueRef::DateTime(dt) => Value::DateTime(dt),
}
}
}
impl<'a> From<&'a Value> for ValueRef<'a> {
fn from(value: &'a Value) -> Self {
match value {
Value::Null => ValueRef::Null,
Value::Boolean(b) => ValueRef::Boolean(*b),
Value::Integer(i) => ValueRef::Integer(*i),
Value::Float(f) => ValueRef::Float(*f),
Value::Text(s) => ValueRef::Text(s),
Value::Blob(b) => ValueRef::Blob(b),
Value::Date(d) => ValueRef::Date(*d),
Value::DateTime(dt) => ValueRef::DateTime(*dt),
}
}
}
impl PartialEq<Value> for ValueRef<'_> {
fn eq(&self, other: &Value) -> bool {
*self == ValueRef::from(other)
}
}
impl PartialEq<ValueRef<'_>> for Value {
fn eq(&self, other: &ValueRef<'_>) -> bool {
ValueRef::from(self) == *other
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DateTimeParsing {
#[default]
Strict,
Lenient,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum StorageStrictness {
#[default]
Lenient,
Strict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct ConversionOptions {
pub datetime: DateTimeParsing,
pub storage: StorageStrictness,
}
impl ConversionOptions {
pub fn strict() -> Self {
Self {
datetime: DateTimeParsing::Strict,
storage: StorageStrictness::Strict,
}
}
pub fn lenient() -> Self {
Self {
datetime: DateTimeParsing::Lenient,
storage: StorageStrictness::Lenient,
}
}
#[must_use]
pub fn with_datetime(mut self, datetime: DateTimeParsing) -> Self {
self.datetime = datetime;
self
}
#[must_use]
pub fn with_storage(mut self, storage: StorageStrictness) -> Self {
self.storage = storage;
self
}
}
impl GeoPackage {
pub fn column_values(
&self,
table_name: &str,
column_name: &str,
options: ConversionOptions,
) -> Result<Vec<Value>> {
let schema = self.table_schema(table_name)?;
let column = schema
.column(column_name)
.ok_or_else(|| Error::NoSuchColumn {
table_name: table_name.to_owned(),
column_name: column_name.to_owned(),
})?;
if let Some(ColumnType::Geometry(_)) = column.column_type {
return Err(Error::GeometryValueUnsupported {
column: column_name.to_owned(),
});
}
let column_type = column.column_type.clone();
let sql = format!(
"SELECT {} FROM {}",
ident::quote(column_name)?,
ident::quote(table_name)?
);
let mut stmt = self.connection().prepare(&sql)?;
let mut rows = stmt.query([])?;
let mut out = Vec::new();
while let Some(row) = rows.next()? {
out.push(value_from_ref(
row.get_ref(0)?,
column_type.as_ref(),
column_name,
options,
)?);
}
Ok(out)
}
}
pub(crate) fn value_ref_to_sql(value: &ValueRef<'_>) -> rusqlite::types::Value {
value_into_sql(value.to_value())
}
pub(crate) fn value_ref_to_bind(value: ValueRef<'_>) -> rusqlite::types::ToSqlOutput<'_> {
use rusqlite::types::{ToSqlOutput, Value as Sql};
match value {
ValueRef::Null => ToSqlOutput::Borrowed(SqlValueRef::Null),
ValueRef::Boolean(b) => ToSqlOutput::Borrowed(SqlValueRef::Integer(i64::from(b))),
ValueRef::Integer(i) => ToSqlOutput::Borrowed(SqlValueRef::Integer(i)),
ValueRef::Float(f) => ToSqlOutput::Borrowed(SqlValueRef::Real(f)),
ValueRef::Text(s) => ToSqlOutput::Borrowed(SqlValueRef::Text(s.as_bytes())),
ValueRef::Blob(b) => ToSqlOutput::Borrowed(SqlValueRef::Blob(b)),
ValueRef::Date(d) => ToSqlOutput::Owned(Sql::Text(d.to_string())),
ValueRef::DateTime(dt) => ToSqlOutput::Owned(Sql::Text(dt.to_string())),
}
}
pub(crate) fn value_to_bind(value: &Value) -> rusqlite::types::ToSqlOutput<'_> {
use rusqlite::types::{ToSqlOutput, Value as Sql};
match value {
Value::Null => ToSqlOutput::Borrowed(SqlValueRef::Null),
Value::Boolean(b) => ToSqlOutput::Borrowed(SqlValueRef::Integer(i64::from(*b))),
Value::Integer(i) => ToSqlOutput::Borrowed(SqlValueRef::Integer(*i)),
Value::Float(f) => ToSqlOutput::Borrowed(SqlValueRef::Real(*f)),
Value::Text(s) => ToSqlOutput::Borrowed(SqlValueRef::Text(s.as_bytes())),
Value::Blob(b) => ToSqlOutput::Borrowed(SqlValueRef::Blob(b)),
Value::Date(d) => ToSqlOutput::Owned(Sql::Text(d.to_string())),
Value::DateTime(dt) => ToSqlOutput::Owned(Sql::Text(dt.to_string())),
}
}
pub(crate) fn value_into_sql(value: Value) -> rusqlite::types::Value {
use rusqlite::types::Value as Sql;
match value {
Value::Null => Sql::Null,
Value::Boolean(b) => Sql::Integer(i64::from(b)),
Value::Integer(i) => Sql::Integer(i),
Value::Float(f) => Sql::Real(f),
Value::Text(s) => Sql::Text(s),
Value::Blob(b) => Sql::Blob(b),
Value::Date(d) => Sql::Text(d.to_string()),
Value::DateTime(dt) => Sql::Text(dt.to_string()),
}
}
pub(crate) fn value_ref_from_sql<'a>(
value: SqlValueRef<'a>,
column_type: Option<&ColumnType>,
column_name: &str,
options: ConversionOptions,
) -> Result<ValueRef<'a>> {
if let SqlValueRef::Null = value {
return Ok(ValueRef::Null);
}
let Some(declared) = column_type else {
return untyped(value);
};
match declared {
ColumnType::Boolean => match value {
SqlValueRef::Integer(0) => Ok(ValueRef::Boolean(false)),
SqlValueRef::Integer(1) => Ok(ValueRef::Boolean(true)),
SqlValueRef::Integer(value) => match options.storage {
StorageStrictness::Lenient => Ok(ValueRef::Boolean(true)),
StorageStrictness::Strict => Err(Error::NonBooleanInteger {
column: column_name.to_owned(),
value,
}),
},
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::TinyInt
| ColumnType::SmallInt
| ColumnType::MediumInt
| ColumnType::Integer => match value {
SqlValueRef::Integer(i) => Ok(ValueRef::Integer(i)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Float | ColumnType::Double => match value {
SqlValueRef::Real(f) => Ok(ValueRef::Float(f)),
SqlValueRef::Integer(i) => match options.storage {
StorageStrictness::Lenient => Ok(ValueRef::Float(i as f64)),
StorageStrictness::Strict => Err(mismatch(column_name, declared, value)),
},
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Text(_) => match value {
SqlValueRef::Text(bytes) => Ok(ValueRef::Text(text_ref(bytes)?)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Blob(_) => match value {
SqlValueRef::Blob(bytes) => Ok(ValueRef::Blob(bytes)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Date => match value {
SqlValueRef::Text(bytes) => {
let s = text_ref(bytes)?;
Date::parse(s)
.map(ValueRef::Date)
.map_err(|source| Error::InvalidDateTimeValue {
column: column_name.to_owned(),
text: s.to_owned(),
source,
})
}
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::DateTime => match value {
SqlValueRef::Text(bytes) => {
let s = text_ref(bytes)?;
let parsed = match options.datetime {
DateTimeParsing::Strict => DateTime::parse_strict(s),
DateTimeParsing::Lenient => DateTime::parse_lenient(s),
};
parsed
.map(ValueRef::DateTime)
.map_err(|source| Error::InvalidDateTimeValue {
column: column_name.to_owned(),
text: s.to_owned(),
source,
})
}
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Geometry(_) => Err(Error::GeometryValueUnsupported {
column: column_name.to_owned(),
}),
_ => untyped(value),
}
}
fn untyped<'a>(value: SqlValueRef<'a>) -> Result<ValueRef<'a>> {
Ok(match value {
SqlValueRef::Null => ValueRef::Null,
SqlValueRef::Integer(i) => ValueRef::Integer(i),
SqlValueRef::Real(f) => ValueRef::Float(f),
SqlValueRef::Text(bytes) => ValueRef::Text(text_ref(bytes)?),
SqlValueRef::Blob(bytes) => ValueRef::Blob(bytes),
})
}
pub(crate) fn value_from_ref(
value: SqlValueRef<'_>,
column_type: Option<&ColumnType>,
column_name: &str,
options: ConversionOptions,
) -> Result<Value> {
value_ref_from_sql(value, column_type, column_name, options).map(Value::from)
}
fn text_ref(bytes: &[u8]) -> Result<&str> {
Ok(std::str::from_utf8(bytes).map_err(rusqlite::Error::from)?)
}
fn mismatch(column: &str, declared: &ColumnType, found: SqlValueRef<'_>) -> Error {
Error::ValueTypeMismatch {
column: column.to_owned(),
declared: declared.clone(),
found: storage_class(found),
}
}
fn storage_class(value: SqlValueRef<'_>) -> &'static str {
match value {
SqlValueRef::Null => "NULL",
SqlValueRef::Integer(_) => "INTEGER",
SqlValueRef::Real(_) => "REAL",
SqlValueRef::Text(_) => "TEXT",
SqlValueRef::Blob(_) => "BLOB",
}
}