use crate::conversions::{
column_type_from_str, column_type_to_str, dimension_from_zm, dimension_to_zm,
geometry_type_from_str, geometry_type_to_str,
};
use crate::error::{GpkgError, Result};
use crate::ogc_sql::{
SQL_INSERT_GPKG_CONTENTS, SQL_INSERT_GPKG_CONTENTS_ATTRIBUTES,
SQL_INSERT_GPKG_GEOMETRY_COLUMNS, SQL_LIST_ATTRIBUTE_TABLES, SQL_LIST_LAYERS,
SQL_SELECT_DATA_TYPE, SQL_SELECT_GEOMETRY_COLUMN_META, execute_rtree_sqls, gpkg_rtree_drop_sql,
initialize_gpkg, sql_create_table, sql_drop_table, sql_table_columns,
};
use crate::sql_functions::register_spatial_functions;
use crate::types::{ColumnSpec, GpkgLayerMetadata};
#[cfg(target_family = "wasm")]
use crate::vfs::HybridVfsBuilder;
use rusqlite::OpenFlags;
#[cfg(target_family = "wasm")]
use std::io::{Seek, Write};
use std::path::Path;
use std::rc::Rc;
use super::attribute_table::GpkgAttributeTable;
use super::layer::GpkgLayer;
#[derive(Debug)]
/// GeoPackage connection wrapper for reading (and later writing) layers.
pub struct Gpkg {
pub(crate) conn: Rc<rusqlite::Connection>,
pub(crate) read_only: bool,
}
#[cfg(not(target_family = "wasm"))]
fn rusqlite_open_path<P: AsRef<Path>>(
path: P,
flags: rusqlite::OpenFlags,
) -> rusqlite::Result<rusqlite::Connection> {
rusqlite::Connection::open_with_flags(path, flags)
}
// Use OPFS. cf. https://github.com/Spxg/sqlite-wasm-rs/issues/24
#[cfg(target_family = "wasm")]
fn rusqlite_open_path<P: AsRef<Path>>(
path: P,
flags: rusqlite::OpenFlags,
) -> rusqlite::Result<rusqlite::Connection> {
rusqlite::Connection::open_with_flags_and_vfs(path, flags, "opfs-sahpool")
}
#[cfg(target_family = "wasm")]
fn rusqlite_open_path_with_vfs<P: AsRef<Path>>(
path: P,
flags: rusqlite::OpenFlags,
vfs_name: &str,
) -> rusqlite::Result<rusqlite::Connection> {
rusqlite::Connection::open_with_flags_and_vfs(path, flags, vfs_name)
}
impl Gpkg {
pub(crate) fn new_from_conn(conn: Rc<rusqlite::Connection>, read_only: bool) -> Result<Self> {
register_spatial_functions(&conn)?;
Ok(Self { conn, read_only })
}
/// Open a GeoPackage in read-only mode.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn open_read_only<P: AsRef<Path>>(path: P) -> Result<Self> {
let conn = rusqlite_open_path(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
Self::new_from_conn(Rc::new(conn), true)
}
/// Open a new or existing GeoPackage in read-write mode.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open("data/example.gpkg")?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let is_existing = path.exists();
let conn = rusqlite_open_path(path, rusqlite::OpenFlags::default())?;
// In the case of new file, initialize it
if !is_existing {
initialize_gpkg(&conn)?;
}
Self::new_from_conn(Rc::new(conn), false)
}
/// Open a new or existing GeoPackage in read-write mode with an explicit VFS.
///
/// This is available only on wasm targets where custom SQLite VFS usage is
/// required (for example, a user-registered hybrid VFS).
#[cfg(target_family = "wasm")]
pub(crate) fn open_with_vfs<P: AsRef<Path>>(path: P, vfs_name: &str) -> Result<Self> {
let path = path.as_ref();
let is_existing = path.exists();
let conn = rusqlite_open_path_with_vfs(path, rusqlite::OpenFlags::default(), vfs_name)?;
if !is_existing {
initialize_gpkg(&conn)?;
}
Self::new_from_conn(Rc::new(conn), false)
}
/// Open a new or existing GeoPackage in read-write mode with a custom writer.
///
/// This is available only on wasm targets. It uses the Hybrid VFS internally
/// and reuses a default VFS registration across calls.
///
/// The writer must implement `Seek` because SQLite writes pages at arbitrary
/// offsets (page splits, journal replay, etc.) — a stream-only writer would
/// receive pages in write-order and produce a corrupt file.
#[cfg(target_family = "wasm")]
#[cfg_attr(docsrs, doc(cfg(target_family = "wasm")))]
pub fn open_with_writer<P: AsRef<Path>, W: Write + Seek + 'static>(
path: P,
writer: W,
) -> Result<Self> {
HybridVfsBuilder::new(writer).open_gpkg(path)
}
/// Create a new GeoPackage in memory.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_in_memory()?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn open_in_memory() -> Result<Self> {
let conn = rusqlite::Connection::open_in_memory()?;
initialize_gpkg(&conn)?;
Self::new_from_conn(Rc::new(conn), false)
}
/// Expert-only: register a spatial reference system in gpkg_spatial_ref_sys.
///
/// GeoPackage layers must reference a valid `srs_id` that already exists in
/// `gpkg_spatial_ref_sys`. The GeoPackage spec requires a full SRS definition
/// (notably the WKT `definition` and descriptive metadata). In practice, this
/// data is often sourced from an external authority like EPSG, but this crate
/// does not bundle or generate that catalog. As a result, callers must insert
/// SRS entries themselves before creating layers, which is why this low-level
/// helper exists.
///
/// This method performs a direct insert with all required columns and does
/// no validation of the WKT or authority fields. Use only if you understand
/// the GeoPackage SRS requirements and have authoritative metadata.
///
/// Example: register EPSG:3857 (Web Mercator / Pseudo-Mercator).
/// ```no_run
/// # use rusqlite_gpkg::Gpkg;
/// let gpkg = Gpkg::open_in_memory().expect("new gpkg");
/// let definition = r#"PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]"#;
/// gpkg.register_srs(
/// "WGS 84 / Pseudo-Mercator",
/// 3857,
/// "EPSG",
/// 3857,
/// definition,
/// "Web Mercator / Pseudo-Mercator (EPSG:3857)",
/// ).expect("register srs");
/// ```
pub fn register_srs(
&self,
srs_name: &str,
srs_id: i32,
organization: &str,
organization_coordsys_id: i32,
definition: &str,
description: &str,
) -> Result<()> {
if self.read_only {
return Err(GpkgError::ReadOnly);
}
self.conn.execute(
"INSERT INTO gpkg_spatial_ref_sys \
(srs_name, srs_id, organization, organization_coordsys_id, definition, description) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
srs_name,
srs_id,
organization,
organization_coordsys_id,
definition,
description
],
)?;
Ok(())
}
/// List the names of the feature layers (tables with `data_type = 'features'`).
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
/// let layers = gpkg.list_layers()?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn list_layers(&self) -> Result<Vec<String>> {
let mut stmt = self.conn.prepare(SQL_LIST_LAYERS)?;
let layers = stmt
.query_map([], |row| row.get(0))?
.collect::<std::result::Result<Vec<String>, _>>()?;
Ok(layers)
}
/// List the names of the attribute tables (tables with `data_type = 'attributes'`).
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
/// let tables = gpkg.list_attribute_tables()?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn list_attribute_tables(&self) -> Result<Vec<String>> {
let mut stmt = self.conn.prepare(SQL_LIST_ATTRIBUTE_TABLES)?;
let tables = stmt
.query_map([], |row| row.get(0))?
.collect::<std::result::Result<Vec<String>, _>>()?;
Ok(tables)
}
/// Load a layer definition and metadata by name.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
/// let layer = gpkg.get_layer("points")?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn get_layer(&self, layer_name: &str) -> Result<GpkgLayer> {
// Guard: if this table exists but is not a feature layer, give a clear error.
if let Ok(data_type) = self.get_data_type(layer_name)
&& data_type != "features"
{
if data_type == "attributes" {
return Err(GpkgError::NotAFeatureLayer {
layer_name: layer_name.to_string(),
});
}
return Err(GpkgError::UnsupportedDataType {
layer_name: layer_name.to_string(),
data_type,
});
}
let (geometry_column, geometry_type, geometry_dimension, srs_id) =
self.get_geometry_column_and_srs_id(layer_name)?;
let column_specs = self.get_column_specs(
layer_name,
&geometry_column,
geometry_type,
geometry_dimension,
srs_id,
)?;
let primary_key_column = column_specs.primary_key_column.clone();
let other_columns = column_specs.other_columns;
let insert_sql = GpkgLayer::build_insert_sql(layer_name, &geometry_column, &other_columns);
let update_sql = GpkgLayer::build_update_sql(
layer_name,
&geometry_column,
&primary_key_column,
&other_columns,
);
let property_index_by_name =
Rc::new(GpkgLayer::build_property_index_by_name(&other_columns));
Ok(GpkgLayer {
conn: self.conn.clone(),
is_read_only: self.read_only,
layer_name: layer_name.to_string(),
geometry_column,
primary_key_column,
geometry_type,
geometry_dimension,
srs_id,
property_columns: other_columns,
property_index_by_name,
insert_sql,
update_sql,
})
}
// Create a new layer.
///
/// Example:
/// ```no_run
/// use geo_types::Point;
/// use rusqlite_gpkg::{ColumnSpec, ColumnType, Gpkg, params};
///
/// let gpkg = Gpkg::open_in_memory()?;
/// let columns = vec![ColumnSpec {
/// name: "name".to_string(),
/// column_type: ColumnType::Varchar,
/// }];
/// let layer = gpkg.create_layer(
/// "points",
/// "geom",
/// wkb::reader::GeometryType::Point,
/// wkb::reader::Dimension::Xy,
/// 4326,
/// &columns,
/// )?;
/// layer.insert(Point::new(1.0, 2.0), params!["alpha"])?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn create_layer(
&self,
layer_name: &str,
geometry_column: &str,
geometry_type: wkb::reader::GeometryType,
geometry_dimension: wkb::reader::Dimension,
srs_id: u32,
other_column_specs: &[ColumnSpec],
) -> Result<GpkgLayer> {
if self.read_only {
return Err(GpkgError::ReadOnly);
}
if self.table_exists_in_contents(layer_name)? {
return Err(GpkgError::LayerAlreadyExists {
layer_name: layer_name.to_string(),
});
}
let srs_exists: i64 = self.conn.query_row(
"SELECT EXISTS(SELECT 1 FROM gpkg_spatial_ref_sys WHERE srs_id = ?1)",
rusqlite::params![srs_id],
|row| row.get(0),
)?;
if srs_exists == 0 {
return Err(GpkgError::MissingSpatialRefSysId { srs_id });
}
let geometry_type_name = geometry_type_to_str(geometry_type);
let (z, m) = dimension_to_zm(geometry_dimension);
let mut column_defs = Vec::with_capacity(other_column_specs.len() + 2);
column_defs.push("fid INTEGER PRIMARY KEY AUTOINCREMENT".to_string());
column_defs.push(format!(r#""{}" {geometry_type_name}"#, geometry_column));
for spec in other_column_specs {
let col_type = column_type_to_str(spec.column_type);
column_defs.push(format!(r#""{}" {col_type}"#, spec.name));
}
let create_sql = sql_create_table(layer_name, &column_defs.join(", "));
self.conn.execute_batch(&create_sql)?;
self.conn.execute(
SQL_INSERT_GPKG_CONTENTS,
rusqlite::params![layer_name, layer_name, srs_id],
)?;
self.conn.execute(
SQL_INSERT_GPKG_GEOMETRY_COLUMNS,
rusqlite::params![
layer_name,
geometry_column,
geometry_type_name,
srs_id,
z,
m
],
)?;
execute_rtree_sqls(&self.conn, layer_name, geometry_column, "fid")?;
let insert_sql =
GpkgLayer::build_insert_sql(layer_name, geometry_column, other_column_specs);
let update_sql =
GpkgLayer::build_update_sql(layer_name, geometry_column, "fid", other_column_specs);
let property_index_by_name =
Rc::new(GpkgLayer::build_property_index_by_name(other_column_specs));
Ok(GpkgLayer {
conn: self.conn.clone(),
is_read_only: self.read_only,
layer_name: layer_name.to_string(),
geometry_column: geometry_column.to_string(),
primary_key_column: "fid".to_string(),
geometry_type,
geometry_dimension,
srs_id,
property_columns: other_column_specs.to_vec(),
property_index_by_name,
insert_sql,
update_sql,
})
}
/// Delete a layer.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open("data/example.gpkg")?;
/// gpkg.delete_layer("points")?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn delete_layer(&self, layer_name: &str) -> Result<()> {
if self.read_only {
return Err(GpkgError::ReadOnly);
}
// Guard: don't try to delete non-feature tables via delete_layer.
if let Ok(data_type) = self.get_data_type(layer_name)
&& data_type != "features"
{
if data_type == "attributes" {
return Err(GpkgError::NotAFeatureLayer {
layer_name: layer_name.to_string(),
});
}
return Err(GpkgError::UnsupportedDataType {
layer_name: layer_name.to_string(),
data_type,
});
}
let (geometry_column, _, _, _) = self.get_geometry_column_and_srs_id(layer_name)?;
self.conn
.execute_batch(&gpkg_rtree_drop_sql(layer_name, &geometry_column))?;
self.conn.execute_batch(&sql_drop_table(layer_name))?;
Ok(())
}
/// Dump the GeoPackage data to `Vec<u8>`.
///
/// This is intended for environments without filesystem access (for example,
/// running in a web browser). You can serialize an in-memory GeoPackage and
/// move the bytes over the wire or store them in browser storage.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_in_memory()?;
/// let bytes = gpkg.to_bytes()?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let data: &[u8] = &self.conn.serialize("main")?;
Ok(data.to_vec())
}
/// Load the GeoPackage data from a dump.
///
/// This is intended for environments without filesystem access (for example,
/// running in a web browser). Provide the bytes from `Gpkg::to_bytes()` to
/// recreate an in-memory GeoPackage.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_in_memory()?;
/// let bytes = gpkg.to_bytes()?;
/// let restored = Gpkg::from_bytes(&bytes)?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn from_bytes<D: AsRef<[u8]>>(data: D) -> Result<Self> {
let mut conn = rusqlite::Connection::open_in_memory()?;
let data_ref = data.as_ref();
let reader = std::io::Cursor::new(data_ref);
conn.deserialize_read_exact("main", reader, data_ref.len(), false)?;
Ok(Self {
conn: Rc::new(conn),
read_only: false,
})
}
/// Load an attribute table definition by name.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
/// let table = gpkg.get_attribute_table("observations")?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn get_attribute_table(&self, table_name: &str) -> Result<GpkgAttributeTable> {
let data_type = self.get_data_type(table_name)?;
if data_type != "attributes" {
if data_type == "features" {
return Err(GpkgError::NotAnAttributeTable {
layer_name: table_name.to_string(),
});
}
return Err(GpkgError::UnsupportedDataType {
layer_name: table_name.to_string(),
data_type,
});
}
let (primary_key_column, other_columns) = self.get_attribute_column_specs(table_name)?;
let insert_sql = GpkgAttributeTable::build_insert_sql(table_name, &other_columns);
let update_sql =
GpkgAttributeTable::build_update_sql(table_name, &primary_key_column, &other_columns);
let property_index_by_name = Rc::new(GpkgAttributeTable::build_property_index_by_name(
&other_columns,
));
Ok(GpkgAttributeTable {
conn: self.conn.clone(),
is_read_only: self.read_only,
table_name: table_name.to_string(),
primary_key_column,
property_columns: other_columns,
property_index_by_name,
insert_sql,
update_sql,
})
}
/// Create a new attribute table (non-spatial, no geometry column).
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::{ColumnSpec, ColumnType, Gpkg, params};
///
/// let gpkg = Gpkg::open_in_memory()?;
/// let columns = vec![
/// ColumnSpec { name: "name".to_string(), column_type: ColumnType::Varchar },
/// ColumnSpec { name: "value".to_string(), column_type: ColumnType::Integer },
/// ];
/// let table = gpkg.create_attribute_table("observations", &columns)?;
/// table.insert(params!["alpha", 7_i64])?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn create_attribute_table(
&self,
table_name: &str,
column_specs: &[ColumnSpec],
) -> Result<GpkgAttributeTable> {
if self.read_only {
return Err(GpkgError::ReadOnly);
}
if self.table_exists_in_contents(table_name)? {
return Err(GpkgError::LayerAlreadyExists {
layer_name: table_name.to_string(),
});
}
// Attribute tables must not have geometry columns.
if let Some(spec) = column_specs
.iter()
.find(|s| s.column_type == crate::types::ColumnType::Geometry)
{
return Err(GpkgError::GeometryColumnInAttributeTable {
column: spec.name.clone(),
});
}
let mut column_defs = Vec::with_capacity(column_specs.len() + 1);
column_defs.push("fid INTEGER PRIMARY KEY AUTOINCREMENT".to_string());
for spec in column_specs {
let col_type = crate::conversions::column_type_to_str(spec.column_type);
column_defs.push(format!(r#""{}" {col_type}"#, spec.name));
}
let create_sql = sql_create_table(table_name, &column_defs.join(", "));
self.conn.execute_batch(&create_sql)?;
self.conn.execute(
SQL_INSERT_GPKG_CONTENTS_ATTRIBUTES,
rusqlite::params![table_name, table_name],
)?;
let insert_sql = GpkgAttributeTable::build_insert_sql(table_name, column_specs);
let update_sql = GpkgAttributeTable::build_update_sql(table_name, "fid", column_specs);
let property_index_by_name = Rc::new(GpkgAttributeTable::build_property_index_by_name(
column_specs,
));
Ok(GpkgAttributeTable {
conn: self.conn.clone(),
is_read_only: self.read_only,
table_name: table_name.to_string(),
primary_key_column: "fid".to_string(),
property_columns: column_specs.to_vec(),
property_index_by_name,
insert_sql,
update_sql,
})
}
/// Delete an attribute table.
///
/// Example:
/// ```no_run
/// use rusqlite_gpkg::Gpkg;
///
/// let gpkg = Gpkg::open("data/example.gpkg")?;
/// gpkg.delete_attribute_table("observations")?;
/// # Ok::<(), rusqlite_gpkg::GpkgError>(())
/// ```
pub fn delete_attribute_table(&self, table_name: &str) -> Result<()> {
if self.read_only {
return Err(GpkgError::ReadOnly);
}
let data_type = self.get_data_type(table_name)?;
if data_type != "attributes" {
if data_type == "features" {
return Err(GpkgError::NotAnAttributeTable {
layer_name: table_name.to_string(),
});
}
return Err(GpkgError::UnsupportedDataType {
layer_name: table_name.to_string(),
data_type,
});
}
self.conn.execute_batch(&sql_drop_table(table_name))?;
self.conn.execute(
"DELETE FROM gpkg_contents WHERE table_name = ?1",
rusqlite::params![table_name],
)?;
Ok(())
}
/// Check whether a table name already exists in `gpkg_contents` (any data_type).
fn table_exists_in_contents(&self, table_name: &str) -> Result<bool> {
let exists: i64 = self.conn.query_row(
"SELECT EXISTS(SELECT 1 FROM gpkg_contents WHERE table_name = ?1)",
rusqlite::params![table_name],
|row| row.get(0),
)?;
Ok(exists == 1)
}
/// Look up the `data_type` for a table in `gpkg_contents`.
pub(crate) fn get_data_type(&self, table_name: &str) -> Result<String> {
let mut stmt = self.conn.prepare(SQL_SELECT_DATA_TYPE)?;
let data_type = stmt.query_one([table_name], |row| row.get::<_, String>(0))?;
Ok(data_type)
}
/// Resolve column specs for an attribute table (no geometry column expected).
pub(crate) fn get_attribute_column_specs(
&self,
table_name: &str,
) -> Result<(String, Vec<ColumnSpec>)> {
let query = sql_table_columns(table_name);
let mut stmt = self.conn.prepare(&query)?;
let column_specs = stmt.query_map([], |row| {
let name: String = row.get(0)?;
let column_type_str: String = row.get(1)?;
let primary_key: i32 = row.get(2)?;
let primary_key = primary_key != 0;
Ok((name, column_type_str, primary_key))
})?;
let result: std::result::Result<Vec<(String, String, bool)>, _> = column_specs.collect();
let mut primary_key_column: Option<String> = None;
let mut other_columns = Vec::new();
for (name, column_type_str, is_primary_key) in result? {
let column_type = crate::conversions::column_type_from_str(&column_type_str)
.ok_or_else(|| GpkgError::UnsupportedColumnType {
column: name.clone(),
declared_type: column_type_str,
})?;
if is_primary_key {
if primary_key_column.is_some() {
return Err(GpkgError::CompositePrimaryKeyUnsupported {
layer_name: table_name.to_string(),
});
}
primary_key_column = Some(name);
continue;
}
if column_type == crate::types::ColumnType::Geometry {
return Err(GpkgError::GeometryColumnInAttributeTable {
column: name,
});
}
other_columns.push(ColumnSpec { name, column_type });
}
let primary_key_column =
primary_key_column.ok_or_else(|| GpkgError::MissingPrimaryKeyColumn {
layer_name: table_name.to_string(),
})?;
Ok((primary_key_column, other_columns))
}
/// Resolve the table columns and map SQLite types.
pub(crate) fn get_column_specs(
&self,
layer_name: &str,
geometry_column: &str,
geometry_type: wkb::reader::GeometryType,
geometry_dimension: wkb::reader::Dimension,
srs_id: u32,
) -> Result<GpkgLayerMetadata> {
let query = sql_table_columns(layer_name);
let mut stmt = self.conn.prepare(&query)?;
let mut primary_key_column: Option<String> = None;
let mut geometry_column_name: Option<String> = None;
let column_specs = stmt.query_map([], |row| {
let name: String = row.get(0)?;
let column_type_str: String = row.get(1)?;
let primary_key: i32 = row.get(2)?;
let primary_key = primary_key != 0;
Ok((name, column_type_str, primary_key))
})?;
let result: std::result::Result<Vec<(String, String, bool)>, _> = column_specs.collect();
let mut other_columns = Vec::new();
for (name, column_type_str, is_primary_key) in result? {
// cf. https://www.geopackage.org/spec140/index.html#_sqlite_container
let column_type = column_type_from_str(&column_type_str).ok_or_else(|| {
GpkgError::UnsupportedColumnType {
column: name.clone(),
declared_type: column_type_str,
}
})?;
if is_primary_key {
if primary_key_column.is_some() {
return Err(GpkgError::CompositePrimaryKeyUnsupported {
layer_name: layer_name.to_string(),
});
}
primary_key_column = Some(name.clone());
continue;
}
if name == geometry_column {
geometry_column_name = Some(name.clone());
} else {
other_columns.push(ColumnSpec { name, column_type });
}
}
let primary_key_column =
primary_key_column.ok_or_else(|| GpkgError::MissingPrimaryKeyColumn {
layer_name: layer_name.to_string(),
})?;
let geometry_column =
geometry_column_name.ok_or_else(|| GpkgError::MissingGeometryColumn {
layer_name: layer_name.to_string(),
})?;
Ok(GpkgLayerMetadata {
primary_key_column,
geometry_column,
geometry_type,
geometry_dimension,
srs_id,
other_columns,
})
}
/// Resolve the geometry column metadata and SRS information for a layer.
pub(crate) fn get_geometry_column_and_srs_id(
&self,
layer_name: &str,
) -> Result<(
String,
wkb::reader::GeometryType,
wkb::reader::Dimension,
u32,
)> {
let mut stmt = self.conn.prepare(SQL_SELECT_GEOMETRY_COLUMN_META)?;
let (geometry_column, geometry_type_str, z, m, srs_id) =
stmt.query_one([layer_name], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i8>(2)?,
row.get::<_, i8>(3)?,
row.get::<_, u32>(4)?,
))
})?;
let geometry_type = geometry_type_from_str(&geometry_type_str)?;
let geometry_dimension = dimension_from_zm(z, m)?;
Ok((geometry_column, geometry_type, geometry_dimension, srs_id))
}
}
#[cfg(test)]
mod tests {
use super::Gpkg;
use crate::error::GpkgError;
use crate::params;
use crate::types::{ColumnSpec, ColumnType};
use geo_types::Point;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use wkb::reader::{Dimension, GeometryType};
#[test]
fn create_layer_requires_existing_srs() {
let gpkg = Gpkg::open_in_memory().expect("new gpkg");
let columns: Vec<ColumnSpec> = Vec::new();
let err = gpkg
.create_layer(
"missing_srs",
"geom",
wkb::reader::GeometryType::Point,
wkb::reader::Dimension::Xy,
9999,
&columns,
)
.expect_err("missing srs should fail");
assert!(matches!(
err,
GpkgError::MissingSpatialRefSysId { srs_id: 9999 }
));
}
#[test]
fn delete_layer_rejects_read_only() {
let gpkg =
Gpkg::open_read_only("src/test/test_generated.gpkg").expect("open read-only gpkg");
let err = gpkg
.delete_layer("points")
.expect_err("read-only should fail");
assert!(matches!(err, GpkgError::ReadOnly));
}
#[test]
fn dump_roundtrips_in_memory_gpkg() -> Result<(), GpkgError> {
let gpkg = Gpkg::open_in_memory()?;
let columns = vec![
ColumnSpec {
name: "name".to_string(),
column_type: ColumnType::Varchar,
},
ColumnSpec {
name: "value".to_string(),
column_type: ColumnType::Integer,
},
];
let layer = gpkg.create_layer(
"points",
"geom",
GeometryType::Point,
Dimension::Xy,
4326,
&columns,
)?;
let name_a = "alpha".to_string();
let value_a = 7_i64;
layer.insert(Point::new(1.0, 2.0), params![name_a, value_a])?;
let name_b = "beta".to_string();
let value_b = 9_i64;
layer.insert(Point::new(-3.0, 4.5), params![name_b, value_b])?;
let dump = gpkg.to_bytes()?;
let mut path = std::env::temp_dir();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time")
.as_nanos();
path.push(format!("rusqlite_gpkg_dump_{nanos}.gpkg"));
fs::write(&path, dump).unwrap();
let reopened = Gpkg::open_read_only(&path)?;
let layers = reopened.list_layers()?;
assert_eq!(layers, vec!["points".to_string()]);
let reopened_layer = reopened.get_layer("points")?;
let features = reopened_layer.features()?;
assert_eq!(features.len(), 2);
assert_eq!(features[0].id(), 1);
let name: String = features[0]
.property("name")
.ok_or_else(|| GpkgError::MissingProperty {
property: "name".to_string(),
})?
.try_into()?;
assert_eq!(name, "alpha");
let value: i64 = features[0]
.property("value")
.ok_or_else(|| GpkgError::MissingProperty {
property: "value".to_string(),
})?
.try_into()?;
assert_eq!(value, 7);
assert_eq!(features[0].geometry()?.geometry_type(), GeometryType::Point);
assert_eq!(features[1].id(), 2);
let name: String = features[1]
.property("name")
.ok_or_else(|| GpkgError::MissingProperty {
property: "name".to_string(),
})?
.try_into()?;
assert_eq!(name, "beta");
let value: i64 = features[1]
.property("value")
.ok_or_else(|| GpkgError::MissingProperty {
property: "value".to_string(),
})?
.try_into()?;
assert_eq!(value, 9);
assert_eq!(features[1].geometry()?.geometry_type(), GeometryType::Point);
let _ = fs::remove_file(&path);
Ok(())
}
#[test]
fn dump_roundtrips_in_memory_gpkg_from_bytes() -> Result<(), GpkgError> {
let gpkg = Gpkg::open_in_memory()?;
let columns = vec![
ColumnSpec {
name: "name".to_string(),
column_type: ColumnType::Varchar,
},
ColumnSpec {
name: "value".to_string(),
column_type: ColumnType::Integer,
},
];
let layer = gpkg.create_layer(
"points",
"geom",
GeometryType::Point,
Dimension::Xy,
4326,
&columns,
)?;
let name_a = "alpha".to_string();
let value_a = 7_i64;
layer.insert(Point::new(1.0, 2.0), params![name_a, value_a])?;
let name_b = "beta".to_string();
let value_b = 9_i64;
layer.insert(Point::new(-3.0, 4.5), params![name_b, value_b])?;
let dump = gpkg.to_bytes()?;
let restored = Gpkg::from_bytes(&dump)?;
let layers = restored.list_layers()?;
assert_eq!(layers, vec!["points".to_string()]);
let restored_layer = restored.get_layer("points")?;
let features = restored_layer.features()?;
assert_eq!(features.len(), 2);
assert_eq!(features[0].id(), 1);
let name: String = features[0]
.property("name")
.ok_or_else(|| GpkgError::MissingProperty {
property: "name".to_string(),
})?
.try_into()?;
assert_eq!(name, "alpha");
let value: i64 = features[0]
.property("value")
.ok_or_else(|| GpkgError::MissingProperty {
property: "value".to_string(),
})?
.try_into()?;
assert_eq!(value, 7);
assert_eq!(features[0].geometry()?.geometry_type(), GeometryType::Point);
assert_eq!(features[1].id(), 2);
let name: String = features[1]
.property("name")
.ok_or_else(|| GpkgError::MissingProperty {
property: "name".to_string(),
})?
.try_into()?;
assert_eq!(name, "beta");
let value: i64 = features[1]
.property("value")
.ok_or_else(|| GpkgError::MissingProperty {
property: "value".to_string(),
})?
.try_into()?;
assert_eq!(value, 9);
assert_eq!(features[1].geometry()?.geometry_type(), GeometryType::Point);
Ok(())
}
}