mod bulk;
mod create;
mod error;
mod functions;
mod index;
mod layer;
mod open;
mod options;
mod packed;
mod schema;
mod srs;
mod value;
mod writer;
pub use bulk::{BulkIndexOptions, DEFAULT_BULK_THRESHOLD, DEFAULT_FILL_FACTOR, StructuralCheck};
pub use create::{
ColumnSpec, DEFAULT_GEOMETRY_COLUMN, DEFAULT_PRIMARY_KEY, GeometrySpec, TableSchemaBuilder,
};
pub use error::{Error, Result};
pub use geopackage_core as core;
pub use geopackage_core::GpkgVersion;
pub use index::SpatialIndexStatus;
pub use layer::{BoundingBox, Feature, FeatureCursor, FeatureStream, Features, Layer, LayerKind};
pub use open::OpenWarning;
pub use options::{JournalMode, OpenOptions, Synchronous};
pub use schema::{Column, GeometryColumn, TableSchema};
pub use srs::Srs;
pub use value::{ConversionOptions, DateTimeParsing, Value};
pub use writer::{FeatureWriter, NewFeature};
use geopackage_core::{ddl, version};
use rusqlite::{Connection, OpenFlags, OptionalExtension};
use std::path::Path;
pub struct GeoPackage {
conn: Option<Connection>,
version: GpkgVersion,
warnings: Vec<OpenWarning>,
journal_mode: JournalMode,
}
impl GeoPackage {
pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
OpenOptions::new().create(path)
}
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
OpenOptions::new().open(path)
}
pub fn open_read_only<P: AsRef<Path>>(path: P) -> Result<Self> {
OpenOptions::new().open_read_only(path)
}
pub fn from_connection(conn: Connection) -> Result<Self> {
Self::from_connection_configured(conn, OpenOptions::new(), false)
}
pub(crate) fn create_configured(path: &Path, options: OpenOptions) -> Result<Self> {
if path.exists()
&& std::fs::metadata(path)
.map(|m| m.len() > 0)
.unwrap_or(false)
{
return Err(Error::AlreadyExists(path.to_owned()));
}
let conn = Connection::open(path)?;
conn.pragma_update(None, "application_id", version::APPLICATION_ID_GPKG)?;
conn.pragma_update(
None,
"user_version",
GpkgVersion::V1_4
.user_version()
.expect("1.4 has a user_version"),
)?;
conn.pragma_update(None, "foreign_keys", true)?;
let journal_mode = apply_open_options(&conn, options, true)?;
let tx = conn.unchecked_transaction()?;
tx.execute_batch(&format!(
"{};\n{};",
ddl::CREATE_GPKG_SPATIAL_REF_SYS,
ddl::CREATE_GPKG_CONTENTS
))?;
for stmt in ddl::SEED_SPATIAL_REF_SYS {
tx.execute(stmt, [])?;
}
tx.commit()?;
functions::register(&conn)?;
Ok(Self {
conn: Some(conn),
version: GpkgVersion::V1_4,
warnings: Vec::new(),
journal_mode,
})
}
pub(crate) fn open_configured(
path: &Path,
flags: OpenFlags,
options: OpenOptions,
) -> Result<Self> {
let conn = Connection::open_with_flags(path, flags)?;
let apply_journal = flags.contains(OpenFlags::SQLITE_OPEN_READ_WRITE);
Self::from_connection_configured(conn, options, apply_journal)
}
pub(crate) fn from_connection_configured(
conn: Connection,
options: OpenOptions,
apply_journal: bool,
) -> Result<Self> {
let application_id = read_header_u32(&conn, "application_id")?;
let user_version = read_header_u32(&conn, "user_version")?;
let version = GpkgVersion::from_pragmas(application_id, user_version).ok_or(
Error::NotAGeoPackage {
reason: "unrecognized application_id/user_version",
application_id,
user_version,
},
)?;
for required in ["gpkg_spatial_ref_sys", "gpkg_contents"] {
if !table_exists(&conn, required)? {
return Err(Error::NotAGeoPackage {
reason: "missing required core table",
application_id,
user_version,
});
}
}
let journal_mode = apply_open_options(&conn, options, apply_journal)?;
functions::register(&conn)?;
Ok(Self {
conn: Some(conn),
version,
warnings: Vec::new(),
journal_mode,
})
}
pub fn version(&self) -> GpkgVersion {
self.version
}
pub fn contents(&self) -> Result<Vec<ContentsEntry>> {
let mut stmt = self.connection().prepare(
"SELECT table_name, data_type, identifier, srs_id, min_x, min_y, max_x, max_y \
FROM gpkg_contents ORDER BY table_name",
)?;
let rows = stmt.query_map([], |r| {
Ok(ContentsEntry {
table_name: r.get(0)?,
data_type: ContentsDataType::from_str(&r.get::<_, String>(1)?),
identifier: r.get(2)?,
srs_id: r.get(3)?,
min_x: r.get(4)?,
min_y: r.get(5)?,
max_x: r.get(6)?,
max_y: r.get(7)?,
})
})?;
Ok(rows.collect::<rusqlite::Result<_>>()?)
}
pub fn connection(&self) -> &Connection {
self.conn
.as_ref()
.expect("connection is present for the whole handle lifetime")
}
pub fn into_connection(mut self) -> Connection {
self.conn
.take()
.expect("connection is present until into_connection consumes it")
}
pub fn close(mut self) -> Result<()> {
if self.journal_mode == JournalMode::Wal
&& let Some(conn) = self.conn.as_ref()
{
finalize_wal_to_delete(conn)?;
self.journal_mode = JournalMode::Delete;
}
Ok(())
}
}
impl Drop for GeoPackage {
fn drop(&mut self) {
if self.journal_mode == JournalMode::Wal
&& let Some(conn) = self.conn.as_ref()
&& finalize_wal_to_delete(conn).is_err()
{
}
}
}
fn apply_open_options(
conn: &Connection,
options: OpenOptions,
apply_journal: bool,
) -> Result<JournalMode> {
if let Some(synchronous) = options.synchronous {
conn.pragma_update(None, "synchronous", synchronous.code())?;
}
if apply_journal && let Some(mode) = options.journal_mode {
conn.query_row(
&format!("PRAGMA journal_mode = {}", mode.keyword()),
[],
|_| Ok(()),
)?;
return Ok(mode);
}
Ok(JournalMode::Delete)
}
fn finalize_wal_to_delete(conn: &Connection) -> Result<()> {
conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))?;
conn.query_row("PRAGMA journal_mode = DELETE", [], |_| Ok(()))?;
Ok(())
}
#[expect(
clippy::cast_sign_loss,
reason = "application_id/user_version are 32-bit header magics; reading their low bits as unsigned is intentional, and preserves the bit pattern for any header value"
)]
pub(crate) fn read_header_u32(conn: &Connection, pragma: &str) -> rusqlite::Result<u32> {
Ok(conn.pragma_query_value(None, pragma, |r| r.get::<_, i64>(0))? as u32)
}
pub(crate) fn table_exists(conn: &Connection, name: &str) -> rusqlite::Result<bool> {
conn.query_row(
"SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = ?1",
[name],
|_| Ok(()),
)
.optional()
.map(|o| o.is_some())
}
pub(crate) fn resolve_table_name(
conn: &Connection,
name: &str,
) -> rusqlite::Result<Option<String>> {
conn.query_row(
"SELECT name FROM sqlite_master \
WHERE type IN ('table','view') AND name = ?1 COLLATE NOCASE",
[name],
|r| r.get::<_, String>(0),
)
.optional()
}
#[derive(Debug, Clone, PartialEq)]
pub struct ContentsEntry {
pub table_name: String,
pub data_type: ContentsDataType,
pub identifier: Option<String>,
pub srs_id: Option<i32>,
pub min_x: Option<f64>,
pub min_y: Option<f64>,
pub max_x: Option<f64>,
pub max_y: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContentsDataType {
Features,
Tiles,
Attributes,
Other(String),
}
impl ContentsDataType {
fn from_str(s: &str) -> Self {
match s {
"features" => Self::Features,
"tiles" => Self::Tiles,
"attributes" => Self::Attributes,
other => Self::Other(other.to_owned()),
}
}
}