use crate::{Error, GeoPackage, Result};
use geopackage_core::datetime::{Date, DateTime};
use geopackage_core::ident;
use geopackage_core::types::ColumnType;
use rusqlite::types::ValueRef;
#[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, Eq, Default)]
#[non_exhaustive]
pub enum DateTimeParsing {
#[default]
Strict,
Lenient,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct ConversionOptions {
pub datetime: DateTimeParsing,
}
impl ConversionOptions {
pub fn strict() -> Self {
Self {
datetime: DateTimeParsing::Strict,
}
}
pub fn lenient() -> Self {
Self {
datetime: DateTimeParsing::Lenient,
}
}
}
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_to_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.clone()),
Value::Blob(b) => Sql::Blob(b.clone()),
Value::Date(d) => Sql::Text(d.to_string()),
Value::DateTime(dt) => Sql::Text(dt.to_string()),
}
}
pub(crate) fn value_from_ref(
value: ValueRef<'_>,
column_type: Option<&ColumnType>,
column_name: &str,
options: ConversionOptions,
) -> Result<Value> {
if let ValueRef::Null = value {
return Ok(Value::Null);
}
let Some(declared) = column_type else {
return untyped(value);
};
match declared {
ColumnType::Boolean => match value {
ValueRef::Integer(i) => Ok(Value::Boolean(i != 0)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::TinyInt
| ColumnType::SmallInt
| ColumnType::MediumInt
| ColumnType::Integer => match value {
ValueRef::Integer(i) => Ok(Value::Integer(i)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Float | ColumnType::Double => match value {
ValueRef::Real(f) => Ok(Value::Float(f)),
ValueRef::Integer(i) => Ok(Value::Float(i as f64)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Text(_) => match value {
ValueRef::Text(bytes) => Ok(Value::Text(text(bytes)?)),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Blob(_) => match value {
ValueRef::Blob(bytes) => Ok(Value::Blob(bytes.to_vec())),
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Date => match value {
ValueRef::Text(bytes) => {
let s = text(bytes)?;
Date::parse(&s)
.map(Value::Date)
.map_err(|source| Error::InvalidDateTimeValue {
column: column_name.to_owned(),
text: s,
source,
})
}
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::DateTime => match value {
ValueRef::Text(bytes) => {
let s = text(bytes)?;
let parsed = match options.datetime {
DateTimeParsing::Strict => DateTime::parse_strict(&s),
DateTimeParsing::Lenient => DateTime::parse_lenient(&s),
};
parsed
.map(Value::DateTime)
.map_err(|source| Error::InvalidDateTimeValue {
column: column_name.to_owned(),
text: s,
source,
})
}
other => Err(mismatch(column_name, declared, other)),
},
ColumnType::Geometry(_) => Err(Error::GeometryValueUnsupported {
column: column_name.to_owned(),
}),
_ => untyped(value),
}
}
fn untyped(value: ValueRef<'_>) -> Result<Value> {
Ok(match value {
ValueRef::Null => Value::Null,
ValueRef::Integer(i) => Value::Integer(i),
ValueRef::Real(f) => Value::Float(f),
ValueRef::Text(bytes) => Value::Text(text(bytes)?),
ValueRef::Blob(bytes) => Value::Blob(bytes.to_vec()),
})
}
fn text(bytes: &[u8]) -> Result<String> {
Ok(std::str::from_utf8(bytes)
.map_err(rusqlite::Error::from)?
.to_owned())
}
fn mismatch(column: &str, declared: &ColumnType, found: ValueRef<'_>) -> Error {
Error::ValueTypeMismatch {
column: column.to_owned(),
declared: declared.clone(),
found: storage_class(found),
}
}
fn storage_class(value: ValueRef<'_>) -> &'static str {
match value {
ValueRef::Null => "NULL",
ValueRef::Integer(_) => "INTEGER",
ValueRef::Real(_) => "REAL",
ValueRef::Text(_) => "TEXT",
ValueRef::Blob(_) => "BLOB",
}
}