Skip to main content

rusqlite_gpkg/gpkg/
attribute_table.rs

1use crate::Value;
2use crate::error::{GpkgError, Result};
3use crate::ogc_sql::{sql_delete_all, sql_insert_feature, sql_select_attribute_rows};
4use crate::types::ColumnSpec;
5use rusqlite::types::Type;
6use std::collections::HashMap;
7use std::rc::Rc;
8
9use super::attribute_row::GpkgAttributeRow;
10
11#[derive(Debug)]
12/// A GeoPackage attribute table (non-spatial, no geometry column).
13pub struct GpkgAttributeTable {
14    pub(super) conn: Rc<rusqlite::Connection>,
15    pub(super) is_read_only: bool,
16    pub table_name: String,
17    pub primary_key_column: String,
18    pub property_columns: Vec<ColumnSpec>,
19    pub(super) property_index_by_name: Rc<HashMap<String, usize>>,
20    pub(super) insert_sql: String,
21    pub(super) update_sql: String,
22}
23
24impl GpkgAttributeTable {
25    /// Return all rows in the table.
26    pub fn rows(&self) -> Result<Vec<GpkgAttributeRow>> {
27        let columns = self.property_columns.iter().map(|spec| spec.name.as_str());
28        let sql =
29            sql_select_attribute_rows(&self.table_name, &self.primary_key_column, columns, None);
30
31        let mut stmt = self.conn.prepare(&sql)?;
32        let rows = stmt
33            .query_map([], |row| {
34                row_to_attribute_row(
35                    row,
36                    &self.property_columns,
37                    &self.primary_key_column,
38                    &self.property_index_by_name,
39                )
40            })?
41            .collect::<rusqlite::Result<Vec<GpkgAttributeRow>>>()?;
42
43        Ok(rows)
44    }
45
46    /// Remove all rows from the table.
47    pub fn truncate(&self) -> Result<usize> {
48        self.ensure_writable()?;
49        let sql = sql_delete_all(&self.table_name);
50        Ok(self.conn.execute(&sql, [])?)
51    }
52
53    /// Insert a row with ordered property values (no geometry).
54    pub fn insert<'p, P>(&self, properties: P) -> Result<()>
55    where
56        P: IntoIterator<Item = &'p Value>,
57    {
58        let properties: Vec<&Value> = properties.into_iter().collect();
59        let expected = self.property_columns.len();
60        let got = properties.len();
61        if expected != got {
62            return Err(GpkgError::InvalidPropertyCount { expected, got });
63        }
64
65        self.ensure_writable()?;
66
67        let params = params_from_properties(properties, None);
68        let mut stmt = self.conn.prepare_cached(&self.insert_sql)?;
69        stmt.execute(params)?;
70        Ok(())
71    }
72
73    /// Update the row with the given primary key.
74    pub fn update<'p, P>(&self, properties: P, id: i64) -> Result<()>
75    where
76        P: IntoIterator<Item = &'p Value>,
77    {
78        let properties: Vec<&Value> = properties.into_iter().collect();
79        let expected = self.property_columns.len();
80        let got = properties.len();
81        if expected != got {
82            return Err(GpkgError::InvalidPropertyCount { expected, got });
83        }
84
85        self.ensure_writable()?;
86
87        let params = params_from_properties(properties, Some(id));
88        let mut stmt = self.conn.prepare_cached(&self.update_sql)?;
89        stmt.execute(params)?;
90        Ok(())
91    }
92
93    fn ensure_writable(&self) -> Result<()> {
94        if self.is_read_only {
95            return Err(GpkgError::ReadOnly);
96        }
97        Ok(())
98    }
99
100    pub(crate) fn build_insert_sql(table_name: &str, property_columns: &[ColumnSpec]) -> String {
101        if property_columns.is_empty() {
102            return format!(r#"INSERT INTO "{}" DEFAULT VALUES"#, table_name);
103        }
104
105        let columns: Vec<String> = property_columns
106            .iter()
107            .map(|spec| format!(r#""{}""#, spec.name))
108            .collect();
109
110        let placeholders = (1..=columns.len())
111            .map(|i| format!("?{i}"))
112            .collect::<Vec<String>>()
113            .join(",");
114
115        sql_insert_feature(table_name, &columns.join(","), &placeholders)
116    }
117
118    pub(crate) fn build_update_sql(
119        table_name: &str,
120        primary_key_column: &str,
121        property_columns: &[ColumnSpec],
122    ) -> String {
123        if property_columns.is_empty() {
124            // No columns to update; set the PK to itself as a no-op.
125            return format!(
126                r#"UPDATE "{}" SET "{}"=?1 WHERE "{}"=?1"#,
127                table_name, primary_key_column, primary_key_column
128            );
129        }
130
131        let assignments = property_columns
132            .iter()
133            .enumerate()
134            .map(|(idx, spec)| format!(r#""{}"=?{}"#, spec.name, idx + 1))
135            .collect::<Vec<String>>()
136            .join(",");
137        let id_idx = property_columns.len() + 1;
138
139        format!(
140            r#"UPDATE "{}" SET {} WHERE "{}"=?{}"#,
141            table_name, assignments, primary_key_column, id_idx
142        )
143    }
144
145    pub(crate) fn build_property_index_by_name(
146        property_columns: &[ColumnSpec],
147    ) -> HashMap<String, usize> {
148        let mut map = HashMap::with_capacity(property_columns.len());
149        for (idx, column) in property_columns.iter().enumerate() {
150            map.insert(column.name.clone(), idx);
151        }
152        map
153    }
154}
155
156const PRIMARY_INDEX: usize = 0;
157
158fn row_to_attribute_row(
159    row: &rusqlite::Row<'_>,
160    property_columns: &[ColumnSpec],
161    primary_key_column: &str,
162    property_index_by_name: &Rc<HashMap<String, usize>>,
163) -> std::result::Result<GpkgAttributeRow, rusqlite::Error> {
164    let mut id: Option<i64> = None;
165    let mut properties = Vec::with_capacity(property_columns.len());
166    let row_len = property_columns.len() + 1;
167
168    for idx in 0..row_len {
169        let value_ref = row.get_ref(idx)?;
170        let value = Value::from(value_ref);
171
172        if idx == PRIMARY_INDEX {
173            match &value {
174                Value::Integer(value) => id = Some(*value),
175                _ => {
176                    return Err(rusqlite::Error::InvalidColumnType(
177                        idx,
178                        primary_key_column.to_string(),
179                        value_ref.data_type(),
180                    ));
181                }
182            }
183        } else {
184            properties.push(value);
185        }
186    }
187
188    let id = id.ok_or_else(|| {
189        rusqlite::Error::InvalidColumnType(
190            PRIMARY_INDEX,
191            primary_key_column.to_string(),
192            Type::Null,
193        )
194    })?;
195
196    Ok(GpkgAttributeRow {
197        id,
198        properties,
199        property_index_by_name: property_index_by_name.clone(),
200    })
201}
202
203fn params_from_properties<'p, P>(properties: P, id: Option<i64>) -> impl rusqlite::Params
204where
205    P: IntoIterator<Item = &'p Value>,
206{
207    let params = properties
208        .into_iter()
209        .map(SqlParam::Borrowed)
210        .chain(id.into_iter().map(|i| SqlParam::Owned(Value::Integer(i))));
211    rusqlite::params_from_iter(params)
212}
213
214enum SqlParam<'a> {
215    Owned(Value),
216    Borrowed(&'a Value),
217}
218
219impl<'a> rusqlite::ToSql for SqlParam<'a> {
220    #[inline]
221    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
222        match self {
223            SqlParam::Owned(value) => value.to_sql(),
224            SqlParam::Borrowed(value) => value.to_sql(),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use crate::GpkgError;
232    use crate::Result;
233    use crate::gpkg::Gpkg;
234    use crate::params;
235    use crate::types::{ColumnSpec, ColumnType};
236
237    #[test]
238    fn create_and_read_attribute_table() -> Result<()> {
239        let gpkg = Gpkg::open_in_memory()?;
240        let columns = vec![
241            ColumnSpec {
242                name: "name".to_string(),
243                column_type: ColumnType::Varchar,
244            },
245            ColumnSpec {
246                name: "value".to_string(),
247                column_type: ColumnType::Integer,
248            },
249        ];
250
251        let table = gpkg.create_attribute_table("observations", &columns)?;
252        table.insert(params!["alpha", 7_i64])?;
253        table.insert(params!["beta", 9_i64])?;
254
255        let rows = table.rows()?;
256        assert_eq!(rows.len(), 2);
257
258        assert_eq!(rows[0].id(), 1);
259        let name: String = rows[0].property("name").unwrap().try_into()?;
260        assert_eq!(name, "alpha");
261        let value: i64 = rows[0].property("value").unwrap().try_into()?;
262        assert_eq!(value, 7);
263
264        assert_eq!(rows[1].id(), 2);
265        let name: String = rows[1].property("name").unwrap().try_into()?;
266        assert_eq!(name, "beta");
267        let value: i64 = rows[1].property("value").unwrap().try_into()?;
268        assert_eq!(value, 9);
269
270        Ok(())
271    }
272
273    #[test]
274    fn attribute_table_metadata_in_gpkg_contents() -> Result<()> {
275        let gpkg = Gpkg::open_in_memory()?;
276        let columns = vec![ColumnSpec {
277            name: "name".to_string(),
278            column_type: ColumnType::Varchar,
279        }];
280
281        gpkg.create_attribute_table("observations", &columns)?;
282
283        // Verify gpkg_contents has data_type = 'attributes' and NULL srs_id
284        let (data_type, srs_id): (String, Option<i32>) = gpkg.conn.query_row(
285            "SELECT data_type, srs_id FROM gpkg_contents WHERE table_name = 'observations'",
286            [],
287            |row| Ok((row.get(0)?, row.get(1)?)),
288        )?;
289        assert_eq!(data_type, "attributes");
290        assert_eq!(srs_id, None);
291
292        // Verify NO entry in gpkg_geometry_columns
293        let count: i64 = gpkg.conn.query_row(
294            "SELECT COUNT(*) FROM gpkg_geometry_columns WHERE table_name = 'observations'",
295            [],
296            |row| row.get(0),
297        )?;
298        assert_eq!(count, 0);
299
300        Ok(())
301    }
302
303    #[test]
304    fn list_attribute_tables_returns_only_attributes() -> Result<()> {
305        let gpkg = Gpkg::open_in_memory()?;
306
307        // Create a feature layer
308        gpkg.create_layer(
309            "points",
310            "geom",
311            wkb::reader::GeometryType::Point,
312            wkb::reader::Dimension::Xy,
313            4326,
314            &[],
315        )?;
316
317        // Create an attribute table
318        let columns = vec![ColumnSpec {
319            name: "name".to_string(),
320            column_type: ColumnType::Varchar,
321        }];
322        gpkg.create_attribute_table("observations", &columns)?;
323
324        // list_layers returns only feature layers
325        let layers = gpkg.list_layers()?;
326        assert_eq!(layers, vec!["points"]);
327
328        // list_attribute_tables returns only attribute tables
329        let attr_tables = gpkg.list_attribute_tables()?;
330        assert_eq!(attr_tables, vec!["observations"]);
331
332        Ok(())
333    }
334
335    #[test]
336    fn get_layer_rejects_attribute_table() -> Result<()> {
337        let gpkg = Gpkg::open_in_memory()?;
338        let columns = vec![ColumnSpec {
339            name: "name".to_string(),
340            column_type: ColumnType::Varchar,
341        }];
342        gpkg.create_attribute_table("observations", &columns)?;
343
344        let err = gpkg
345            .get_layer("observations")
346            .expect_err("should fail for attribute table");
347        assert!(matches!(err, GpkgError::NotAFeatureLayer { .. }));
348
349        Ok(())
350    }
351
352    #[test]
353    fn get_attribute_table_rejects_feature_layer() -> Result<()> {
354        let gpkg = Gpkg::open_in_memory()?;
355        gpkg.create_layer(
356            "points",
357            "geom",
358            wkb::reader::GeometryType::Point,
359            wkb::reader::Dimension::Xy,
360            4326,
361            &[],
362        )?;
363
364        let err = gpkg
365            .get_attribute_table("points")
366            .expect_err("should fail for feature layer");
367        assert!(matches!(err, GpkgError::NotAnAttributeTable { .. }));
368
369        Ok(())
370    }
371
372    #[test]
373    fn insert_and_update_attribute_row() -> Result<()> {
374        let gpkg = Gpkg::open_in_memory()?;
375        let columns = vec![
376            ColumnSpec {
377                name: "name".to_string(),
378                column_type: ColumnType::Varchar,
379            },
380            ColumnSpec {
381                name: "value".to_string(),
382                column_type: ColumnType::Integer,
383            },
384        ];
385
386        let table = gpkg.create_attribute_table("observations", &columns)?;
387        table.insert(params!["alpha", 7_i64])?;
388        let id = table.conn.last_insert_rowid();
389
390        table.update(params!["beta", 9_i64], id)?;
391
392        let (name, value): (String, i64) = table.conn.query_row(
393            "SELECT name, value FROM observations WHERE fid = ?1",
394            [id],
395            |row| Ok((row.get(0)?, row.get(1)?)),
396        )?;
397        assert_eq!(name, "beta");
398        assert_eq!(value, 9);
399
400        Ok(())
401    }
402
403    #[test]
404    fn truncate_attribute_table() -> Result<()> {
405        let gpkg = Gpkg::open_in_memory()?;
406        let columns = vec![ColumnSpec {
407            name: "name".to_string(),
408            column_type: ColumnType::Varchar,
409        }];
410
411        let table = gpkg.create_attribute_table("observations", &columns)?;
412        let a = "a".to_string();
413        let b = "b".to_string();
414        table.insert(params![a])?;
415        table.insert(params![b])?;
416
417        let deleted = table.truncate()?;
418        assert_eq!(deleted, 2);
419
420        let count: i64 = table
421            .conn
422            .query_row("SELECT COUNT(*) FROM observations", [], |row| row.get(0))?;
423        assert_eq!(count, 0);
424
425        Ok(())
426    }
427
428    #[test]
429    fn delete_attribute_table() -> Result<()> {
430        let gpkg = Gpkg::open_in_memory()?;
431        let columns = vec![ColumnSpec {
432            name: "name".to_string(),
433            column_type: ColumnType::Varchar,
434        }];
435        gpkg.create_attribute_table("observations", &columns)?;
436
437        gpkg.delete_attribute_table("observations")?;
438
439        let tables = gpkg.list_attribute_tables()?;
440        assert!(tables.is_empty());
441
442        Ok(())
443    }
444
445    #[test]
446    fn rejects_invalid_property_count() -> Result<()> {
447        let gpkg = Gpkg::open_in_memory()?;
448        let columns = vec![
449            ColumnSpec {
450                name: "a".to_string(),
451                column_type: ColumnType::Varchar,
452            },
453            ColumnSpec {
454                name: "b".to_string(),
455                column_type: ColumnType::Integer,
456            },
457        ];
458
459        let table = gpkg.create_attribute_table("test", &columns)?;
460        let only = "only".to_string();
461        let result = table.insert(params![only]);
462        match result {
463            Err(GpkgError::InvalidPropertyCount {
464                expected: 2,
465                got: 1,
466            }) => {}
467            e => panic!("expected InvalidPropertyCount error: {e:?}"),
468        }
469
470        Ok(())
471    }
472
473    #[test]
474    fn nullable_properties() -> Result<()> {
475        let gpkg = Gpkg::open_in_memory()?;
476        let columns = vec![
477            ColumnSpec {
478                name: "a".to_string(),
479                column_type: ColumnType::Double,
480            },
481            ColumnSpec {
482                name: "b".to_string(),
483                column_type: ColumnType::Integer,
484            },
485        ];
486
487        let table = gpkg.create_attribute_table("nullable_test", &columns)?;
488        table.insert(params![Some(1.0_f64), Option::<i64>::None])?;
489
490        let rows = table.rows()?;
491        assert_eq!(rows.len(), 1);
492
493        let a: Option<f64> = rows[0].property("a").unwrap().try_into()?;
494        assert_eq!(a, Some(1.0));
495
496        let b: Option<i64> = rows[0].property("b").unwrap().try_into()?;
497        assert_eq!(b, None);
498
499        Ok(())
500    }
501
502    #[test]
503    fn get_attribute_table_roundtrip() -> Result<()> {
504        let gpkg = Gpkg::open_in_memory()?;
505        let columns = vec![
506            ColumnSpec {
507                name: "name".to_string(),
508                column_type: ColumnType::Varchar,
509            },
510            ColumnSpec {
511                name: "value".to_string(),
512                column_type: ColumnType::Integer,
513            },
514        ];
515
516        let table = gpkg.create_attribute_table("observations", &columns)?;
517        table.insert(params!["alpha", 7_i64])?;
518        drop(table);
519
520        // Re-open the table via get_attribute_table
521        let table = gpkg.get_attribute_table("observations")?;
522        assert_eq!(table.table_name, "observations");
523        assert_eq!(table.property_columns.len(), 2);
524        assert_eq!(table.property_columns[0].name, "name");
525        assert_eq!(table.property_columns[1].name, "value");
526
527        let rows = table.rows()?;
528        assert_eq!(rows.len(), 1);
529        let name: String = rows[0].property("name").unwrap().try_into()?;
530        assert_eq!(name, "alpha");
531
532        Ok(())
533    }
534
535    #[test]
536    fn duplicate_name_across_features_and_attributes() -> Result<()> {
537        let gpkg = Gpkg::open_in_memory()?;
538
539        gpkg.create_layer(
540            "shared_name",
541            "geom",
542            wkb::reader::GeometryType::Point,
543            wkb::reader::Dimension::Xy,
544            4326,
545            &[],
546        )?;
547
548        let err = gpkg
549            .create_attribute_table("shared_name", &[])
550            .expect_err("duplicate name should fail");
551        assert!(matches!(err, GpkgError::LayerAlreadyExists { .. }));
552
553        Ok(())
554    }
555
556    #[test]
557    fn empty_columns_insert_and_update() -> Result<()> {
558        let gpkg = Gpkg::open_in_memory()?;
559        let table = gpkg.create_attribute_table("empty_cols", &[])?;
560
561        // Insert with no properties should succeed.
562        table.insert(std::iter::empty::<&crate::Value>())?;
563        let id = table.conn.last_insert_rowid();
564
565        // Update with no properties should succeed (no-op).
566        table.update(std::iter::empty::<&crate::Value>(), id)?;
567
568        let rows = table.rows()?;
569        assert_eq!(rows.len(), 1);
570        assert_eq!(rows[0].id(), id);
571
572        Ok(())
573    }
574
575    #[test]
576    fn rejects_geometry_column_in_attribute_table() -> Result<()> {
577        let gpkg = Gpkg::open_in_memory()?;
578        let columns = vec![ColumnSpec {
579            name: "geom".to_string(),
580            column_type: ColumnType::Geometry,
581        }];
582
583        let err = gpkg
584            .create_attribute_table("bad_table", &columns)
585            .expect_err("geometry column should be rejected");
586        assert!(matches!(
587            err,
588            GpkgError::GeometryColumnInAttributeTable { .. }
589        ));
590
591        Ok(())
592    }
593}