use geopackage_core::ddl;
use geopackage_core::extensions::{Extension, GEOM_TYPE_EXTENSION_DEFINITION};
use geopackage_core::ident::quote;
use geopackage_core::types::{ColumnType, GeometryType, ZmFlag};
use rusqlite::Connection;
use crate::extensions;
use crate::transaction::WriteTransaction;
use crate::{Error, GeoPackage, Layer, Result, table_exists};
pub const DEFAULT_PRIMARY_KEY: &str = "fid";
pub const DEFAULT_GEOMETRY_COLUMN: &str = "geom";
#[derive(Debug, Clone)]
pub struct ColumnSpec {
name: String,
column_type: ColumnType,
not_null: bool,
unique: bool,
default: Option<String>,
}
impl ColumnSpec {
pub fn new(name: impl Into<String>, column_type: ColumnType) -> Self {
Self {
name: name.into(),
column_type,
not_null: false,
unique: false,
default: None,
}
}
#[must_use]
pub fn not_null(mut self) -> Self {
self.not_null = true;
self
}
#[must_use]
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
#[must_use]
pub fn default_value(mut self, sql: impl Into<String>) -> Self {
self.default = Some(sql.into());
self
}
fn to_ddl(&self) -> Result<String> {
let mut ddl = format!("{} {}", quote(&self.name)?, self.column_type.ddl_name());
if self.not_null {
ddl.push_str(" NOT NULL");
}
if let Some(default) = &self.default {
ddl.push_str(" DEFAULT ");
ddl.push_str(default);
}
if self.unique {
ddl.push_str(" UNIQUE");
}
Ok(ddl)
}
}
#[derive(Debug, Clone)]
pub struct GeometrySpec {
column_name: String,
geometry_type: GeometryType,
srs_id: i32,
z: ZmFlag,
m: ZmFlag,
}
impl GeometrySpec {
pub fn new(geometry_type: GeometryType, srs_id: i32) -> Self {
Self {
column_name: DEFAULT_GEOMETRY_COLUMN.to_owned(),
geometry_type,
srs_id,
z: ZmFlag::Prohibited,
m: ZmFlag::Prohibited,
}
}
#[must_use]
pub fn column_name(mut self, name: impl Into<String>) -> Self {
self.column_name = name.into();
self
}
#[must_use]
pub fn z(mut self, flag: ZmFlag) -> Self {
self.z = flag;
self
}
#[must_use]
pub fn m(mut self, flag: ZmFlag) -> Self {
self.m = flag;
self
}
pub fn geometry_type(&self) -> GeometryType {
self.geometry_type
}
pub fn srs_id(&self) -> i32 {
self.srs_id
}
}
#[derive(Debug, Clone)]
pub struct TableSchemaBuilder {
table_name: String,
identifier: Option<String>,
description: Option<String>,
primary_key: String,
columns: Vec<ColumnSpec>,
geometry: Option<GeometrySpec>,
spatial_index: bool,
}
impl TableSchemaBuilder {
pub fn new(table_name: impl Into<String>) -> Self {
Self {
table_name: table_name.into(),
identifier: None,
description: None,
primary_key: DEFAULT_PRIMARY_KEY.to_owned(),
columns: Vec::new(),
geometry: None,
spatial_index: true,
}
}
#[must_use]
pub fn primary_key(mut self, name: impl Into<String>) -> Self {
self.primary_key = name.into();
self
}
pub fn primary_key_name(&self) -> &str {
&self.primary_key
}
#[must_use]
pub fn spatial_index(mut self, spatial_index: bool) -> Self {
self.spatial_index = spatial_index;
self
}
#[must_use]
pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
self.identifier = Some(identifier.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn column(mut self, column: ColumnSpec) -> Self {
self.columns.push(column);
self
}
#[must_use]
pub fn geometry(mut self, geometry: GeometrySpec) -> Self {
self.geometry = Some(geometry);
self
}
pub fn table_name(&self) -> &str {
&self.table_name
}
fn create_table_sql(&self) -> Result<String> {
let mut defs = Vec::with_capacity(self.columns.len() + 2);
defs.push(format!(
"{} INTEGER PRIMARY KEY AUTOINCREMENT",
quote(&self.primary_key)?
));
for column in &self.columns {
defs.push(column.to_ddl()?);
}
if let Some(geometry) = &self.geometry {
defs.push(format!(
"{} {}",
quote(&geometry.column_name)?,
geometry.geometry_type.as_str()
));
}
Ok(format!(
"CREATE TABLE {} ({})",
quote(&self.table_name)?,
defs.join(", ")
))
}
}
impl GeoPackage {
pub fn create_layer(&self, builder: &TableSchemaBuilder) -> Result<Layer<'_>> {
self.check_writable(&builder.table_name)?;
let geometry = builder
.geometry
.as_ref()
.ok_or_else(|| Error::MissingGeometrySpec {
table_name: builder.table_name.clone(),
})?;
let conn = self.connection();
let tx = WriteTransaction::begin(conn)?;
self.create_table_in(conn, builder, Some(geometry))?;
if builder.spatial_index {
crate::index::create_index_in_transaction(
conn,
&builder.table_name,
&geometry.column_name,
&builder.primary_key,
)?;
}
tx.commit()?;
self.layer(&builder.table_name)
}
pub fn create_attributes_table(&self, builder: &TableSchemaBuilder) -> Result<Layer<'_>> {
if builder.geometry.is_some() {
return Err(Error::UnexpectedGeometrySpec {
table_name: builder.table_name.clone(),
});
}
self.create_table(builder, None)?;
self.attributes(&builder.table_name)
}
fn create_table(
&self,
builder: &TableSchemaBuilder,
geometry: Option<&GeometrySpec>,
) -> Result<()> {
let conn = self.connection();
let tx = WriteTransaction::begin(conn)?;
self.create_table_in(conn, builder, geometry)?;
tx.commit()?;
Ok(())
}
fn create_table_in(
&self,
tx: &Connection,
builder: &TableSchemaBuilder,
geometry: Option<&GeometrySpec>,
) -> Result<()> {
let name = &builder.table_name;
if name
.get(..5)
.is_some_and(|p| p.eq_ignore_ascii_case("gpkg_"))
{
return Err(Error::ReservedTablePrefix {
table_name: name.clone(),
});
}
if table_exists(tx, name)? {
return Err(Error::TableAlreadyExists {
table_name: name.clone(),
});
}
if let Some(geometry) = geometry
&& self.srs(geometry.srs_id)?.is_none()
{
return Err(Error::UnknownSrs {
srs_id: geometry.srs_id,
});
}
let create_sql = builder.create_table_sql()?;
let data_type = if geometry.is_some() {
"features"
} else {
"attributes"
};
let identifier = builder.identifier.clone().unwrap_or_else(|| name.clone());
let description = builder.description.clone().unwrap_or_default();
let contents_srs: Option<i32> = geometry.map(|g| g.srs_id);
if geometry.is_some() && !table_exists(tx, "gpkg_geometry_columns")? {
tx.execute_batch(ddl::CREATE_GPKG_GEOMETRY_COLUMNS)?;
}
tx.execute_batch(&create_sql)?;
tx.execute(
"INSERT INTO gpkg_contents \
(table_name, data_type, identifier, description, srs_id) \
VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![name, data_type, identifier, description, contents_srs],
)?;
if let Some(geometry) = geometry {
tx.execute(
"INSERT INTO gpkg_geometry_columns \
(table_name, column_name, geometry_type_name, srs_id, z, m) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
name,
geometry.column_name,
geometry.geometry_type.as_str(),
geometry.srs_id,
geometry.z.code(),
geometry.m.code(),
],
)?;
if geometry.geometry_type.is_extension() {
let extension = Extension::GeometryType(geometry.geometry_type);
extensions::register(
tx,
Some(name),
Some(&geometry.column_name),
&extension.name(),
GEOM_TYPE_EXTENSION_DEFINITION,
"read-write",
)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feature_table_ddl() {
let builder = TableSchemaBuilder::new("roads")
.column(ColumnSpec::new("name", ColumnType::Text(Some(64))))
.column(
ColumnSpec::new("lanes", ColumnType::MediumInt)
.not_null()
.default_value("2"),
)
.geometry(GeometrySpec::new(GeometryType::LineString, 4326));
assert_eq!(
builder.create_table_sql().unwrap(),
"CREATE TABLE \"roads\" (\
\"fid\" INTEGER PRIMARY KEY AUTOINCREMENT, \
\"name\" TEXT(64), \
\"lanes\" MEDIUMINT NOT NULL DEFAULT 2, \
\"geom\" LINESTRING)"
);
}
#[test]
fn attributes_table_ddl_and_custom_pk() {
let builder = TableSchemaBuilder::new("notes")
.primary_key("id")
.column(ColumnSpec::new("body", ColumnType::Text(None)).unique());
assert_eq!(
builder.create_table_sql().unwrap(),
"CREATE TABLE \"notes\" (\
\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \
\"body\" TEXT UNIQUE)"
);
}
#[test]
fn quotes_awkward_identifiers() {
let builder = TableSchemaBuilder::new("we\"ird")
.column(ColumnSpec::new("select", ColumnType::Integer))
.geometry(GeometrySpec::new(GeometryType::Point, 4326).column_name("the geom"));
assert_eq!(
builder.create_table_sql().unwrap(),
"CREATE TABLE \"we\"\"ird\" (\
\"fid\" INTEGER PRIMARY KEY AUTOINCREMENT, \
\"select\" INTEGER, \
\"the geom\" POINT)"
);
}
}