geopackage-core 0.7.0

No-IO core for the OGC GeoPackage format: GeoPackage Binary (GPB) codec, table DDL, RTree trigger definitions, and validation rules
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Registered extension names (Annex F) and the `scope` values a
//! `gpkg_extensions` row may carry.
//!
//! This is the naming half of the extension mechanism (spec clause 2.3.2,
//! `spec/core/2e_extensions-mechanism.adoc`). Reading a file's catalogue, and
//! deciding what this workspace does about a row it finds there, belong to the
//! `geopackage` crate's `extensions` module.
//!
//! An extension name is `<author>_<extension name>`, case sensitive, with
//! `gpkg` reserved for extensions OGC maintains (Requirement 62). Two names
//! here have other authors: `gdal_aspatial`, which predates the attributes
//! data type, and `related_tables`, which OGC 18-000 registered without an
//! author prefix at all.
//!
//! Annex F numbers the extensions in the order the annex includes them, so the
//! two extensions removed in 2016 still occupy F.2, F.4 and F.5 and everything
//! after them is numbered around the gaps.

use std::fmt;

use crate::types::GeometryType;

/// `gpkg_extensions.scope`: what an extension affects (Requirement 64).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtensionScope {
    /// `read-write`: the extension affects both readers and writers.
    ReadWrite,
    /// `write-only`: the extension affects only writers.
    ///
    /// The spec's example is an extension defining a trigger that calls a
    /// non-standard SQL function: triggers fire only on write, so read-only
    /// access can ignore it safely.
    WriteOnly,
    /// A value Requirement 64 does not allow, kept as written.
    Other(String),
}

impl ExtensionScope {
    /// Classify a `scope` column value.
    ///
    /// Requirement 64 fixes the two valid values as lowercase, and anything
    /// else becomes [`ExtensionScope::Other`] rather than an error: this is a
    /// value read from a file someone else wrote.
    pub fn parse(value: &str) -> Self {
        match value {
            "read-write" => Self::ReadWrite,
            "write-only" => Self::WriteOnly,
            other => Self::Other(other.to_owned()),
        }
    }

    /// The `scope` column value.
    pub fn as_str(&self) -> &str {
        match self {
            Self::ReadWrite => "read-write",
            Self::WriteOnly => "write-only",
            Self::Other(value) => value,
        }
    }

    /// Whether a reader has to understand the extension to read the affected
    /// data correctly.
    ///
    /// True for an unrecognised scope value: a value the spec does not define
    /// says nothing about what can be ignored.
    pub fn affects_readers(&self) -> bool {
        !matches!(self, Self::WriteOnly)
    }

    /// Whether a writer has to understand the extension to write the affected
    /// data correctly. True for every scope, including an unrecognised one.
    pub fn affects_writers(&self) -> bool {
        true
    }
}

/// An extension name this workspace can identify.
///
/// Names come from Annex F, from the two extensions the GeoPackage SWG voted
/// to remove on 2016-08-15 (still numbered in the annex, and still present in
/// files written before then), and from `gdal_aspatial`, which is not an OGC
/// extension but is common enough in older GDAL output to be worth naming.
///
/// This is the interpretation of a name, not the name itself: several
/// extensions have been registered under more than one spelling over the
/// years, and all of an extension's spellings map to one variant here.
/// [`Extension::name`] returns the current spelling, so a name read from a
/// file and passed back through this type is normalised rather than preserved.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Extension {
    /// `gpkg_geom_<TYPE>` (Annex F.1): a geometry type beyond the core seven,
    /// registered against the geometry column that holds it.
    GeometryType(GeometryType),
    /// `gpkg_rtree_index` (Annex F.3): the RTree spatial index.
    RtreeIndex,
    /// `gpkg_geometry_type_trigger` (Annex F.4).
    ///
    /// Removed from the standard on 2016-08-15 by SWG vote, over
    /// interoperability concerns. Files written before then still carry it.
    GeometryTypeTrigger,
    /// `gpkg_srs_id_trigger` (Annex F.5). Removed on 2016-08-15, as
    /// [`Extension::GeometryTypeTrigger`] was.
    SrsIdTrigger,
    /// `gpkg_zoom_other` (Annex F.6): zoom levels that do not step by factors
    /// of two.
    ZoomOther,
    /// `gpkg_webp` (Annex F.7): WebP tile payloads.
    Webp,
    /// `gpkg_metadata` (Annex F.8): the `gpkg_metadata` and
    /// `gpkg_metadata_reference` tables.
    Metadata,
    /// `gpkg_schema` (Annex F.9): the `gpkg_data_columns` and
    /// `gpkg_data_column_constraints` tables.
    Schema,
    /// `gpkg_crs_wkt` (Annex F.10): the `definition_12_063` column on
    /// `gpkg_spatial_ref_sys`, holding a WKT2 CRS definition.
    CrsWkt,
    /// `gpkg_crs_wkt_1_1`: version 1.1 of Annex F.10, which adds the `epoch`
    /// column beside `definition_12_063`.
    ///
    /// A file conforming to 1.1 registers both this and [`Extension::CrsWkt`].
    CrsWkt11,
    /// `gpkg_2d_gridded_coverage` (Annex F.11, published separately as OGC
    /// 17-066r1): tile payloads holding gridded values rather than pictures.
    ///
    /// Also matches the two earlier spellings, `gpkg_elevation_tiles` from
    /// before GeoPackage 1.2 and `2d_gridded_coverage` from before 17-066r1
    /// was final.
    GriddedCoverage,
    /// `related_tables` (Annex F.12, published separately as OGC 18-000):
    /// `gpkgext_relations` and the user-defined mapping tables it describes.
    ///
    /// Also matches the `gpkg_related_tables` spelling, which OGC 18-000 uses
    /// in places and which GDAL both reads and writes.
    RelatedTables,
    /// `gdal_aspatial`: GDAL's pre-1.2 convention for a table with no
    /// geometry, superseded by the `attributes` data type.
    ///
    /// Not an OGC extension: the author prefix is `gdal`.
    GdalAspatial,
    /// A name this workspace does not recognise, kept as written.
    Other(String),
}

/// The prefix of an Annex F.1 geometry type extension name.
const GEOM_PREFIX: &str = "gpkg_geom_";

/// The `definition` column value for an Annex F.1 `gpkg_geom_<TYPE>` row.
pub const GEOM_TYPE_EXTENSION_DEFINITION: &str =
    "http://www.geopackage.org/spec140/#extension_geometry_types";

impl Extension {
    /// Identify an `extension_name` column value.
    ///
    /// Names are case sensitive per Requirement 62, and are matched that way,
    /// with one exception: the geometry type in a `gpkg_geom_<TYPE>` name is
    /// parsed the way [`GeometryType::parse`] parses one from
    /// `gpkg_geometry_columns`, which tolerates the spellings that turn up in
    /// the wild. A `gpkg_geom_` name whose type is not one this crate knows,
    /// or is not an extension type, is [`Extension::Other`]: naming a type we
    /// cannot parse would claim an understanding we do not have.
    pub fn from_name(name: &str) -> Self {
        if let Some(geometry_type) = name.strip_prefix(GEOM_PREFIX) {
            return match GeometryType::parse(geometry_type) {
                Some(parsed) if parsed.is_extension() => Self::GeometryType(parsed),
                _ => Self::Other(name.to_owned()),
            };
        }
        match name {
            crate::triggers::EXTENSION_NAME => Self::RtreeIndex,
            "gpkg_geometry_type_trigger" => Self::GeometryTypeTrigger,
            "gpkg_srs_id_trigger" => Self::SrsIdTrigger,
            crate::tiles::ZOOM_OTHER_EXTENSION_NAME => Self::ZoomOther,
            crate::tiles::WEBP_EXTENSION_NAME => Self::Webp,
            "gpkg_metadata" => Self::Metadata,
            "gpkg_schema" => Self::Schema,
            "gpkg_crs_wkt" => Self::CrsWkt,
            "gpkg_crs_wkt_1_1" => Self::CrsWkt11,
            "gpkg_2d_gridded_coverage" | "2d_gridded_coverage" | "gpkg_elevation_tiles" => {
                Self::GriddedCoverage
            }
            "related_tables" | "gpkg_related_tables" => Self::RelatedTables,
            "gdal_aspatial" => Self::GdalAspatial,
            other => Self::Other(other.to_owned()),
        }
    }

    /// The current `extension_name` spelling.
    ///
    /// For an extension registered under more than one name over the years
    /// this is the newest spelling, which is not necessarily the one the file
    /// being read carries.
    pub fn name(&self) -> String {
        match self {
            Self::GeometryType(geometry_type) => {
                format!("{GEOM_PREFIX}{}", geometry_type.as_str())
            }
            Self::RtreeIndex => crate::triggers::EXTENSION_NAME.to_owned(),
            Self::GeometryTypeTrigger => "gpkg_geometry_type_trigger".to_owned(),
            Self::SrsIdTrigger => "gpkg_srs_id_trigger".to_owned(),
            Self::ZoomOther => crate::tiles::ZOOM_OTHER_EXTENSION_NAME.to_owned(),
            Self::Webp => crate::tiles::WEBP_EXTENSION_NAME.to_owned(),
            Self::Metadata => "gpkg_metadata".to_owned(),
            Self::Schema => "gpkg_schema".to_owned(),
            Self::CrsWkt => "gpkg_crs_wkt".to_owned(),
            Self::CrsWkt11 => "gpkg_crs_wkt_1_1".to_owned(),
            Self::GriddedCoverage => "gpkg_2d_gridded_coverage".to_owned(),
            Self::RelatedTables => crate::related::EXTENSION_NAME.to_owned(),
            Self::GdalAspatial => "gdal_aspatial".to_owned(),
            Self::Other(name) => name.clone(),
        }
    }

    /// What this workspace can do with the extension.
    ///
    /// This lives beside the names rather than in the `geopackage` crate so
    /// that the match is exhaustive: adding a variant above stops compiling
    /// until its support level is stated, where a wildcard in another crate
    /// would silently classify it as [`ExtensionSupport::Unrecognised`].
    pub fn support(&self) -> ExtensionSupport {
        match self {
            // A non-linear geometry column is read and written: the blobs are
            // encoded with the extended flag, their envelopes are computed from
            // the WKB, and they index. What a caller cannot do is get one back
            // as a geometry object, because `geo-traits` has no arc to give.
            // That is a limit of the Rust geometry model rather than of this
            // extension's support, and `Known` would say the data is left
            // untouched, which is no longer true of it.
            Self::RtreeIndex
            | Self::ZoomOther
            | Self::Webp
            | Self::CrsWkt
            | Self::CrsWkt11
            | Self::GeometryType(_)
            | Self::Metadata
            | Self::Schema
            | Self::RelatedTables => ExtensionSupport::Implemented,
            Self::GriddedCoverage | Self::GdalAspatial => ExtensionSupport::Known,
            Self::GeometryTypeTrigger | Self::SrsIdTrigger => ExtensionSupport::Removed,
            Self::Other(_) => ExtensionSupport::Unrecognised,
        }
    }

    /// Whether OGC removed this extension from the standard.
    ///
    /// Both removals happened on 2016-08-15, in the same SWG vote, over
    /// interoperability concerns. A file may still carry them, and this
    /// workspace reads such a file, but never writes either.
    pub fn is_removed(&self) -> bool {
        self.support() == ExtensionSupport::Removed
    }
}

/// What this workspace can do with a registered extension.
///
/// This describes the implementation, not the extension: an extension is
/// [`ExtensionSupport::Known`] when we can say what it is and which tables it
/// owns, which is enough to leave it alone safely, and
/// [`ExtensionSupport::Implemented`] only when we read and write it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExtensionSupport {
    /// Read and written by this workspace.
    Implemented,
    /// Identified, but not read or written.
    ///
    /// The extension's own tables are left untouched, and writing to an
    /// ordinary feature or tile table in the same file is unaffected, because
    /// what these extensions add sits beside the feature data rather than
    /// inside it.
    Known,
    /// Removed from the standard by the SWG vote of 2016-08-15.
    ///
    /// Tolerated on read, never written.
    Removed,
    /// Not recognised.
    ///
    /// Nothing can be assumed about what such an extension requires of a
    /// writer, which is what separates it from [`ExtensionSupport::Known`].
    Unrecognised,
}

impl ExtensionSupport {
    /// The support level as a short phrase, for reporting a catalogue row.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Implemented => "implemented",
            Self::Known => "known, not read or written",
            Self::Removed => "removed from the standard in 2016",
            Self::Unrecognised => "unrecognised",
        }
    }
}

impl fmt::Display for ExtensionSupport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl fmt::Display for ExtensionScope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn names_round_trip() {
        for extension in [
            Extension::GeometryType(GeometryType::CircularString),
            Extension::RtreeIndex,
            Extension::GeometryTypeTrigger,
            Extension::SrsIdTrigger,
            Extension::ZoomOther,
            Extension::Webp,
            Extension::Metadata,
            Extension::Schema,
            Extension::CrsWkt,
            Extension::CrsWkt11,
            Extension::GriddedCoverage,
            Extension::RelatedTables,
            Extension::GdalAspatial,
            Extension::Other("acme_something".to_owned()),
        ] {
            assert_eq!(Extension::from_name(&extension.name()), extension);
        }
    }

    #[test]
    fn historical_spellings_map_to_the_current_extension() {
        for name in [
            "gpkg_elevation_tiles",
            "2d_gridded_coverage",
            "gpkg_2d_gridded_coverage",
        ] {
            assert_eq!(
                Extension::from_name(name),
                Extension::GriddedCoverage,
                "{name}"
            );
        }
        for name in ["related_tables", "gpkg_related_tables"] {
            assert_eq!(
                Extension::from_name(name),
                Extension::RelatedTables,
                "{name}"
            );
        }
    }

    #[test]
    fn geometry_type_names_need_an_extension_type() {
        assert_eq!(
            Extension::from_name("gpkg_geom_CIRCULARSTRING"),
            Extension::GeometryType(GeometryType::CircularString)
        );
        // A core type needs no extension, so this name is meaningless rather
        // than an Annex F.1 registration.
        assert_eq!(
            Extension::from_name("gpkg_geom_POINT"),
            Extension::Other("gpkg_geom_POINT".to_owned())
        );
        // TIN and POLYHEDRALSURFACE are Annex G types this crate cannot name,
        // so their registrations stay unrecognised rather than half-understood.
        assert_eq!(
            Extension::from_name("gpkg_geom_TIN"),
            Extension::Other("gpkg_geom_TIN".to_owned())
        );
    }

    #[test]
    fn scope_values_outside_requirement_64_are_kept_as_written() {
        assert_eq!(
            ExtensionScope::parse("read-write"),
            ExtensionScope::ReadWrite
        );
        assert_eq!(
            ExtensionScope::parse("write-only"),
            ExtensionScope::WriteOnly
        );
        // Requirement 64 asks for lowercase, so this is not "read-write".
        let shouty = ExtensionScope::parse("Read-Write");
        assert_eq!(shouty, ExtensionScope::Other("Read-Write".to_owned()));
        assert_eq!(shouty.as_str(), "Read-Write");
        assert!(
            shouty.affects_readers(),
            "an undefined scope excuses nothing"
        );
        assert!(shouty.affects_writers());
    }

    #[test]
    fn write_only_is_the_only_scope_a_reader_can_ignore() {
        assert!(!ExtensionScope::WriteOnly.affects_readers());
        assert!(ExtensionScope::WriteOnly.affects_writers());
        assert!(ExtensionScope::ReadWrite.affects_readers());
        assert!(ExtensionScope::ReadWrite.affects_writers());
    }
}