ogc_cql2/ds/
gpkg.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! Artifacts specific to handling geospatial data stored in GeoPackage database
6//! files.
7//!
8
9use crate::{CRS, Expression, MyError, QString, config::config, ds::DataSource, expr::E, op::Op};
10use sqlx::{AssertSqlSafe, FromRow, Pool, Sqlite, pool::PoolOptions, sqlite::SqliteConnectOptions};
11use std::{cmp::Ordering, str::FromStr};
12use tracing::info;
13use unicode_normalization::{UnicodeNormalization, char::is_combining_mark};
14
15const GPKG_APPLICATION_ID: i32 = 0x47504B47;
16const FIND_TABLE: &str = "SELECT * FROM gpkg_contents WHERE table_name = $1";
17const FIND_SRS: &str = "SELECT * FROM gpkg_spatial_ref_sys WHERE srid = $1";
18const EPSG_AUTH: &str = "EPSG";
19
20/// Name of a collation that is case-insensitive.
21const CQL2_CI: &str = "CQL2_CI";
22/// Name of a collation that is accent-insensitive.
23const CQL2_AI: &str = "CQL2_AI";
24/// Name of a collation that is both case- and accent-insensitive.
25const CQL2_CAI: &str = "CQL2_CI_AI";
26/// Name of a collation that is suitable for comparing date/time strings.
27const CQL2_BOUNDS: &str = "CQL2_BOUNDS";
28
29// structure to read back a textual PRAGMA value.
30#[derive(Debug, FromRow)]
31struct Pragma(String);
32
33// Structure to use when SQL query is returning an integer be it a row ID or a
34// numeric PRAGMA value.
35#[derive(Debug, FromRow)]
36struct RowID(i32);
37
38// Partial representation of a `gpkg_spatial_ref_sys` table row.
39#[derive(Debug, FromRow)]
40struct TSpatialRefSys {
41    organization: String,
42    organization_coordsys_id: i32,
43}
44
45// Partial representation of a GeoPackage `gpkg_contents` table row.
46#[allow(dead_code)]
47#[derive(Debug, FromRow)]
48pub(crate) struct TContents {
49    table_name: String,
50    data_type: String,
51    srs_id: Option<i32>,
52}
53
54/// GeoPackage [DataSource] binding a .gpkg database file + a layer name that
55/// maps rows to _Features_ and [Resource][1]s.
56///
57/// [1]: crate::Resource
58#[derive(Debug)]
59#[allow(dead_code)]
60pub struct GPkgDataSource {
61    layer: String,
62    pool: Pool<Sqlite>,
63    srid: Option<u32>,
64}
65
66impl DataSource for GPkgDataSource {}
67
68impl GPkgDataSource {
69    /// Constructor.
70    pub async fn from(gpkg_url: &str, layer_name: &str) -> Result<Self, MyError> {
71        // FIXME (rsn) 20251023 - allow configuring the pool from environment
72        // variables.
73
74        // closure for case-insesnitive string comparisons.
75        // let collate_ci = |a: &str, b: &str| QString::cmp_ci(a, b);
76        let collate_ci = |a: &str, b: &str| cmp_ci(a, b);
77
78        // closure for accent-insensitive string comparisons.
79        let collate_ai = |a: &str, b: &str| cmp_ai(a, b);
80
81        // closure for accent- and case-insensitive string comparisons.
82        let collate_aci = |a: &str, b: &str| cmp_aci(a, b);
83
84        // closure for comparing date + timestamp strings, incl. unbounded ones.
85        let collate_bounds = |a: &str, b: &str| cmp_bounds(a, b);
86
87        // IMPORTANT - this is UNSAFE but i have no control over how to do it
88        // differently since handling GeoPackage data sources is a no go w/o
89        // `spatialite`...
90        let pool_opts = unsafe {
91            SqliteConnectOptions::from_str(gpkg_url)?
92                .extension("mod_spatialite")
93                .collation(CQL2_CI, collate_ci)
94                .collation(CQL2_AI, collate_ai)
95                .collation(CQL2_CAI, collate_aci)
96                .collation(CQL2_BOUNDS, collate_bounds)
97        };
98
99        let pool = PoolOptions::new().connect_with(pool_opts).await?;
100        // GeoPackage SQLite DB files are expected to have 0x47504B47 (or 1196444487)
101        // as their `application_id` in the DB header.
102        let pragma = sqlx::query_as::<_, RowID>("PRAGMA application_id")
103            .fetch_one(&pool)
104            .await?;
105        let application_id = pragma.0;
106        if application_id != GPKG_APPLICATION_ID {
107            return Err(MyError::Runtime("Unexpected application_id".into()));
108        }
109
110        // ensure it passes integerity checks...
111        let pragma = sqlx::query_as::<_, Pragma>("PRAGMA integrity_check")
112            .fetch_one(&pool)
113            .await?;
114        if pragma.0 != "ok" {
115            return Err(MyError::Runtime("Failed integrity_check".into()));
116        }
117
118        // ensure it has no invalid foreign key values...
119        let fk_values: Vec<_> = sqlx::query("PRAGMA foreign_key_check")
120            .fetch_all(&pool)
121            .await?;
122        if !fk_values.is_empty() {
123            return Err(MyError::Runtime("Found invalid FK value(s)".into()));
124        }
125
126        // ensure designated layer/table exists...
127        let layer = sqlx::query_as::<_, TContents>(FIND_TABLE)
128            .bind(layer_name)
129            .fetch_one(&pool)
130            .await?;
131        // we only handle vector-based features, not tiles. check...
132        if layer.data_type != "features" {
133            return Err(MyError::Runtime("Layer is NOT vector-based".into()));
134        }
135
136        // also create a virtual table using `spatialite` _VirtualGPKG_...
137        let sql = format!(
138            r#"CREATE VIRTUAL TABLE IF NOT EXISTS "vgpkg_{0}" USING VirtualGPKG("{0}");"#,
139            layer_name
140        );
141        let safe_sql = AssertSqlSafe(sql);
142        sqlx::query(safe_sql).execute(&pool).await?;
143
144        let srid = match layer.srs_id {
145            // NOTE (rsn) 20251021 - the specs mandate the support for at least
146            // 3 values: `4326`, `-1`, and `0` w/ the last 2 to indicate an
147            // "undefined" cartesian or geographic system respectively.  ensure
148            // we can represent it as a valid CRS but only if it's not an
149            // undefined standard indicator...
150            Some(fk) => {
151                if fk < 1 {
152                    // NOTE (rsn) 20251023 - while the specs mandate the support
153                    // for a `4326` value, there's no guarantee that this is in
154                    // fact the EPSG:4326 SRS code.  what is guaranteed is that
155                    // it's a foreign key into: `gpkg_spatial_ref_sys`.
156                    let srs = sqlx::query_as::<_, TSpatialRefSys>(FIND_SRS)
157                        .bind(fk)
158                        .fetch_one(&pool)
159                        .await?;
160                    // FIXME (rsn) 20251024 - handle other authorities.
161                    let authority = srs.organization;
162                    if !authority.eq_ignore_ascii_case(EPSG_AUTH) {
163                        return Err(MyError::Runtime(
164                            format!("Unexpected ({authority}) Authority").into(),
165                        ));
166                    }
167
168                    let it = srs.organization_coordsys_id;
169                    let epsg = format!("{authority}:{fk}");
170                    // raise an error if Proj cannot handle it...
171                    let _ = CRS::new(&epsg)?;
172                    Some(u32::try_from(it)?)
173                } else {
174                    info!("GeoPackage uses an undefined ({}) SRS ID", fk);
175                    None
176                }
177            }
178            None => None,
179        };
180
181        Ok(Self {
182            layer: layer_name.to_owned(),
183            pool,
184            srid,
185        })
186    }
187
188    /// Return a reference to the connection pool.
189    pub fn pool(&self) -> &Pool<Sqlite> {
190        &self.pool
191    }
192
193    /// Return name of the virtual table created for querying this
194    /// GeoPackage table.
195    ///
196    /// This name is manufactured by pre-pending "vgpkg_" to the layer name
197    /// in a similar way to how `spatialite` handles _GeoPackage_ files.
198    pub fn vtable(&self) -> String {
199        format!("vgpkg_{}", self.layer)
200    }
201
202    /// Translate given [Expression] to an SQL _WHERE_ clause that can be used
203    /// for selecting a subset of this data source items.
204    pub fn to_sql(&self, exp: &Expression) -> Result<String, MyError> {
205        let mut e = exp.to_inner()?;
206        let it = E::reduce(&mut e)?;
207        to_sql(it)
208    }
209}
210
211/// Return the [Ordering] when comparing `a` to `b` ignoring case.
212fn cmp_ci(a: &str, b: &str) -> Ordering {
213    a.to_lowercase().cmp(&b.to_lowercase())
214}
215
216/// Return the [Ordering] when comparing `a` to `b` ignoring accents.
217fn cmp_ai(a: &str, b: &str) -> Ordering {
218    let lhs = a.nfd().filter(|x| !is_combining_mark(*x)).nfc();
219    let rhs = b.nfd().filter(|x| !is_combining_mark(*x)).nfc();
220    lhs.cmp(rhs)
221}
222
223/// Return the [Ordering] when comparing `a` to `b` ignoring both accents
224/// and case.
225fn cmp_aci(a: &str, b: &str) -> Ordering {
226    let x = a.to_lowercase();
227    let y = b.to_lowercase();
228    let lhs = x.nfd().filter(|x| !is_combining_mark(*x)).nfc();
229    let rhs = y.nfd().filter(|x| !is_combining_mark(*x)).nfc();
230    lhs.cmp(rhs)
231}
232
233/// Return the [Ordering] when comparing `a` to `b` taking into account the
234/// way CQL2 is expected to handle unbound time interval limits.
235fn cmp_bounds(a: &str, b: &str) -> Ordering {
236    let a_is_unbound = matches!(a, "..");
237    let b_is_unbound = matches!(b, "..");
238    match (a_is_unbound, b_is_unbound) {
239        (true, true) => Ordering::Equal,
240        (true, false) => Ordering::Less,
241        (false, true) => Ordering::Greater,
242        (false, false) => a.cmp(b),
243    }
244}
245
246pub(crate) fn to_sql(exp: E) -> Result<String, MyError> {
247    match exp {
248        E::Null => Ok("NULL".to_owned()),
249        E::Unbounded => Ok("'..'".to_owned()),
250        E::Bool(true) => Ok("TRUE".to_owned()),
251        E::Bool(false) => Ok("FALSE".to_owned()),
252        E::Num(x) => Ok(x.to_string()),
253        E::Str(x) => qstr_to_sql(x),
254        E::Date(x) => Ok(format!("'{}'", x.date())),
255        E::Timestamp(x) => Ok(format!("'{}'", x.datetime())),
256        E::Spatial(x) => Ok(x.to_sql()?),
257        E::Id(x) => Ok(x.to_owned()),
258        // some work need to be done when handling these options...
259        E::Monadic(op, x) if op.nullable() => {
260            let is_literal = x.is_literal_or_id();
261            let lhs = to_sql(*x)?;
262            let z_op = op.to_sql();
263            if is_literal {
264                Ok(format!("{lhs} {z_op}"))
265            } else {
266                Ok(format!("({lhs}) {z_op}"))
267            }
268        }
269        E::Monadic(op, x) => match op {
270            Op::Neg | Op::Minus => {
271                let is_literal = x.is_literal_or_id();
272                let rhs = to_sql(*x)?;
273                let z_op = op.to_sql();
274                if is_literal {
275                    Ok(format!("{z_op} {rhs}"))
276                } else {
277                    Ok(format!("{z_op} ({rhs})"))
278                }
279            }
280            Op::CaseI => match *x {
281                E::Monadic(Op::AccentI, y) => {
282                    let rhs = to_sql(*y)?;
283                    Ok(format!("{rhs} COLLATE {CQL2_CAI}"))
284                }
285                _ => {
286                    let rhs = to_sql(*x)?;
287                    Ok(format!("{rhs} COLLATE {CQL2_CI}"))
288                }
289            },
290            Op::AccentI => match *x {
291                E::Monadic(Op::CaseI, y) => {
292                    let rhs = to_sql(*y)?;
293                    Ok(format!("{rhs} COLLATE {CQL2_CAI}"))
294                }
295                _ => {
296                    let rhs = to_sql(*x)?;
297                    Ok(format!("{rhs} COLLATE {CQL2_AI}"))
298                }
299            },
300            x => unreachable!("Unexpected ({x}) monadic operator"),
301        },
302        E::Dyadic(op, a, b) if matches!(op, Op::IsBetween) || matches!(op, Op::IsNotBetween) => {
303            // RHS of [NOT] BETWEEN is an array of 2 numeric expressions...
304            match *b {
305                E::Array(rhs) => {
306                    let z_op = op.to_sql();
307                    let lhs = to_sql(*a)?;
308                    let lo = to_sql(rhs[0].to_owned())?;
309                    let hi = to_sql(rhs[1].to_owned())?;
310                    Ok(format!("{lhs} {z_op} {lo} AND {hi}"))
311                }
312                _ => unreachable!("Expetced [NOT] BETWEEN's RHS expression to be an array"),
313            }
314        }
315        E::Dyadic(op, a, b) if op.spatial() => match op {
316            Op::SWithin | Op::SOverlaps | Op::STouches => reduce_precision(op, *a, *b),
317            _ => {
318                let lhs = to_sql(*a)?;
319                let rhs = to_sql(*b)?;
320                let z_op = op.to_sql();
321                Ok(format!("{z_op}({lhs}, {rhs})"))
322            }
323        },
324        E::Dyadic(op, a, b) if op.temporal() => match op {
325            Op::TAfter => t_after_sql(*a, *b),
326            Op::TBefore => t_before_sql(*a, *b),
327            Op::TDisjoint => t_disjoint_sql(*a, *b),
328            Op::TEquals => t_equals_sql(*a, *b),
329            Op::TIntersects => t_intersects_sql(*a, *b),
330
331            Op::TContains => t_contains_sql(*a, *b),
332            Op::TDuring => t_during_sql(*a, *b),
333            Op::TFinishedBy => t_finished_by_sql(*a, *b),
334            Op::TFinishes => t_finishes_sql(*a, *b),
335            Op::TMeets => t_meets_sql(*a, *b),
336            Op::TMetBy => t_met_by_sql(*a, *b),
337            Op::TOverlappedBy => t_overlapped_by_sql(*a, *b),
338            Op::TOverlaps => t_overlaps_sql(*a, *b),
339            Op::TStartedBy => t_started_by_sql(*a, *b),
340            Op::TStarts => t_starts_sql(*a, *b),
341            x => unreachable!("Unexpected ({x:?}) operator"),
342        },
343        E::Dyadic(op, a, b) if op.array() => {
344            let z_op = op.to_sql();
345            let lhs = to_sql(*a)?;
346            let rhs = to_sql(*b)?;
347            Ok(format!("{z_op}({lhs}, {rhs})"))
348        }
349        E::Dyadic(op, a, b) if matches!(op, Op::IsLike) || matches!(op, Op::IsNotLike) => {
350            let a_is_literal = a.is_literal_or_id();
351            let lhs = to_sql(*a)?;
352            let rhs = to_sql(*b)?;
353            let z_op = op.to_sql();
354            match a_is_literal {
355                true => Ok(format!("{lhs} {z_op} ({rhs})")),
356                false => Ok(format!("({lhs}) {z_op} ({rhs})")),
357            }
358        }
359        E::Dyadic(op, a, b) => {
360            let a_is_literal = a.is_literal_or_id();
361            let b_is_literal = b.is_literal_or_id();
362            let lhs = to_sql(*a)?;
363            let rhs = to_sql(*b)?;
364            let z_op = op.to_sql();
365            match (a_is_literal, b_is_literal) {
366                (true, true) => Ok(format!("{lhs} {z_op} {rhs}")),
367                (true, false) => Ok(format!("{lhs} {z_op} ({rhs})")),
368                (false, true) => Ok(format!("({lhs}) {z_op} {rhs}")),
369                (false, false) => Ok(format!("({lhs}) {z_op} ({rhs})")),
370            }
371        }
372        E::Function(x) => {
373            let params: Result<Vec<String>, MyError> = x.params.into_iter().map(to_sql).collect();
374            let params_ = params?;
375            Ok(format!("{}({})", x.name, params_.join(", ")))
376        }
377        // NOTE (rsn) 20251105 - SQLite does not accept array elemenets w/in
378        // square brackets; only parenthesis...
379        E::Array(x) => {
380            let items: Result<Vec<String>, MyError> = x.into_iter().map(to_sql).collect();
381            let items_ = items?;
382            Ok(format!("({})", items_.join(", ")))
383        }
384        x => unreachable!("{x:?} cannot be translated to SQL"),
385    }
386}
387
388/// Generate a string that can be used in composing an SQL WHERE clause.
389fn qstr_to_sql(qs: QString) -> Result<String, MyError> {
390    match qs.flags() {
391        0 => Ok(format!("'{}'", qs.inner())),
392        1 => Ok(format!("'{}' COLLATE {CQL2_CI}", qs.inner())),
393        2 => Ok(format!("'{}' COLLATE {CQL2_AI}", qs.inner())),
394        3 => Ok(format!("'{}' COLLATE {CQL2_CAI}", qs.inner())),
395        x => {
396            let msg = format!("String w/ '{x}' flags has NO direct SQL representation");
397            Err(MyError::Runtime(msg.into()))
398        }
399    }
400}
401
402// NOTE (rsn) 20251120 - Some spatial functions (i.e. `ST_Within`, `ST_Covers`,
403// and `ST_Touches`) w/ GeoPackage data sources do NOT yield same results to
404// those obtained when directly using GEOS, when one of the arguments is a table
405// column.
406// we work around this by applying `ST_ReducePrecision` *before* calling those
407// functions. the precision value used in those instances is the same one
408// configured as the default (see DEFAULT_PRECISION in `config::config()`) which
409// we already use when outputing WKT strings...
410fn reduce_precision(op: Op, a: E, b: E) -> Result<String, MyError> {
411    let a_is_id = a.is_id();
412    let b_is_id = b.is_id();
413    let (lhs, rhs) = match (a_is_id, b_is_id) {
414        (true, false) => {
415            let lhs = reduce_precision_sql(a)?;
416            let rhs = to_sql(b)?;
417            (lhs, rhs)
418        }
419        (false, true) => {
420            let lhs = to_sql(a)?;
421            let rhs = reduce_precision_sql(b)?;
422            (lhs, rhs)
423        }
424        _ => {
425            let lhs = to_sql(a)?;
426            let rhs = to_sql(b)?;
427            (lhs, rhs)
428        }
429    };
430    let z_op = op.to_sql();
431    Ok(format!("{z_op}({lhs}, {rhs})"))
432}
433
434fn reduce_precision_sql(a: E) -> Result<String, MyError> {
435    let it = format!(
436        "ST_ReducePrecision({}, 1E-{})",
437        to_sql(a)?,
438        config().default_precision()
439    );
440    Ok(it)
441}
442
443// mixed (instant and interval) arguments...
444fn t_after_sql(a: E, b: E) -> Result<String, MyError> {
445    let (a_is_interval, b_is_interval, e0, e1, e2, e3) = crate::unfold_expressions!(a, b);
446    match (a_is_interval, b_is_interval) {
447        (false, false) => Ok(format!("{} > {}", to_sql(e0)?, to_sql(e2)?)),
448        // w/ the remaining cases, we may need additional xxx IS NOT NULL fragments...
449        (false, true) => {
450            let base = format!("{} > {} COLLATE CQL2_BOUNDS", to_sql(e0)?, to_sql(e3)?);
451            let sql = crate::check_ids!(e2, base);
452            Ok(sql)
453        }
454        (true, false) => {
455            let base = format!("{} > {} COLLATE CQL2_BOUNDS", to_sql(e0)?, to_sql(e2)?);
456            let sql = crate::check_ids!(e1, base);
457            Ok(sql)
458        }
459        (true, true) => {
460            let base = format!("{} > {} COLLATE CQL2_BOUNDS", to_sql(e0)?, to_sql(e3)?);
461            let sql = crate::check_ids!(e1, e2, base);
462            Ok(sql)
463        }
464    }
465}
466
467fn t_before_sql(a: E, b: E) -> Result<String, MyError> {
468    let (a_is_interval, b_is_interval, e0, e1, e2, e3) = crate::unfold_expressions!(a, b);
469    match (a_is_interval, b_is_interval) {
470        (false, false) => Ok(format!("{} < {}", to_sql(e0)?, to_sql(e2)?)),
471        (false, true) => {
472            let base = format!("{} < {} COLLATE CQL2_BOUNDS", to_sql(e0)?, to_sql(e2)?);
473            let sql = crate::check_ids!(e3, base);
474            Ok(sql)
475        }
476        (true, false) => {
477            let base = format!("{} < {} COLLATE CQL2_BOUNDS", to_sql(e1)?, to_sql(e2)?);
478            let sql = crate::check_ids!(e0, base);
479            Ok(sql)
480        }
481        (true, true) => {
482            let base = format!("{} < {} COLLATE CQL2_BOUNDS", to_sql(e1)?, to_sql(e2)?);
483            let sql = crate::check_ids!(e0, e3, base);
484            Ok(sql)
485        }
486    }
487}
488
489fn t_disjoint_sql(a: E, b: E) -> Result<String, MyError> {
490    let (a_is_interval, b_is_interval, e0, e1, e2, e3) = crate::unfold_expressions!(a, b);
491    match (a_is_interval, b_is_interval) {
492        (false, false) => Ok(format!("{} != {}", to_sql(e0)?, to_sql(e2)?)),
493        (false, true) => {
494            let e2_ = e2.clone();
495            let e3_ = e3.clone();
496            let s0 = to_sql(e0)?;
497            let s2 = to_sql(e2)?;
498            let s3 = to_sql(e3)?;
499            let base1 = format!("{s0} < {s2}");
500            let sql1 = crate::check_ids!(e3_, base1);
501            let base2 = format!("{s0} > {s3}");
502            let sql2 = crate::check_ids!(e2_, base2);
503            Ok(format!("({sql1}) OR ({sql2})"))
504        }
505        (true, false) => {
506            let e0_ = e0.clone();
507            let e1_ = e1.clone();
508            let s0 = to_sql(e0)?;
509            let s1 = to_sql(e1)?;
510            let s2 = to_sql(e2)?;
511            let base1 = format!("{s1} < {s2}");
512            let sql1 = crate::check_ids!(e0_, base1);
513            let base2 = format!("{s0} > {s2}");
514            let sql2 = crate::check_ids!(e1_, base2);
515            Ok(format!("({sql1}) OR ({sql2})"))
516        }
517        (true, true) => {
518            let e0_ = e0.clone();
519            let e1_ = e1.clone();
520            let e2_ = e2.clone();
521            let e3_ = e3.clone();
522            let s0 = to_sql(e0)?;
523            let s1 = to_sql(e1)?;
524            let s2 = to_sql(e2)?;
525            let s3 = to_sql(e3)?;
526            let base1 = format!("{s1} < {s2}");
527            let sql1 = crate::check_ids!(e0_, e3_, base1);
528            let base2 = format!("{s0} > {s3}");
529            let sql2 = crate::check_ids!(e1_, e2_, base2);
530            Ok(format!("({sql1}) OR ({sql2})"))
531        }
532    }
533}
534
535fn t_equals_sql(a: E, b: E) -> Result<String, MyError> {
536    let (a_is_interval, b_is_interval, e0, e1, e2, e3) = crate::unfold_expressions!(a, b);
537    match (a_is_interval, b_is_interval) {
538        (false, false) => Ok(format!("{} == {}", to_sql(e0)?, to_sql(e2)?)),
539        (false, true) => Ok(format!(
540            "({0} == {1}) AND ({0} == {2})",
541            to_sql(e0)?,
542            to_sql(e2)?,
543            to_sql(e3)?
544        )),
545        (true, false) => Ok(format!(
546            "({0} == {2}) AND ({1} == {2})",
547            to_sql(e0)?,
548            to_sql(e1)?,
549            to_sql(e2)?
550        )),
551        (true, true) => Ok(format!(
552            "({0} == {2}) AND ({1} == {3})",
553            to_sql(e0)?,
554            to_sql(e1)?,
555            to_sql(e2)?,
556            to_sql(e3)?
557        )),
558    }
559}
560
561fn t_intersects_sql(a: E, b: E) -> Result<String, MyError> {
562    let (a_is_interval, b_is_interval, e0, e1, e2, e3) = crate::unfold_expressions!(a, b);
563    match (a_is_interval, b_is_interval) {
564        (false, false) => Ok(format!("{} == {}", to_sql(e0)?, to_sql(e2)?)),
565        (false, true) => Ok(format!(
566            "NOT(({0} < {1}) OR ({0} > {2}))",
567            to_sql(e0)?,
568            to_sql(e2)?,
569            to_sql(e3)?
570        )),
571        (true, false) => Ok(format!(
572            "NOT(({1} < {2}) OR ({0} > {2}))",
573            to_sql(e0)?,
574            to_sql(e1)?,
575            to_sql(e2)?
576        )),
577        (true, true) => Ok(format!(
578            "NOT(({1} < {2}) OR ({0} > {3}))",
579            to_sql(e0)?,
580            to_sql(e1)?,
581            to_sql(e2)?,
582            to_sql(e3)?
583        )),
584    }
585}
586
587// intervals only...
588fn t_contains_sql(a: E, b: E) -> Result<String, MyError> {
589    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
590    Ok(format!(
591        "({0} < {2}) AND ({1} > {3})",
592        to_sql(e0)?,
593        to_sql(e1)?,
594        to_sql(e2)?,
595        to_sql(e3)?
596    ))
597}
598
599fn t_during_sql(a: E, b: E) -> Result<String, MyError> {
600    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
601    Ok(format!(
602        "({0} > {2}) AND ({1} < {3})",
603        to_sql(e0)?,
604        to_sql(e1)?,
605        to_sql(e2)?,
606        to_sql(e3)?
607    ))
608}
609
610fn t_finished_by_sql(a: E, b: E) -> Result<String, MyError> {
611    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
612    Ok(format!(
613        "({0} < {2}) AND ({1} == {3})",
614        to_sql(e0)?,
615        to_sql(e1)?,
616        to_sql(e2)?,
617        to_sql(e3)?
618    ))
619}
620
621fn t_finishes_sql(a: E, b: E) -> Result<String, MyError> {
622    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
623    Ok(format!(
624        "({0} > {2}) AND ({1} == {3})",
625        to_sql(e0)?,
626        to_sql(e1)?,
627        to_sql(e2)?,
628        to_sql(e3)?
629    ))
630}
631
632fn t_meets_sql(a: E, b: E) -> Result<String, MyError> {
633    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
634    let base = format!("{0} == {1}", to_sql(e1)?, to_sql(e2)?);
635    let sql = crate::check_ids!(e0, e3, base);
636    Ok(sql)
637}
638
639fn t_met_by_sql(a: E, b: E) -> Result<String, MyError> {
640    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
641    let base = format!("{0} == {1}", to_sql(e0)?, to_sql(e3)?);
642    let sql = crate::check_ids!(e1, e2, base);
643    Ok(sql)
644}
645
646fn t_overlapped_by_sql(a: E, b: E) -> Result<String, MyError> {
647    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
648    Ok(format!(
649        "({0} > {2}) AND ({0} < {3}) AND ({1} > {3})",
650        to_sql(e0)?,
651        to_sql(e1)?,
652        to_sql(e2)?,
653        to_sql(e3)?
654    ))
655}
656
657fn t_overlaps_sql(a: E, b: E) -> Result<String, MyError> {
658    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
659    Ok(format!(
660        "({0} < {2}) AND ({1} > {2}) AND ({1} < {3})",
661        to_sql(e0)?,
662        to_sql(e1)?,
663        to_sql(e2)?,
664        to_sql(e3)?
665    ))
666}
667
668fn t_started_by_sql(a: E, b: E) -> Result<String, MyError> {
669    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
670    Ok(format!(
671        "({0} == {2}) AND ({1} > {3})",
672        to_sql(e0)?,
673        to_sql(e1)?,
674        to_sql(e2)?,
675        to_sql(e3)?
676    ))
677}
678
679fn t_starts_sql(a: E, b: E) -> Result<String, MyError> {
680    let (e0, e1, e2, e3) = crate::unfold_intervals!(a, b);
681    Ok(format!(
682        "({0} == {2}) AND ({1} < {3})",
683        to_sql(e0)?,
684        to_sql(e1)?,
685        to_sql(e2)?,
686        to_sql(e3)?
687    ))
688}
689
690/// Macro to generate a concrete [GPkgDataSource].
691///
692/// Caller must provide the following parameters:
693/// * $vis: Visibility specifier of the generated artifacts.
694/// * $name: Prefix of the concrete data source structure name to materialize.
695///   The final name will have a 'GPkg' suffix appended; eg. `Foo` -> `FooGPkg`.
696/// * $gpkg_url: Database URL to an accessible GeoPackage DB; e.g.
697///   `sqlite:path/to/a/geo_package.gpkg`
698/// * $layer: Name of the table/layer containing the features' data.
699/// * $feature: `sqlx` _FromRow_ convertible structure to map database layer
700///   table rows to Features.
701#[macro_export]
702macro_rules! gen_gpkg_ds {
703    ($vis:vis, $name:expr, $gpkg_url:expr, $layer:expr, $feature:expr) => {
704        paste::paste! {
705            /// Concrete GeoPackage source.
706            $vis struct [<$name GPkg>](GPkgDataSource);
707
708            impl [<$name GPkg>] {
709                /// Constructor.
710                $vis async fn new() -> Result<Self, MyError> {
711                    let gpkp = GPkgDataSource::from($gpkg_url, $layer).await?;
712                    Ok(Self(gpkp))
713                }
714
715                /// Convert a GeoPackage row (aka Feature) to a generic Resource.
716                $vis fn to_resource(r: $feature) -> Result<Resource, Box<dyn Error>> {
717                    let row = $feature::try_from(r)?;
718                    Ok(Resource::try_from(row)?)
719                }
720
721                /// Convenience method. Calls inner's samilarly named method.
722                $vis fn vtable(&self) -> String {
723                    self.0.vtable()
724                }
725            }
726
727            impl ::core::fmt::Display for [<$name GPkg>] {
728                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
729                    write!(f, "{}GPkg({})", $name, $layer)
730                }
731            }
732
733            #[::async_trait::async_trait]
734            impl StreamableDS for [<$name GPkg>] {
735                type Item = $feature;
736                type Err = MyError;
737
738                async fn fetch(
739                    &self
740                ) -> Result<::futures::stream::BoxStream<'_, Result<$feature, MyError>>, MyError> {
741                    let sql = format!("SELECT * FROM {}", $layer);
742                    let safe_sql = AssertSqlSafe(sql);
743                    let it = sqlx::query_as::<_, $feature>(safe_sql)
744                        .fetch(self.0.pool())
745                        .map_err(MyError::SQL);
746                    Ok(Box::pin(it))
747                }
748
749                async fn stream(
750                    &self
751                ) -> Result<::futures::stream::BoxStream<'_, Result<Resource, MyError>>, MyError> {
752                    let rows = self.fetch().await?;
753                    let resources = rows
754                        .try_filter_map(|row| async move {
755                            match Resource::try_from(row) {
756                                Ok(x) => Ok(Some(x)),
757                                Err(x) => Err(x),
758                            }
759                        })
760                        .boxed();
761                    Ok(resources)
762                }
763
764                async fn fetch_where(
765                    &self,
766                    exp: &Expression,
767                ) -> Result<::futures::stream::BoxStream<'_, Result<$feature, MyError>>, MyError> {
768                    let where_clause = self.0.to_sql(exp)?;
769                    // let sql = format!("SELECT * FROM {} WHERE {where_clause}", $layer);
770                    let sql = format!(r#"SELECT * FROM "{}" WHERE {}"#, self.vtable(), where_clause);
771                    let safe_sql = AssertSqlSafe(sql);
772                    let it = sqlx::query_as::<_, $feature>(safe_sql)
773                        .fetch(self.0.pool())
774                        .map_err(MyError::SQL);
775                    Ok(Box::pin(it))
776                }
777
778                async fn stream_where(
779                    &self,
780                    exp: &Expression,
781                ) -> Result<::futures::stream::BoxStream<'_, Result<Resource, MyError>>, MyError> {
782                    let rows = self.fetch_where(exp).await?;
783                    let resources = rows
784                        .try_filter_map(|row| async move {
785                            match Resource::try_from(row) {
786                                Ok(x) => Ok(Some(x)),
787                                Err(x) => Err(x),
788                            }
789                        })
790                        .boxed();
791                    Ok(resources)
792                }
793            }
794        }
795    };
796}
797
798/// Given two _Expressions_ `$a` and `$b`, check whether they're _Intervals_ or
799/// not and compute a tuple that represents the result along w/ four expressions
800/// representing the entities to use in formulating comparison predicates that
801/// will reflect a desired CQL2 date/time function.
802#[macro_export]
803macro_rules! unfold_expressions {
804    ( $a: expr, $b: expr ) => {{
805        let a_is_interval = $a.is_interval();
806        let b_is_interval = $b.is_interval();
807        match (a_is_interval, b_is_interval) {
808            (false, false) => (false, false, $a, E::Null, $b, E::Null),
809            (false, true) => {
810                let t2 = $b.as_interval().expect("2nd argument is NOT an interval");
811                (false, true, $a, E::Null, t2.0, t2.1)
812            }
813            (true, false) => {
814                let t1 = $a.as_interval().expect("1st argument is NOT an interval");
815                (true, false, t1.0, t1.1, $b, E::Null)
816            }
817            (true, true) => {
818                let t1 = $a.as_interval().expect("1st argument is NOT an interval");
819                let t2 = $b.as_interval().expect("2nd argument is NOT an interval");
820                (true, true, t1.0, t1.1, t2.0, t2.1)
821            }
822        }
823    }};
824}
825
826/// Augment a given `$sql` fragment by appending a `<x> NOT NULL` fragment(s)
827/// if either or both `$a` and `$b` are _Identifiers_.
828#[macro_export]
829macro_rules! check_ids {
830    ( $a: expr, $sql: expr ) => {{
831        if $a.is_id() {
832            let id = $a.as_id().expect("Argument is not an Identifier");
833            format!("\"{}\" NOT NULL AND ({})", id, $sql)
834        } else {
835            $sql
836        }
837    }};
838
839    ( $a: expr, $b: expr, $sql: expr ) => {{
840        match ($a.is_id(), $b.is_id()) {
841            (true, true) => {
842                let id1 = $a.as_id().expect("1st argument is not an Identifier");
843                let id2 = $b.as_id().expect("2nd argument is not an Identifier");
844                format!(
845                    "\"{}\" NOT NULL AND \"{}\" NOT NULL AND ({})",
846                    id1, id2, $sql
847                )
848            }
849            (true, false) => {
850                let id = $a.as_id().expect("1st argument is not an Identifier");
851                format!("\"{}\" NOT NULL AND ({})", id, $sql)
852            }
853            (false, true) => {
854                let id = $b.as_id().expect("2nd argument is not an Identifier");
855                format!("\"{}\" NOT NULL AND ({})", id, $sql)
856            }
857            (false, false) => $sql,
858        }
859    }};
860}
861
862/// Similar to `unfold_expressions!` except that it always expects the arguments
863/// to be _Intervals_.
864#[macro_export]
865macro_rules! unfold_intervals {
866    ( $a: expr, $b: expr ) => {{
867        let t1 = $a.as_interval().expect("1st argument is NOT an interval");
868        let t2 = $b.as_interval().expect("2nd argument is NOT an interval");
869        (t1.0, t1.1, t2.0, t2.1)
870    }};
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876
877    #[test]
878    fn test_cmp_ci() {
879        let eq = cmp_ci("abc", "ABC");
880        assert_eq!(eq, Ordering::Equal);
881
882        let eq = cmp_ci("ABC", "abc");
883        assert_eq!(eq, Ordering::Equal);
884
885        let eq = cmp_ci("aBc", "AbC");
886        assert_eq!(eq, Ordering::Equal);
887
888        let eq = cmp_ci("abcd", "ABCe");
889        assert_eq!(eq, Ordering::Less);
890
891        let eq = cmp_ci("bcd", "ACz");
892        assert_eq!(eq, Ordering::Greater);
893    }
894
895    #[test]
896    fn test_cmp_ai() {
897        let eq = cmp_ai("ÁBC", "ABC");
898        assert_eq!(eq, Ordering::Equal);
899
900        let eq = cmp_ai("ÁBC", "ABÇ");
901        assert_eq!(eq, Ordering::Equal);
902    }
903
904    #[test]
905    fn test_cmp_aci() {
906        let eq = cmp_aci("ábc", "ABÇ");
907        assert_eq!(eq, Ordering::Equal);
908    }
909}