rusqlite_gpkg/gpkg/
gpkg.rs

1use crate::conversions::{
2    column_type_from_str, column_type_to_str, dimension_from_zm, dimension_to_zm,
3    geometry_type_from_str, geometry_type_to_str,
4};
5use crate::error::{GpkgError, Result};
6use crate::ogc_sql::{
7    SQL_INSERT_GPKG_CONTENTS, SQL_INSERT_GPKG_GEOMETRY_COLUMNS, SQL_LIST_LAYERS,
8    SQL_SELECT_GEOMETRY_COLUMN_META, execute_rtree_sqls, gpkg_rtree_drop_sql, initialize_gpkg,
9    sql_create_table, sql_drop_table, sql_table_columns,
10};
11use crate::sql_functions::register_spatial_functions;
12use crate::types::{ColumnSpec, GpkgLayerMetadata};
13use rusqlite::OpenFlags;
14use std::path::Path;
15use std::rc::Rc;
16
17use super::layer::GpkgLayer;
18
19#[derive(Debug)]
20/// GeoPackage connection wrapper for reading (and later writing) layers.
21pub struct Gpkg {
22    pub(crate) conn: Rc<rusqlite::Connection>,
23    pub(crate) read_only: bool,
24}
25
26#[cfg(not(target_family = "wasm"))]
27fn rusqlite_open_path<P: AsRef<Path>>(
28    path: P,
29    flags: rusqlite::OpenFlags,
30) -> rusqlite::Result<rusqlite::Connection> {
31    rusqlite::Connection::open_with_flags(path, flags)
32}
33
34// Use OPFS. cf. https://github.com/Spxg/sqlite-wasm-rs/issues/24
35#[cfg(target_family = "wasm")]
36fn rusqlite_open_path<P: AsRef<Path>>(
37    path: P,
38    flags: rusqlite::OpenFlags,
39) -> rusqlite::Result<rusqlite::Connection> {
40    rusqlite::Connection::open_with_flags_and_vfs(path, flags, "opfs-sahpool")
41}
42
43impl Gpkg {
44    pub(crate) fn new_from_conn(conn: Rc<rusqlite::Connection>, read_only: bool) -> Result<Self> {
45        register_spatial_functions(&conn)?;
46        Ok(Self { conn, read_only })
47    }
48
49    /// Open a GeoPackage in read-only mode.
50    ///
51    /// Example:
52    /// ```no_run
53    /// use rusqlite_gpkg::Gpkg;
54    ///
55    /// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
56    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
57    /// ```
58    pub fn open_read_only<P: AsRef<Path>>(path: P) -> Result<Self> {
59        let conn = rusqlite_open_path(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
60        Self::new_from_conn(Rc::new(conn), true)
61    }
62
63    /// Open a new or existing GeoPackage in read-write mode.
64    ///
65    /// Example:
66    /// ```no_run
67    /// use rusqlite_gpkg::Gpkg;
68    ///
69    /// let gpkg = Gpkg::open("data/example.gpkg")?;
70    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
71    /// ```
72    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
73        let path = path.as_ref();
74        let is_existing = path.exists();
75
76        let conn = rusqlite_open_path(path, rusqlite::OpenFlags::default())?;
77
78        // In the case of new file, initialize it
79        if !is_existing {
80            initialize_gpkg(&conn)?;
81        }
82
83        Self::new_from_conn(Rc::new(conn), false)
84    }
85
86    /// Create a new GeoPackage in memory.
87    ///
88    /// Example:
89    /// ```no_run
90    /// use rusqlite_gpkg::Gpkg;
91    ///
92    /// let gpkg = Gpkg::open_in_memory()?;
93    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
94    /// ```
95    pub fn open_in_memory() -> Result<Self> {
96        let conn = rusqlite::Connection::open_in_memory()?;
97
98        initialize_gpkg(&conn)?;
99
100        Self::new_from_conn(Rc::new(conn), false)
101    }
102
103    /// Expert-only: register a spatial reference system in gpkg_spatial_ref_sys.
104    ///
105    /// GeoPackage layers must reference a valid `srs_id` that already exists in
106    /// `gpkg_spatial_ref_sys`. The GeoPackage spec requires a full SRS definition
107    /// (notably the WKT `definition` and descriptive metadata). In practice, this
108    /// data is often sourced from an external authority like EPSG, but this crate
109    /// does not bundle or generate that catalog. As a result, callers must insert
110    /// SRS entries themselves before creating layers, which is why this low-level
111    /// helper exists.
112    ///
113    /// This method performs a direct insert with all required columns and does
114    /// no validation of the WKT or authority fields. Use only if you understand
115    /// the GeoPackage SRS requirements and have authoritative metadata.
116    ///
117    /// Example: register EPSG:3857 (Web Mercator / Pseudo-Mercator).
118    /// ```no_run
119    /// # use rusqlite_gpkg::Gpkg;
120    /// let gpkg = Gpkg::open_in_memory().expect("new gpkg");
121    /// 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"]]"#;
122    /// gpkg.register_srs(
123    ///     "WGS 84 / Pseudo-Mercator",
124    ///     3857,
125    ///     "EPSG",
126    ///     3857,
127    ///     definition,
128    ///     "Web Mercator / Pseudo-Mercator (EPSG:3857)",
129    /// ).expect("register srs");
130    /// ```
131    pub fn register_srs(
132        &self,
133        srs_name: &str,
134        srs_id: i32,
135        organization: &str,
136        organization_coordsys_id: i32,
137        definition: &str,
138        description: &str,
139    ) -> Result<()> {
140        if self.read_only {
141            return Err(GpkgError::ReadOnly);
142        }
143
144        self.conn.execute(
145            "INSERT INTO gpkg_spatial_ref_sys \
146            (srs_name, srs_id, organization, organization_coordsys_id, definition, description) \
147            VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
148            rusqlite::params![
149                srs_name,
150                srs_id,
151                organization,
152                organization_coordsys_id,
153                definition,
154                description
155            ],
156        )?;
157        Ok(())
158    }
159
160    /// List the names of the layers.
161    ///
162    /// Example:
163    /// ```no_run
164    /// use rusqlite_gpkg::Gpkg;
165    ///
166    /// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
167    /// let layers = gpkg.list_layers()?;
168    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
169    /// ```
170    pub fn list_layers(&self) -> Result<Vec<String>> {
171        let mut stmt = self.conn.prepare(SQL_LIST_LAYERS)?;
172        let layers = stmt
173            .query_map([], |row| row.get(0))?
174            .collect::<std::result::Result<Vec<String>, _>>()?;
175        Ok(layers)
176    }
177
178    /// Load a layer definition and metadata by name.
179    ///
180    /// Example:
181    /// ```no_run
182    /// use rusqlite_gpkg::Gpkg;
183    ///
184    /// let gpkg = Gpkg::open_read_only("data/example.gpkg")?;
185    /// let layer = gpkg.get_layer("points")?;
186    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
187    /// ```
188    pub fn get_layer(&self, layer_name: &str) -> Result<GpkgLayer> {
189        let (geometry_column, geometry_type, geometry_dimension, srs_id) =
190            self.get_geometry_column_and_srs_id(layer_name)?;
191        let column_specs = self.get_column_specs(
192            layer_name,
193            &geometry_column,
194            geometry_type,
195            geometry_dimension,
196            srs_id,
197        )?;
198        let primary_key_column = column_specs.primary_key_column.clone();
199        let other_columns = column_specs.other_columns;
200
201        let insert_sql = GpkgLayer::build_insert_sql(layer_name, &geometry_column, &other_columns);
202        let update_sql = GpkgLayer::build_update_sql(
203            layer_name,
204            &geometry_column,
205            &primary_key_column,
206            &other_columns,
207        );
208        let property_index_by_name =
209            Rc::new(GpkgLayer::build_property_index_by_name(&other_columns));
210
211        Ok(GpkgLayer {
212            conn: self.conn.clone(),
213            is_read_only: self.read_only,
214            layer_name: layer_name.to_string(),
215            geometry_column,
216            primary_key_column,
217            geometry_type,
218            geometry_dimension,
219            srs_id,
220            property_columns: other_columns,
221            property_index_by_name,
222            insert_sql,
223            update_sql,
224        })
225    }
226
227    // Create a new layer.
228    ///
229    /// Example:
230    /// ```no_run
231    /// use geo_types::Point;
232    /// use rusqlite_gpkg::{ColumnSpec, ColumnType, Gpkg, params};
233    ///
234    /// let gpkg = Gpkg::open_in_memory()?;
235    /// let columns = vec![ColumnSpec {
236    ///     name: "name".to_string(),
237    ///     column_type: ColumnType::Varchar,
238    /// }];
239    /// let layer = gpkg.create_layer(
240    ///     "points",
241    ///     "geom",
242    ///     wkb::reader::GeometryType::Point,
243    ///     wkb::reader::Dimension::Xy,
244    ///     4326,
245    ///     &columns,
246    /// )?;
247    /// layer.insert(Point::new(1.0, 2.0), params!["alpha"])?;
248    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
249    /// ```
250    pub fn create_layer(
251        &self,
252        layer_name: &str,
253        geometry_column: &str,
254        geometry_type: wkb::reader::GeometryType,
255        geometry_dimension: wkb::reader::Dimension,
256        srs_id: u32,
257        other_column_specs: &[ColumnSpec],
258    ) -> Result<GpkgLayer> {
259        if self.read_only {
260            return Err(GpkgError::ReadOnly);
261        }
262
263        if self.list_layers()?.iter().any(|name| name == layer_name) {
264            return Err(GpkgError::Message(format!(
265                "Layer already exists: {layer_name}"
266            )));
267        }
268
269        let srs_exists: i64 = self.conn.query_row(
270            "SELECT EXISTS(SELECT 1 FROM gpkg_spatial_ref_sys WHERE srs_id = ?1)",
271            rusqlite::params![srs_id],
272            |row| row.get(0),
273        )?;
274        if srs_exists == 0 {
275            return Err(GpkgError::Message(format!(
276                "srs_id {srs_id} not found in gpkg_spatial_ref_sys"
277            )));
278        }
279
280        let geometry_type_name = geometry_type_to_str(geometry_type);
281        let (z, m) = dimension_to_zm(geometry_dimension);
282
283        let mut column_defs = Vec::with_capacity(other_column_specs.len() + 2);
284        column_defs.push("fid INTEGER PRIMARY KEY AUTOINCREMENT".to_string());
285        column_defs.push(format!(r#""{}" BLOB"#, geometry_column));
286        for spec in other_column_specs {
287            let col_type = column_type_to_str(spec.column_type);
288            column_defs.push(format!(r#""{}" {col_type}"#, spec.name));
289        }
290
291        let create_sql = sql_create_table(layer_name, &column_defs.join(", "));
292        self.conn.execute_batch(&create_sql)?;
293
294        self.conn.execute(
295            SQL_INSERT_GPKG_CONTENTS,
296            rusqlite::params![layer_name, layer_name, srs_id],
297        )?;
298        self.conn.execute(
299            SQL_INSERT_GPKG_GEOMETRY_COLUMNS,
300            rusqlite::params![
301                layer_name,
302                geometry_column,
303                geometry_type_name,
304                srs_id,
305                z,
306                m
307            ],
308        )?;
309
310        execute_rtree_sqls(&self.conn, layer_name, geometry_column, "fid")?;
311
312        let insert_sql =
313            GpkgLayer::build_insert_sql(layer_name, geometry_column, other_column_specs);
314        let update_sql =
315            GpkgLayer::build_update_sql(layer_name, geometry_column, "fid", other_column_specs);
316        let property_index_by_name =
317            Rc::new(GpkgLayer::build_property_index_by_name(other_column_specs));
318
319        Ok(GpkgLayer {
320            conn: self.conn.clone(),
321            is_read_only: self.read_only,
322            layer_name: layer_name.to_string(),
323            geometry_column: geometry_column.to_string(),
324            primary_key_column: "fid".to_string(),
325            geometry_type,
326            geometry_dimension,
327            srs_id,
328            property_columns: other_column_specs.to_vec(),
329            property_index_by_name,
330            insert_sql,
331            update_sql,
332        })
333    }
334
335    /// Delete a layer.
336    ///
337    /// Example:
338    /// ```no_run
339    /// use rusqlite_gpkg::Gpkg;
340    ///
341    /// let gpkg = Gpkg::open("data/example.gpkg")?;
342    /// gpkg.delete_layer("points")?;
343    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
344    /// ```
345    pub fn delete_layer(&self, layer_name: &str) -> Result<()> {
346        if self.read_only {
347            return Err(GpkgError::ReadOnly);
348        }
349
350        let (geometry_column, _, _, _) = self.get_geometry_column_and_srs_id(layer_name)?;
351
352        self.conn
353            .execute_batch(&gpkg_rtree_drop_sql(layer_name, &geometry_column))?;
354
355        self.conn.execute_batch(&sql_drop_table(layer_name))?;
356        Ok(())
357    }
358
359    /// Dump the GeoPackage data to `Vec<u8>`.
360    ///
361    /// This is intended for environments without filesystem access (for example,
362    /// running in a web browser). You can serialize an in-memory GeoPackage and
363    /// move the bytes over the wire or store them in browser storage.
364    ///
365    /// Example:
366    /// ```no_run
367    /// use rusqlite_gpkg::Gpkg;
368    ///
369    /// let gpkg = Gpkg::open_in_memory()?;
370    /// let bytes = gpkg.to_bytes()?;
371    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
372    /// ```
373    pub fn to_bytes(&self) -> Result<Vec<u8>> {
374        let data: &[u8] = &self.conn.serialize("main")?;
375        Ok(data.to_vec())
376    }
377
378    /// Load the GeoPackage data from a dump.
379    ///
380    /// This is intended for environments without filesystem access (for example,
381    /// running in a web browser). Provide the bytes from `Gpkg::to_bytes()` to
382    /// recreate an in-memory GeoPackage.
383    ///
384    /// Example:
385    /// ```no_run
386    /// use rusqlite_gpkg::Gpkg;
387    ///
388    /// let gpkg = Gpkg::open_in_memory()?;
389    /// let bytes = gpkg.to_bytes()?;
390    /// let restored = Gpkg::from_bytes(&bytes)?;
391    /// # Ok::<(), rusqlite_gpkg::GpkgError>(())
392    /// ```
393    pub fn from_bytes<D: AsRef<[u8]>>(data: D) -> Result<Self> {
394        let mut conn = rusqlite::Connection::open_in_memory()?;
395
396        let data_ref = data.as_ref();
397        let reader = std::io::Cursor::new(data_ref);
398        conn.deserialize_read_exact("main", reader, data_ref.len(), false)?;
399        Ok(Self {
400            conn: Rc::new(conn),
401            read_only: false,
402        })
403    }
404
405    /// Resolve the table columns and map SQLite types.
406    pub(crate) fn get_column_specs(
407        &self,
408        layer_name: &str,
409        geometry_column: &str,
410        geometry_type: wkb::reader::GeometryType,
411        geometry_dimension: wkb::reader::Dimension,
412        srs_id: u32,
413    ) -> Result<GpkgLayerMetadata> {
414        let query = sql_table_columns(layer_name);
415        let mut stmt = self.conn.prepare(&query)?;
416
417        let mut primary_key_column: Option<String> = None;
418        let mut geometry_column_name: Option<String> = None;
419        let column_specs = stmt.query_map([], |row| {
420            let name: String = row.get(0)?;
421            let column_type_str: String = row.get(1)?;
422            let primary_key: i32 = row.get(2)?;
423            let primary_key = primary_key != 0;
424
425            // cf. https://www.geopackage.org/spec140/index.html#_sqlite_container
426            let column_type = column_type_from_str(&column_type_str).ok_or_else(|| {
427                rusqlite::Error::InvalidColumnType(
428                    1,
429                    format!("Unexpected type {}", column_type_str),
430                    rusqlite::types::Type::Text,
431                )
432            })?;
433
434            Ok((name, column_type, primary_key))
435        })?;
436
437        let result: std::result::Result<Vec<(String, crate::types::ColumnType, bool)>, _> =
438            column_specs.collect();
439        let mut other_columns = Vec::new();
440        for (name, column_type, is_primary_key) in result? {
441            if is_primary_key {
442                if primary_key_column.is_some() {
443                    return Err(GpkgError::Message(format!(
444                        "Composite primary keys are not supported yet for layer: {layer_name}"
445                    )));
446                }
447                primary_key_column = Some(name.clone());
448                continue;
449            }
450            if name == geometry_column {
451                geometry_column_name = Some(name.clone());
452            } else {
453                other_columns.push(ColumnSpec { name, column_type });
454            }
455        }
456
457        let primary_key_column = primary_key_column.ok_or_else(|| {
458            GpkgError::Message(format!(
459                "No primary key column found for layer: {layer_name}"
460            ))
461        })?;
462
463        let geometry_column = geometry_column_name.ok_or_else(|| {
464            GpkgError::Message(format!("No geometry column found for layer: {layer_name}"))
465        })?;
466
467        Ok(GpkgLayerMetadata {
468            primary_key_column,
469            geometry_column,
470            geometry_type,
471            geometry_dimension,
472            srs_id,
473            other_columns,
474        })
475    }
476
477    /// Resolve the geometry column metadata and SRS information for a layer.
478    pub(crate) fn get_geometry_column_and_srs_id(
479        &self,
480        layer_name: &str,
481    ) -> Result<(
482        String,
483        wkb::reader::GeometryType,
484        wkb::reader::Dimension,
485        u32,
486    )> {
487        let mut stmt = self.conn.prepare(SQL_SELECT_GEOMETRY_COLUMN_META)?;
488
489        let (geometry_column, geometry_type_str, z, m, srs_id) =
490            stmt.query_one([layer_name], |row| {
491                Ok((
492                    row.get::<_, String>(0)?,
493                    row.get::<_, String>(1)?,
494                    row.get::<_, i8>(2)?,
495                    row.get::<_, i8>(3)?,
496                    row.get::<_, u32>(4)?,
497                ))
498            })?;
499
500        let geometry_type = geometry_type_from_str(&geometry_type_str)?;
501        let geometry_dimension = dimension_from_zm(z, m)?;
502
503        Ok((geometry_column, geometry_type, geometry_dimension, srs_id))
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::Gpkg;
510    use crate::error::GpkgError;
511    use crate::params;
512    use crate::types::{ColumnSpec, ColumnType};
513    use geo_types::Point;
514    use std::fs;
515    use std::time::{SystemTime, UNIX_EPOCH};
516    use wkb::reader::{Dimension, GeometryType};
517
518    #[test]
519    fn create_layer_requires_existing_srs() {
520        let gpkg = Gpkg::open_in_memory().expect("new gpkg");
521        let columns: Vec<ColumnSpec> = Vec::new();
522        let err = gpkg
523            .create_layer(
524                "missing_srs",
525                "geom",
526                wkb::reader::GeometryType::Point,
527                wkb::reader::Dimension::Xy,
528                9999,
529                &columns,
530            )
531            .expect_err("missing srs should fail");
532
533        match err {
534            GpkgError::Message(message) => {
535                assert!(message.contains("srs_id 9999"));
536            }
537            other => panic!("unexpected error: {other:?}"),
538        }
539    }
540
541    #[test]
542    fn delete_layer_rejects_read_only() {
543        let gpkg =
544            Gpkg::open_read_only("src/test/test_generated.gpkg").expect("open read-only gpkg");
545        let err = gpkg
546            .delete_layer("points")
547            .expect_err("read-only should fail");
548        assert!(matches!(err, GpkgError::ReadOnly));
549    }
550
551    #[test]
552    fn dump_roundtrips_in_memory_gpkg() -> Result<(), GpkgError> {
553        let gpkg = Gpkg::open_in_memory()?;
554
555        let columns = vec![
556            ColumnSpec {
557                name: "name".to_string(),
558                column_type: ColumnType::Varchar,
559            },
560            ColumnSpec {
561                name: "value".to_string(),
562                column_type: ColumnType::Integer,
563            },
564        ];
565        let layer = gpkg.create_layer(
566            "points",
567            "geom",
568            GeometryType::Point,
569            Dimension::Xy,
570            4326,
571            &columns,
572        )?;
573
574        let name_a = "alpha".to_string();
575        let value_a = 7_i64;
576        layer.insert(Point::new(1.0, 2.0), params![name_a, value_a])?;
577        let name_b = "beta".to_string();
578        let value_b = 9_i64;
579        layer.insert(Point::new(-3.0, 4.5), params![name_b, value_b])?;
580
581        let dump = gpkg.to_bytes()?;
582        let mut path = std::env::temp_dir();
583        let nanos = SystemTime::now()
584            .duration_since(UNIX_EPOCH)
585            .expect("time")
586            .as_nanos();
587        path.push(format!("rusqlite_gpkg_dump_{nanos}.gpkg"));
588        fs::write(&path, dump).unwrap();
589
590        let reopened = Gpkg::open_read_only(&path)?;
591        let layers = reopened.list_layers()?;
592        assert_eq!(layers, vec!["points".to_string()]);
593
594        let reopened_layer = reopened.get_layer("points")?;
595        let features = reopened_layer.features()?;
596        assert_eq!(features.len(), 2);
597
598        assert_eq!(features[0].id(), 1);
599        let name: String = features[0]
600            .property("name")
601            .ok_or("missing name")?
602            .try_into()?;
603        assert_eq!(name, "alpha");
604        let value: i64 = features[0]
605            .property("value")
606            .ok_or("missing value")?
607            .try_into()?;
608        assert_eq!(value, 7);
609        assert_eq!(features[0].geometry()?.geometry_type(), GeometryType::Point);
610
611        assert_eq!(features[1].id(), 2);
612        let name: String = features[1]
613            .property("name")
614            .ok_or("missing name")?
615            .try_into()?;
616        assert_eq!(name, "beta");
617        let value: i64 = features[1]
618            .property("value")
619            .ok_or("missing value")?
620            .try_into()?;
621        assert_eq!(value, 9);
622        assert_eq!(features[1].geometry()?.geometry_type(), GeometryType::Point);
623
624        let _ = fs::remove_file(&path);
625        Ok(())
626    }
627
628    #[test]
629    fn dump_roundtrips_in_memory_gpkg_from_bytes() -> Result<(), GpkgError> {
630        let gpkg = Gpkg::open_in_memory()?;
631
632        let columns = vec![
633            ColumnSpec {
634                name: "name".to_string(),
635                column_type: ColumnType::Varchar,
636            },
637            ColumnSpec {
638                name: "value".to_string(),
639                column_type: ColumnType::Integer,
640            },
641        ];
642        let layer = gpkg.create_layer(
643            "points",
644            "geom",
645            GeometryType::Point,
646            Dimension::Xy,
647            4326,
648            &columns,
649        )?;
650
651        let name_a = "alpha".to_string();
652        let value_a = 7_i64;
653        layer.insert(Point::new(1.0, 2.0), params![name_a, value_a])?;
654        let name_b = "beta".to_string();
655        let value_b = 9_i64;
656        layer.insert(Point::new(-3.0, 4.5), params![name_b, value_b])?;
657
658        let dump = gpkg.to_bytes()?;
659
660        let restored = Gpkg::from_bytes(&dump)?;
661
662        let layers = restored.list_layers()?;
663        assert_eq!(layers, vec!["points".to_string()]);
664
665        let restored_layer = restored.get_layer("points")?;
666        let features = restored_layer.features()?;
667        assert_eq!(features.len(), 2);
668
669        assert_eq!(features[0].id(), 1);
670        let name: String = features[0]
671            .property("name")
672            .ok_or("missing name")?
673            .try_into()?;
674        assert_eq!(name, "alpha");
675        let value: i64 = features[0]
676            .property("value")
677            .ok_or("missing value")?
678            .try_into()?;
679        assert_eq!(value, 7);
680        assert_eq!(features[0].geometry()?.geometry_type(), GeometryType::Point);
681
682        assert_eq!(features[1].id(), 2);
683        let name: String = features[1]
684            .property("name")
685            .ok_or("missing name")?
686            .try_into()?;
687        assert_eq!(name, "beta");
688        let value: i64 = features[1]
689            .property("value")
690            .ok_or("missing value")?
691            .try_into()?;
692        assert_eq!(value, 9);
693        assert_eq!(features[1].geometry()?.geometry_type(), GeometryType::Point);
694
695        Ok(())
696    }
697}