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)]
12pub 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 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 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 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 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 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 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 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 gpkg.create_layer(
309 "points",
310 "geom",
311 wkb::reader::GeometryType::Point,
312 wkb::reader::Dimension::Xy,
313 4326,
314 &[],
315 )?;
316
317 let columns = vec![ColumnSpec {
319 name: "name".to_string(),
320 column_type: ColumnType::Varchar,
321 }];
322 gpkg.create_attribute_table("observations", &columns)?;
323
324 let layers = gpkg.list_layers()?;
326 assert_eq!(layers, vec!["points"]);
327
328 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 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 table.insert(std::iter::empty::<&crate::Value>())?;
563 let id = table.conn.last_insert_rowid();
564
565 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}