use std::sync::Arc;
use geopackage_core::datetime::{Date, DateTime};
use geopackage_core::geometry::{self, GpbGeometry};
use geopackage_core::gpb;
use geopackage_core::ident::quote;
use geopackage_core::triggers::{self, TriggerGeneration};
use rusqlite::OptionalExtension;
use rusqlite::types::ValueRef as SqlValueRef;
use crate::value::{ValueRef, value_ref_from_sql, value_ref_to_sql};
use crate::{
Column, ConversionOptions, Error, GeoPackage, GeometryColumn, Result, TableSchema,
resolve_table_name, table_exists,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
pub min_x: f64,
pub min_y: f64,
pub max_x: f64,
pub max_y: f64,
}
impl BoundingBox {
pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
Self {
min_x,
min_y,
max_x,
max_y,
}
}
fn intersects_envelope(&self, env: [f64; 4]) -> bool {
env[0] <= self.max_x && env[1] >= self.min_x && env[2] <= self.max_y && env[3] >= self.min_y
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LayerKind {
Feature,
Attributes,
}
impl LayerKind {
pub fn as_str(self) -> &'static str {
self.data_type()
}
fn data_type(self) -> &'static str {
match self {
Self::Feature => "features",
Self::Attributes => "attributes",
}
}
}
impl std::fmt::Display for LayerKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub struct Layer<'a> {
gpkg: &'a GeoPackage,
table_name: String,
schema: TableSchema,
kind: LayerKind,
geometry_column: Option<GeometryColumn>,
pk_column: Option<String>,
value_columns: Vec<Column>,
value_column_names: Arc<[String]>,
options: ConversionOptions,
validate_geometry_type: bool,
projection: Option<Projection>,
}
#[derive(Debug, Clone)]
struct Projection {
value_columns: Vec<Column>,
value_column_names: Arc<[String]>,
geometry: bool,
}
impl std::fmt::Debug for Layer<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Layer")
.field("table_name", &self.table_name)
.field("kind", &self.kind)
.field("geometry_column", &self.geometry_column)
.field("primary_key", &self.pk_column)
.field("options", &self.options)
.finish_non_exhaustive()
}
}
impl GeoPackage {
pub fn layers(&self) -> Result<Vec<Layer<'_>>> {
if !table_exists(self.connection(), "gpkg_geometry_columns")? {
return Ok(Vec::new());
}
let names: Vec<String> = {
let mut stmt = self.connection().prepare(
"SELECT DISTINCT c.table_name FROM gpkg_contents c \
JOIN gpkg_geometry_columns g ON g.table_name = c.table_name COLLATE NOCASE \
WHERE c.data_type = 'features' ORDER BY c.table_name",
)?;
stmt.query_map([], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?
};
names.iter().map(|n| self.layer(n)).collect()
}
pub fn layer(&self, name: &str) -> Result<Layer<'_>> {
self.build_layer(name, LayerKind::Feature)
}
pub fn attributes(&self, name: &str) -> Result<Layer<'_>> {
self.build_layer(name, LayerKind::Attributes)
}
fn build_layer(&self, name: &str, kind: LayerKind) -> Result<Layer<'_>> {
let row = self
.connection()
.query_row(
"SELECT table_name, data_type FROM gpkg_contents \
WHERE table_name = ?1 COLLATE NOCASE",
[name],
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)
.optional()?;
let (declared_name, data_type) = row.ok_or_else(|| Error::NoSuchLayer {
table_name: name.to_owned(),
})?;
if data_type != kind.data_type() {
return Err(Error::WrongDataType {
table_name: declared_name,
expected: kind.data_type(),
found: data_type,
});
}
let table_name =
resolve_table_name(self.connection(), &declared_name)?.unwrap_or(declared_name);
let schema = self.table_schema(&table_name)?;
let geometry_column = match kind {
LayerKind::Feature => match &schema.geometry_column {
Some(g) => Some(g.clone()),
None => self.geometry_column_ci(&table_name)?,
},
LayerKind::Attributes => None,
};
let pk_column = schema.primary_key().map(|c| c.name.clone());
let geom_name = geometry_column.as_ref().map(|g| g.column_name.clone());
let value_columns: Vec<Column> = schema
.columns
.iter()
.filter(|c| geom_name.as_deref() != Some(c.name.as_str()))
.filter(|c| pk_column.as_deref() != Some(c.name.as_str()))
.cloned()
.collect();
let value_column_names: Arc<[String]> =
value_columns.iter().map(|c| c.name.clone()).collect();
Ok(Layer {
gpkg: self,
table_name,
schema,
kind,
geometry_column,
pk_column,
value_columns,
value_column_names,
options: ConversionOptions::default(),
validate_geometry_type: false,
projection: None,
})
}
}
impl<'a> Layer<'a> {
pub fn table_name(&self) -> &str {
&self.table_name
}
pub fn kind(&self) -> LayerKind {
self.kind
}
pub fn schema(&self) -> &TableSchema {
&self.schema
}
pub fn geometry_column(&self) -> Option<&GeometryColumn> {
self.geometry_column.as_ref()
}
pub fn primary_key_column(&self) -> Option<&str> {
self.pk_column.as_deref()
}
pub(crate) fn gpkg(&self) -> &'a GeoPackage {
self.gpkg
}
pub(crate) fn value_columns(&self) -> &[Column] {
&self.value_columns
}
pub fn conversion_options(&self) -> ConversionOptions {
self.options
}
#[must_use]
pub fn with_conversion_options(mut self, options: ConversionOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn with_geometry_type_validation(mut self) -> Self {
self.validate_geometry_type = true;
self
}
pub fn with_columns(mut self, columns: &[&str]) -> Result<Self> {
let geometry_name = self.geometry_column.as_ref().map(|g| &g.column_name);
for name in columns {
let is_value = self.value_columns.iter().any(|c| c.name == *name);
let is_geometry = geometry_name.is_some_and(|g| g == *name);
if !is_value && !is_geometry {
return Err(Error::NoSuchColumn {
table_name: self.table_name.clone(),
column_name: (*name).to_owned(),
});
}
}
let value_columns: Vec<Column> = self
.value_columns
.iter()
.filter(|c| columns.contains(&c.name.as_str()))
.cloned()
.collect();
let geometry = geometry_name.is_some_and(|g| columns.contains(&g.as_str()));
self.projection = Some(Projection {
value_column_names: value_columns.iter().map(|c| c.name.clone()).collect(),
value_columns,
geometry,
});
Ok(self)
}
#[must_use]
pub fn without_geometry(mut self) -> Self {
let value_columns = self
.projection
.as_ref()
.map_or_else(|| self.value_columns.clone(), |p| p.value_columns.clone());
self.projection = Some(Projection {
value_column_names: value_columns.iter().map(|c| c.name.clone()).collect(),
value_columns,
geometry: false,
});
self
}
fn read_value_columns(&self) -> &[Column] {
self.projection
.as_ref()
.map_or(&self.value_columns, |p| &p.value_columns)
}
fn read_value_column_names(&self) -> &Arc<[String]> {
self.projection
.as_ref()
.map_or(&self.value_column_names, |p| &p.value_column_names)
}
fn reads_geometry(&self) -> bool {
match &self.projection {
Some(projection) => projection.geometry,
None => self.geometry_column.is_some(),
}
}
pub fn count(&self) -> Result<u64> {
let sql = format!("SELECT COUNT(*) FROM {}", quote(&self.table_name)?);
let count: i64 = self
.gpkg
.connection()
.query_row(&sql, [], |row| row.get(0))?;
Ok(count.unsigned_abs())
}
pub fn features(&self) -> Result<Features> {
let (sql, geom_idx) = self.base_select(self.reads_geometry())?;
self.execute(&sql, Vec::new(), geom_idx, None)
}
pub fn features_in(&self, bbox: BoundingBox) -> Result<Features> {
let (sql, geom_idx, uses_rtree) = self.features_in_plan()?;
let params = if uses_rtree {
use rusqlite::types::Value as Sql;
vec![
Sql::Real(widen_up(bbox.max_x)),
Sql::Real(widen_down(bbox.min_x)),
Sql::Real(widen_up(bbox.max_y)),
Sql::Real(widen_down(bbox.min_y)),
]
} else {
Vec::new()
};
self.execute(&sql, params, geom_idx, Some(bbox))
}
pub fn features_in_sql(&self) -> Result<String> {
Ok(self.features_in_plan()?.0)
}
pub fn select(&self, where_clause: &str, params: &[ValueRef<'_>]) -> Result<Features> {
let (base, geom_idx) = self.base_select(self.reads_geometry())?;
let sql = format!("{base} WHERE ({where_clause})");
let sql_params: Vec<rusqlite::types::Value> = params.iter().map(value_ref_to_sql).collect();
self.execute(&sql, sql_params, geom_idx, None)
}
pub fn cursor(&self) -> Result<FeatureCursor<'_>> {
let (sql, geom_idx) = self.base_select(self.reads_geometry())?;
self.prepare_cursor(&sql, Vec::new(), geom_idx, None)
}
pub fn cursor_in(&self, bbox: BoundingBox) -> Result<FeatureCursor<'_>> {
let (sql, geom_idx, uses_rtree) = self.features_in_plan()?;
let params = if uses_rtree {
use rusqlite::types::Value as Sql;
vec![
Sql::Real(widen_up(bbox.max_x)),
Sql::Real(widen_down(bbox.min_x)),
Sql::Real(widen_up(bbox.max_y)),
Sql::Real(widen_down(bbox.min_y)),
]
} else {
Vec::new()
};
self.prepare_cursor(&sql, params, geom_idx, Some(bbox))
}
pub fn cursor_select(
&self,
where_clause: &str,
params: &[ValueRef<'_>],
) -> Result<FeatureCursor<'_>> {
let (base, geom_idx) = self.base_select(self.reads_geometry())?;
let sql = format!("{base} WHERE ({where_clause})");
let sql_params: Vec<rusqlite::types::Value> = params.iter().map(value_ref_to_sql).collect();
self.prepare_cursor(&sql, sql_params, geom_idx, None)
}
fn prepare_cursor(
&self,
sql: &str,
params: Vec<rusqlite::types::Value>,
geom_idx: Option<usize>,
filter: Option<BoundingBox>,
) -> Result<FeatureCursor<'_>> {
Ok(FeatureCursor {
stmt: self.gpkg.connection().prepare(sql)?,
params,
geom_idx,
filter,
ctx: self.row_context(),
})
}
pub fn has_spatial_index(&self) -> Result<bool> {
let Some(geom) = &self.geometry_column else {
return Ok(false);
};
if self.pk_column.is_none() {
return Ok(false);
}
let conn = self.gpkg.connection();
let rtree = triggers::rtree_table_name(&self.table_name, &geom.column_name);
if !table_exists(conn, &rtree)? {
return Ok(false);
}
Ok(self.classify_rtree_triggers(&geom.column_name)? != TriggerGeneration::None)
}
pub(crate) fn classify_rtree_triggers(&self, column: &str) -> Result<TriggerGeneration> {
let conn = self.gpkg.connection();
let names: Vec<String> = {
let mut stmt = conn.prepare(
"SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?1",
)?;
stmt.query_map([self.table_name.as_str()], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?
};
Ok(triggers::classify_triggers(
names.iter().map(String::as_str),
&self.table_name,
column,
))
}
fn features_in_plan(&self) -> Result<(String, Option<usize>, bool)> {
let Some(geom) = &self.geometry_column else {
return Err(Error::NoGeometryColumn {
table_name: self.table_name.clone(),
});
};
if self.has_spatial_index()? {
let (sql, geom_idx) = self.rtree_select(geom)?;
Ok((sql, geom_idx, true))
} else {
let (sql, geom_idx) = self.base_select(true)?;
Ok((sql, geom_idx, false))
}
}
fn fid_expr(&self, prefix: Option<&str>) -> Result<String> {
match &self.pk_column {
Some(pk) => qualified(pk, prefix),
None => Ok(match prefix {
Some(p) => format!("{p}.rowid"),
None => "rowid".to_owned(),
}),
}
}
fn column_exprs(
&self,
prefix: Option<&str>,
select_geometry: bool,
) -> Result<(String, Option<usize>)> {
let mut exprs = Vec::with_capacity(self.read_value_columns().len() + 2);
exprs.push(self.fid_expr(prefix)?);
for column in self.read_value_columns() {
exprs.push(qualified(&column.name, prefix)?);
}
let geom_idx = match &self.geometry_column {
Some(geom) if select_geometry => {
exprs.push(qualified(&geom.column_name, prefix)?);
Some(exprs.len() - 1)
}
_ => None,
};
Ok((exprs.join(", "), geom_idx))
}
fn base_select(&self, select_geometry: bool) -> Result<(String, Option<usize>)> {
let (list, geom_idx) = self.column_exprs(None, select_geometry)?;
Ok((
format!("SELECT {list} FROM {}", quote(&self.table_name)?),
geom_idx,
))
}
fn rtree_select(&self, geom: &GeometryColumn) -> Result<(String, Option<usize>)> {
let table = quote(&self.table_name)?;
let (list, geom_idx) = self.column_exprs(Some(&table), true)?;
let rtree = quote(&triggers::rtree_table_name(
&self.table_name,
&geom.column_name,
))?;
let id = self.fid_expr(Some(&table))?;
let sql = format!(
"SELECT {list} FROM {table} \
JOIN {rtree} AS \"__gpkg_rtree\" ON {id} = \"__gpkg_rtree\".id \
WHERE \"__gpkg_rtree\".minx <= ?1 AND \"__gpkg_rtree\".maxx >= ?2 \
AND \"__gpkg_rtree\".miny <= ?3 AND \"__gpkg_rtree\".maxy >= ?4"
);
Ok((sql, geom_idx))
}
fn execute(
&self,
sql: &str,
params: Vec<rusqlite::types::Value>,
geom_idx: Option<usize>,
filter: Option<BoundingBox>,
) -> Result<Features> {
let conn = self.gpkg.connection();
let mut stmt = conn.prepare(sql)?;
let mut rows = stmt.query(rusqlite::params_from_iter(params.iter()))?;
let mut out: Vec<Result<Feature>> = Vec::new();
let ctx = self.row_context();
while let Some(row) = rows.next()? {
if let Some(bbox) = &filter {
match row_in_box(row, geom_idx, bbox) {
Ok(true) => {}
Ok(false) => continue,
Err(e) => {
out.push(Err(e));
continue;
}
}
}
out.push(ctx.feature_from_row(row, geom_idx));
}
Ok(Features {
inner: out.into_iter(),
})
}
fn row_context(&self) -> RowContext {
RowContext {
table_name: self.table_name.clone(),
value_columns: self.read_value_columns().to_vec(),
value_column_names: Arc::clone(self.read_value_column_names()),
options: self.options,
validate_geometry_type: self.validate_geometry_type,
geometry_column: self.geometry_column.clone(),
store_geometry: self.reads_geometry(),
value_bytes_hint: std::cell::Cell::new(0),
}
}
}
#[derive(Debug)]
pub struct FeatureCursor<'a> {
stmt: rusqlite::Statement<'a>,
params: Vec<rusqlite::types::Value>,
geom_idx: Option<usize>,
filter: Option<BoundingBox>,
ctx: RowContext,
}
impl FeatureCursor<'_> {
pub fn features(&mut self) -> Result<FeatureStream<'_>> {
let rows = self
.stmt
.query(rusqlite::params_from_iter(self.params.iter()))?;
Ok(FeatureStream {
rows,
ctx: &self.ctx,
geom_idx: self.geom_idx,
filter: self.filter,
})
}
}
pub struct FeatureStream<'c> {
rows: rusqlite::Rows<'c>,
ctx: &'c RowContext,
geom_idx: Option<usize>,
filter: Option<BoundingBox>,
}
impl std::fmt::Debug for FeatureStream<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FeatureStream")
.field("table_name", &self.ctx.table_name)
.field("filtered", &self.filter.is_some())
.finish_non_exhaustive()
}
}
impl Iterator for FeatureStream<'_> {
type Item = Result<Feature>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let row = match self.rows.next() {
Ok(Some(row)) => row,
Ok(None) => return None,
Err(e) => return Some(Err(e.into())),
};
if let Some(bbox) = &self.filter {
match row_in_box(row, self.geom_idx, bbox) {
Ok(true) => {}
Ok(false) => continue,
Err(e) => return Some(Err(e)),
}
}
return Some(self.ctx.feature_from_row(row, self.geom_idx));
}
}
}
#[derive(Debug, Clone)]
struct RowContext {
table_name: String,
value_columns: Vec<Column>,
value_column_names: Arc<[String]>,
options: ConversionOptions,
validate_geometry_type: bool,
geometry_column: Option<GeometryColumn>,
store_geometry: bool,
value_bytes_hint: std::cell::Cell<usize>,
}
impl RowContext {
fn feature_from_row(
&self,
row: &rusqlite::Row<'_>,
geom_idx: Option<usize>,
) -> Result<Feature> {
let fid: i64 = row.get(0)?;
let geometry = match geom_idx {
Some(gi) => match row.get_ref(gi)? {
SqlValueRef::Blob(bytes) => Some(bytes),
_ => None,
},
None => None,
};
if self.validate_geometry_type
&& let (Some(blob), Some(declared)) = (geometry, &self.geometry_column)
{
self.check_declared_type(blob, declared)?;
}
let mut buf: Vec<u8> =
Vec::with_capacity(geometry.map_or(0, <[u8]>::len) + self.value_bytes_hint.get());
let mut slots = Vec::with_capacity(self.value_columns.len());
let geometry_end = geometry.filter(|_| self.store_geometry).map(|blob| {
buf.extend_from_slice(blob);
u32::try_from(buf.len()).unwrap_or(u32::MAX)
});
for (i, column) in self.value_columns.iter().enumerate() {
let value = value_ref_from_sql(
row.get_ref(i + 1)?,
column.column_type.as_ref(),
&column.name,
self.options,
)?;
slots.push(match value {
ValueRef::Null => Slot::Null,
ValueRef::Boolean(b) => Slot::Boolean(b),
ValueRef::Integer(i) => Slot::Integer(i),
ValueRef::Float(f) => Slot::Float(f),
ValueRef::Text(s) => {
let (start, end) = push_bytes(&mut buf, s.as_bytes());
Slot::Text { start, end }
}
ValueRef::Blob(b) => {
let (start, end) = push_bytes(&mut buf, b);
Slot::Blob { start, end }
}
ValueRef::Date(d) => Slot::Date(d),
ValueRef::DateTime(dt) => Slot::DateTime(dt),
});
}
self.value_bytes_hint
.set(buf.len().saturating_sub(geometry_end.unwrap_or(0) as usize));
Ok(Feature {
fid,
buf,
geometry_end,
geometry_projected: self.geometry_column.is_none() || self.store_geometry,
slots: slots.into_boxed_slice(),
columns: Arc::clone(&self.value_column_names),
})
}
fn check_declared_type(&self, blob: &[u8], declared: &GeometryColumn) -> Result<()> {
let (_, offset) = gpb::parse_header(blob).map_err(|e| Error::Core(e.into()))?;
let body = blob.get(offset..).unwrap_or_default();
let found = geometry::wkb_geometry_type(body).map_err(|e| Error::Core(e.into()))?;
if !geometry::geometry_type_matches(found, declared.geometry_type) {
return Err(Error::GeometryTypeMismatch {
table_name: self.table_name.clone(),
column_name: declared.column_name.clone(),
declared: declared.geometry_type,
found,
});
}
Ok(())
}
}
pub(crate) fn row_in_box(
row: &rusqlite::Row<'_>,
geom_idx: Option<usize>,
bbox: &BoundingBox,
) -> Result<bool> {
let Some(gi) = geom_idx else {
return Ok(false);
};
let SqlValueRef::Blob(blob) = row.get_ref(gi)? else {
return Ok(false);
};
match blob_xy_envelope(blob)? {
Some(env) => Ok(bbox.intersects_envelope(env)),
None => Ok(false),
}
}
fn blob_xy_envelope(blob: &[u8]) -> Result<Option<[f64; 4]>> {
geometry::blob_xy_envelope(blob).map_err(|e| Error::Core(e.into()))
}
pub(crate) fn widen_up(v: f64) -> f64 {
f64::from((v as f32).next_up())
}
pub(crate) fn widen_down(v: f64) -> f64 {
f64::from((v as f32).next_down())
}
fn qualified(name: &str, prefix: Option<&str>) -> Result<String> {
let quoted = quote(name)?;
Ok(match prefix {
Some(p) => format!("{p}.{quoted}"),
None => quoted,
})
}
fn push_bytes(buf: &mut Vec<u8>, bytes: &[u8]) -> (u32, u32) {
let start = u32::try_from(buf.len()).unwrap_or(u32::MAX);
buf.extend_from_slice(bytes);
(start, u32::try_from(buf.len()).unwrap_or(u32::MAX))
}
#[derive(Debug, Clone, Copy)]
enum Slot {
Null,
Boolean(bool),
Integer(i64),
Float(f64),
Text {
start: u32,
end: u32,
},
Blob {
start: u32,
end: u32,
},
Date(Date),
DateTime(DateTime),
}
#[derive(Clone)]
pub struct Feature {
fid: i64,
buf: Vec<u8>,
geometry_end: Option<u32>,
geometry_projected: bool,
slots: Box<[Slot]>,
columns: Arc<[String]>,
}
impl std::fmt::Debug for Feature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Feature")
.field("fid", &self.fid)
.field("geometry_bytes", &self.geometry_bytes().map(<[u8]>::len))
.field("values", &self.iter().collect::<Vec<_>>())
.finish()
}
}
impl Feature {
pub fn fid(&self) -> i64 {
self.fid
}
pub fn geometry_bytes(&self) -> Option<&[u8]> {
let end = self.geometry_end?;
self.buf.get(..end as usize)
}
pub fn geometry(&self) -> Result<Option<GpbGeometry<'_>>> {
if !self.geometry_projected {
return Err(Error::GeometryNotProjected);
}
match self.geometry_bytes() {
None => Ok(None),
Some(blob) => Ok(Some(
GpbGeometry::parse(blob).map_err(|e| Error::Core(e.into()))?,
)),
}
}
fn slot_value(&self, slot: Slot) -> ValueRef<'_> {
let bytes = |start: u32, end: u32| self.buf.get(start as usize..end as usize);
match slot {
Slot::Null => ValueRef::Null,
Slot::Boolean(b) => ValueRef::Boolean(b),
Slot::Integer(i) => ValueRef::Integer(i),
Slot::Float(f) => ValueRef::Float(f),
Slot::Text { start, end } => ValueRef::Text(
bytes(start, end)
.and_then(|b| std::str::from_utf8(b).ok())
.unwrap_or_default(),
),
Slot::Blob { start, end } => ValueRef::Blob(bytes(start, end).unwrap_or_default()),
Slot::Date(d) => ValueRef::Date(d),
Slot::DateTime(dt) => ValueRef::DateTime(dt),
}
}
pub fn value(&self, name: &str) -> Option<ValueRef<'_>> {
let index = self.columns.iter().position(|c| c == name)?;
self.get(index)
}
pub fn get(&self, index: usize) -> Option<ValueRef<'_>> {
self.slots.get(index).map(|slot| self.slot_value(*slot))
}
pub fn values(&self) -> impl ExactSizeIterator<Item = ValueRef<'_>> {
self.slots.iter().map(|slot| self.slot_value(*slot))
}
pub fn columns(&self) -> &[String] {
&self.columns
}
#[must_use]
pub fn has_column(&self, name: &str) -> bool {
self.columns.iter().any(|c| c == name)
}
#[must_use]
pub fn has_geometry_column(&self) -> bool {
self.geometry_projected
}
pub fn len(&self) -> usize {
self.slots.len()
}
pub fn is_empty(&self) -> bool {
self.slots.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, ValueRef<'_>)> {
self.columns.iter().map(String::as_str).zip(self.values())
}
}
#[derive(Debug)]
pub struct Features {
inner: std::vec::IntoIter<Result<Feature>>,
}
impl Iterator for Features {
type Item = Result<Feature>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl ExactSizeIterator for Features {}