Skip to main content

ogc_cql2/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! OGC CQL2 parser and runtime interpreter.
6//!
7//! The next paragraphs explain in more details the elements of this project
8//! as well as the rationale behind some of the decisions that shaped its
9//! components.
10//!
11//! # Expressions
12//!
13//! The kernel of this project is OGC CQL2 Expressions represented by the
14//! [`Expression`] enumeration. The two variants: [`TextEncoded`] and [`JsonEncoded`]
15//! respectively represent the text-based and json-based mandated representations.
16//!
17//! Parsing user-provided input is done by invoking one of the following two
18//! methods: [`Expression::try_from_text()`] and [`Expression::try_from_json()`]
19//! as shown in the following example:
20//! ```rust
21//! use ogc_cql2::prelude::*;
22//! use std::error::Error;
23//!
24//! # fn test() -> Result<(), Box<dyn Error>> {
25//! let expr = Expression::try_from_text(r#""name" NOT LIKE 'foo%' AND "value" > 10"#)?;
26//! // ...
27//! let expr = Expression::try_from_json(r#"
28//! {
29//!  "op": "t_finishes",
30//!  "args": [
31//!    { "interval": [ { "property": "starts_at" }, { "property": "ends_at" } ] },
32//!    { "interval": [ "1991-10-07", "2010-02-10T05:29:20.073225Z" ] }
33//!  ]
34//! }"#)?;
35//! #    Ok(())
36//! # }
37//! ```
38//! An `Ok` result implies a syntactically correct parsed expression!
39//!
40//! For convenience, a standalone tool is included that can be used from the
41//! command line to quickly test the vailidity of candidate expressions.
42//!
43//! Once the library is built (`cargo b↵`), it can be invoked by calling:
44//! ```bash
45//! cargo r --bin repl↵
46//! ```
47//! Read more about it [here](https://github.com/raif-s-naffah/ogc-cql2/blob/master/doc/REPL.md)
48//!
49//! # Evaluators
50//!
51//! An OGC CQL2 _Expression_ on its own is close to useless unless it is evaluated
52//! against, what the (CQL2) standard refers to as [`Resource`]s. A [`Resource`]
53//! here is essentially a _Map_ of property names (i.e. strings) to [queryable][Q]
54//! values. More on that later.
55//!
56//! This library filters those [Resources][Resource] with objects that implement the
57//! [`Evaluator`] trait. A simple example of an implementation of this trait is provided
58//! --see [`ExEvaluator`].
59//!
60//! In an earlier incarnation an [`Evaluator`] used to have a `teardown()` hook.
61//! Not anymore. Rust's [`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html)
62//! makes that method superfluous.
63//!
64//! # Data sources
65//!
66//! Data sources represent providers of data to be processed by [`Evaluator`]s to
67//! filter (i.e. include or exclude) resources based on the result of [`Expression`]s.
68//!
69//! The [`DataSource`] (marker) trait represents those objects. Currently the
70//! library provides three implementations: `CSVDataSource`, `GPkgDataSource`,
71//! and `PGDataSource`, gated behind the following corresponding _Features_: `csv_ds`,
72//! `gpkg_ds`, and `pg_ds`.  The first represents _Comma Separated Values_ (CSV) sourced
73//! from tabular data where each row is mapped to a _Feature_ containing one geometry
74//! (spatial) property and other non-geometry attributes. The second represents
75//! [GeoPackage][gpkg] files. A _GeoPackage_ is
76//! > ... _an open, standards-based, platform-independent, portable,
77//! > self-describing, cocurrentlympact format for transferring geospatial information.
78//! > It is a platform-independent SQLite database file_...
79//!
80//! The third `PGDataSource` allows processing [_PostGIS_](https://postgis.net/) enabled
81//! [_PostgreSQL_](https://www.postgresql.org/) tables.
82//!
83//! Coding concrete implementations of those data source traits is facilitated
84//! by the library macros: `gen_csv_ds!`, `gen_gpkg_ds!`, and `gen_pg_ds! for the three
85//! supported [`DataSource`] implementations.
86//!
87//! # GIS Features and Resources
88//!
89//! I frequently mention the term _GIS Feature_ in the documentation to refer
90//! to an abstract GIS type that closely relates to its data source, as opposed
91//! to a Rust _Conditional Compilation Feature_, or simply _Feature_. For a CSV
92//! data source, a _GIS Feature_ is a structure that is [`serde`](https://serde.rs/)
93//! deserializable.
94//! For example, in the `tests/samples/data` folder, a CSV file named
95//! `ne_110m_rivers_lake_centerlines` representing one of the 3 data sets
96//! referred to in the standard for testing compliance is provided. The _GIS
97//! Feature_ for that data source looks like this:
98//! ```rust
99//! use serde::Deserialize;
100//! use std::marker::PhantomData;
101//!
102//! #[cfg(feature = "csv_ds")]
103//! #[derive(Debug, Default, Deserialize)]
104//! pub(crate) struct ZRiver {
105//!     /* 0 */ fid: i32,
106//!     /* 1 */ geom: String,
107//!     /* 2 */ name: String,
108//!     #[serde(skip)] ignored: PhantomData<String>
109//! }
110//! ```
111//! This makes sense b/c the [csv crate](https://crates.io/crates/csv) used for
112//! reading the _CSV_ data works smoothly with deserializable structures.
113//! Worth noting here that the spatial data (the `geom` field) is expected to
114//! be encoded as [WKT (Well Known Text)](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry).
115//!
116//! When dealing w/ a _GeoPackage_ version of the same data, this structure is used:
117//! ```rust
118//! #[cfg(feature = "gpkg_ds")]
119//! #[derive(Debug, sqlx::FromRow)]
120//! pub(crate) struct TRiver {
121//!     fid: i32,
122//!     geom: Vec<u8>,
123//!     name: String,
124//! }
125//! ```
126//! As one can see this best suits the [sqlx crate](https://crates.io/crates/sqlx)
127//! used for reading _GeoPackage_ data. In this type of _Feature_ the same
128//! `geom` spatial attribute is now expected to be a byte array containing the
129//! WKB (Well Known Binary) encoded value of the vector geometry.
130//!
131//! Similarly, when dealing w/ a _PostGIS_ version of the same data, this structure
132//! is used:
133//!
134//! ```rust
135//! use ogc_cql2::prelude::*;
136//!
137//! #[cfg(feature = "pg_ds")]
138//! #[derive(Debug, sqlx::FromRow)]
139//! pub(crate) struct LRiver {
140//!     fid: i32,
141//!     name: String,
142//!     geom: G,
143//! }
144//!
145//! ```
146//!
147//! Finally on that note, a _GIS Feature_ implementation must provide a way of
148//! converting an instance of `Self` to a [`Resource`]. Here it is for the
149//! above _rivers_ CSV version, when feature `csv_ds` is activated:
150//!
151//! ```rust
152//! # use ogc_cql2::prelude::*;
153//! # use std::{collections::HashMap, error::Error, marker::PhantomData};
154//! # use serde::Deserialize;
155//! #
156//! # #[derive(Debug, Default, Deserialize)]
157//! # struct ZRiver {
158//! #    /* 0 */ fid: i32,
159//! #    /* 1 */ geom: String,
160//! #    /* 2 */ name: String,
161//! #    #[serde(skip)] ignored: PhantomData<String>
162//! # }
163//!
164//! #[cfg(feature = "csv_ds")]
165//! impl TryFrom<ZRiver> for Resource {
166//!     type Error = MyError;
167//!
168//!     fn try_from(value: ZRiver) -> Result<Self, Self::Error> {
169//!         use ogc_cql2::prelude::*;
170//!
171//!         Ok(HashMap::from([
172//!             ("fid".into(), Q::try_from(value.fid)?),
173//!             ("geom".into(), Q::try_from_wkt(&value.geom)?),
174//!             ("name".into(), Q::new_plain_str(&value.name)),
175//!         ]))
176//!     }
177//! }
178//! ```
179//!
180//! A [`Resource`] on the other hand, as mentioned earlier, is generic in the
181//! sense that it's a simple map of propery names to values in a similar vain
182//! to how JSON objects are handled. In the same vain as how `serde` models
183//! JSON values, the types of _value_ a _resource's_ queryable, property, or
184//! attribute are embodied by the [Queryable][Q] enumeration.
185//!
186//! Note though that this _resource_ genericity is too expensive in terms of
187//! performance.
188//!
189//! # Iterable and Streamable
190//!
191//! Access to the contents of a [`DataSource`] is possible by implementing
192//! one or both of the two traits: [`IterableDS`] and [`StreamableDS`].
193//!
194//! The first exposes a method ([`iter()`][IterableDS::iter()]) that returns an
195//! [_Iterator_](https://doc.rust-lang.org/std/iter/trait.Iterator.html) over
196//! the _Features_ of the data source.
197//!
198//! Considering that the `CSVDataSource` related macro `gen_csv_ds!` does
199//! exactly that, one can easily write something like this...
200//!
201//! ```rust
202//! use ogc_cql2::prelude::*;
203//! use std::error::Error;
204//! # use std::fs::File;
205//! # use std::collections::HashMap;
206//! # use serde::Deserialize;
207//! # use std::marker::PhantomData;
208//! # #[derive(Debug, Default, Deserialize)]
209//! # struct ZRiver {
210//! #    /* 0 */ fid: i32,
211//! #    /* 1 */ geom: String,
212//! #    /* 2 */ name: String,
213//! #    #[serde(skip)] ignored: PhantomData<String>
214//! # }
215//! # impl TryFrom<ZRiver> for Resource {
216//! #    type Error = MyError;
217//! #
218//! #    fn try_from(value: ZRiver) -> Result<Self, Self::Error> {
219//! #        Ok(HashMap::from([
220//! #            ("fid".into(), Q::try_from(value.fid)?),
221//! #            ("geom".into(), Q::try_from_wkt(&value.geom)?),
222//! #            ("name".into(), Q::new_plain_str(&value.name)),
223//! #        ]))
224//! #    }
225//! # }
226//!
227//! # #[cfg(feature = "csv_ds")]
228//! # fn test() -> Result<(), Box<dyn Error>> {
229//! // somewhere the macro is invoked to generate module-private artifacts...
230//! gen_csv_ds!(pub(crate), "River", "...ne_110m_rivers_lake_centerlines.csv", ZRiver);
231//!
232//! // now we collect all the "rivers" in the collection...
233//! let csv = RiverCSV::new();
234//! let it: Result<Vec<ZRiver>, MyError> = csv.iter()?.collect();
235//! // ...
236//! #     Ok(())
237//! # }
238//! ```
239//! The [`StreamableDS`] trait is more versatile. It exposes methods to stream
240//! asynchronously the contents as _Features_ ([`fetch()`][StreamableDS::fetch()]
241//! and [`fetch_where()`][StreamableDS::fetch_where()]) and _Resources_
242//! ([`stream()`][StreamableDS::stream()] and [`stream_where()`][StreamableDS::stream_where()]).
243//! The methods with the `_where` suffix expect an [`Expression`] argument that
244//! will be delegated to the data source itself to use for _filtering_ the
245//! contents in the best way it can; e.g. SQL WHERE clause for a _GeoPackage_
246//! file, and _PostGIS_ DB tables, etc...
247//!
248//! Similar to the CSV data source, the `gen_gpkg_ds!` macro does the heavy
249//! lifting generating the necessary artifcats for a _GeoPackage_ data-source.
250//! As does the `gen_pg_ds!` macro for a _PostGIS_ data-source.
251//!
252//!
253//! # Relative performance
254//!
255//! With the introduction of the [`DataSource`], [`IterableDS`] and [`StreamableDS`]
256//! traits and the provided [`DataSource`] implementations, a User can
257//! effectively process the data in 3 ways:
258//!
259//! * as _Features_ using the [`IterableDS`] trait --from a _CSV_ table.
260//! * as either _Features_ or [`Resource`]s using the [`StreamableDS`] trait through
261//!   the `fetch()` or `stream()` hooks --from a _GeoPackage_ database file,
262//!   or a _PostGIS_ layer,
263//! * as _Features_ or [`Resource`]s using the [`StreamableDS`] trait through the
264//!   `fetch_where()` or `stream_where()` hooks --from a _GeoPackage_ DB or a
265//!   _PostGIS_ layer.
266//!
267//! The last approach is by far the most effective since it delegates to a
268//! DB engine the job of filtering the records, while the 2<sup>nd</sup> one
269//! is the worst b/c it involves converting every _Feature_ to a [`Resource`]
270//! even when we may not need all the queryables from that newly created
271//! [`Resource`].
272//!
273//! As an example of relative performance of those approaches, consider the
274//! timing of `test_points`, `test_points_gpkg` and `test_points_sql` in
275//! `a9::test_37` which correspond to those 3 strategies respectively when
276//! processing a data set of 243 records. On a 4×Intel® Core™ i7-3610QM CPU
277//! @ 2.30GHz laptop, w/ the `profile [unoptimized + debuginfo]` I get...
278//!
279//! | # | test                 | time  |
280//! |---|----------------------|-------|
281//! | 1 | `test_points()`      | 0.09s |
282//! | 2 | `test_points_gpkg()` | 5.35s |
283//! | 3 | `test_points_sql()`  | 0.11s |
284//!
285//!
286//! # Third-party crates
287//!
288//! This project, in addition to the external software mentioned in the [README][readme],
289//! relies on few 3<sup>rd</sup> party crates. In addition to the `csv`, and `sqlx`
290//! crates already mentioned, here are the most important ones...
291//!
292//! 1. PEG
293//!    * [`peg`](https://crates.io/crates/peg): Provides a Rust macro that builds
294//!      a recursive descent parser from a concise definition of a grammar.
295//!
296//! 2. JSON Deserialization:
297//!    * [serde][3]: for the basic capabilities.
298//!    * [serde_json][4]: for the JSON format bindings.
299//!    * [serde_with][5]: for custom helpers.
300//!
301//! 3. Date + Time:
302//!    * [jiff][6]: for time-zone-aware date and timestamp handling.
303//!
304//! 4. Case + Accent Insensitive Strings:
305//!    * [unicase][7]: for comparing strings when case is not important.
306//!    * [unicode-normalization][8]: for un-accenting strings w/ Unicode
307//!      decomposition.
308//!
309//! 5. CRS Transformation:
310//!    * [proj][9]: for coordinate transformation via bindings to the [PROJ][10]
311//!      API.
312//!
313//! # Rust (conditional compliation) Features
314//!
315//! This project currently allows users the ability to process GIS data
316//! provided by different data-source types, gated by the following features:
317//!
318//! | Feature   | Data Source                                         |
319//! |-----------|-----------------------------------------------------|
320//! | `csv_ds`  | Comma Separated Values (CSV) textual data.<br/>Geometry columns are Well Known Text (WKT) strings.<br/>Properties are formatted strings. |
321//! | `gpkg_ds` | _GeoPackage_ data.<br/>Geometry columns are Well Known Binary (WKB) bytes.<br/>Properties are standard SQLite types. |
322//! | `pg_ds`   | _PostGIS_ tables.<br/>Geometry columns are Well Known Binary (WKB) bytes.<br/>Properties are standard PostgreSQL types. |
323//!
324//!
325//! [1]: https://crates.io/crates/geos
326//! [2]: https://libgeos.org/
327//! [3]: https://crates.io/crates/serde
328//! [4]: https://crates.io/crates/serde_json
329//! [5]: https://crates.io/crates/serde_with
330//! [6]: https://crates.io/crates/jiff
331//! [7]: https://crates.io/crates/unicase
332//! [8]: https://crates.io/crates/unicode-normalization
333//! [9]: https://crates.io/crates/proj
334//! [10]: https://proj.org/
335//!
336//! [gpkg]: https://www.geopackage.org/spec140/index.html
337//! [shapefile]: https://en.wikipedia.org/wiki/Shapefile
338//! [pgis]: https://en.wikipedia.org/wiki/PostGIS
339//! [sqlx]: https://crates.io/crates/sqlx
340//! [readme]: https://crates.io/crates/xapi-rs
341//!
342
343#![doc = include_str!("../doc/FUNCTION.md")]
344#![doc = include_str!("../doc/CONFIGURATION.md")]
345
346mod bound;
347mod config;
348mod context;
349mod crs;
350mod ds;
351mod error;
352mod evaluator;
353mod expr;
354mod function;
355mod geom;
356mod json;
357mod op;
358mod qstring;
359mod queryable;
360mod srid;
361mod text;
362mod wkb;
363
364pub use bound::*;
365pub use context::*;
366pub use crs::*;
367pub use ds::*;
368pub use evaluator::*;
369pub use function::*;
370pub use geom::*;
371pub use qstring::QString;
372pub use queryable::*;
373pub use srid::*;
374
375pub mod prelude;
376
377use crate::{expr::E, text::cql2::expression};
378use core::fmt;
379pub use error::MyError;
380
381/// An instance of an OGC CQL2 filter.
382#[derive(Debug)]
383pub enum Expression {
384    /// Instance generated from a successfully parsed text-encoded input string.
385    Text(TextEncoded),
386    /// Instance generated from a successfully parsed JSON-encoded input string.
387    Json(Box<JsonEncoded>),
388}
389
390impl Expression {
391    /// Try to construct from a text-encoded string.
392    pub fn try_from_text(s: &str) -> Result<Self, MyError> {
393        let x = expression(s).map_err(MyError::Text)?;
394        Ok(Expression::Text(TextEncoded(x)))
395    }
396
397    /// Try to construct from a JSON-encoded string.
398    pub fn try_from_json(s: &str) -> Result<Self, MyError> {
399        let x = serde_json::from_str::<json::Expression>(s).map_err(MyError::Json)?;
400        Ok(Expression::Json(Box::new(JsonEncoded(x))))
401    }
402
403    /// Return a reference to the text-encoded variant as an `Option`.
404    pub fn as_text_encoded(&self) -> Option<&TextEncoded> {
405        match self {
406            Expression::Text(x) => Some(x),
407            Expression::Json(_) => None,
408        }
409    }
410
411    // convert both variants to the common `E` intermediary form.
412    #[allow(dead_code)]
413    pub(crate) fn to_inner(&self) -> Result<E, MyError> {
414        match self {
415            Expression::Text(x) => Ok(x.0.to_owned()),
416            Expression::Json(x) => {
417                let s = &x.0.to_string();
418                let te = Self::try_from_text(s)?;
419                let it = te
420                    .as_text_encoded()
421                    .ok_or_else(|| MyError::Runtime("Failed converting to TE".into()))?;
422                Ok(it.0.to_owned())
423            }
424        }
425    }
426}
427
428impl fmt::Display for Expression {
429    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430        match self {
431            Expression::Text(x) => write!(f, "{}", x.0),
432            Expression::Json(x) => write!(f, "{}", x.0),
433        }
434    }
435}
436
437/// Text-encoded CQL2 [`Expression`].
438#[derive(Debug, PartialEq)]
439pub struct TextEncoded(expr::E);
440
441/// JSON-encoded CQL2 [`Expression`].
442#[derive(Debug)]
443pub struct JsonEncoded(json::Expression);
444
445/// Possible outcome values when evaluating an [`Expression`] against an
446/// individual [`Resource`] from a collection.
447///
448/// From [OGC CQL2](https://docs.ogc.org/is/21-065r2/21-065r2.html):
449/// > _Each resource instance in the source collection is evaluated against
450/// > a filtering expression. The net effect of evaluating a filter
451/// > [`Expression`] is a subset of resources that satisfy the predicate(s)
452/// > in the [`Expression`]._
453///
454/// Logically connected predicates are evaluated according to the following
455/// truth table, where `T` is TRUE, `F` is FALSE and `N` is NULL.
456///
457/// | P1  | P2  | P1 & P2 | P1 &#124; P2 |
458/// |-----|-----|:-------:|:-------:|
459/// |  T  |  T  |    T    |    T    |
460/// |  T  |  F  |    F    |    T    |
461/// |  F  |  T  |    F    |    T    |
462/// |  F  |  F  |    F    |    F    |
463/// |  T  |  N  |    N    |    T    |
464/// |  F  |  N  |    F    |    N    |
465/// |  N  |  T  |    N    |    T    |
466/// |  N  |  F  |    F    |    N    |
467/// |  N  |  N  |    N    |    N    |
468#[derive(Debug, PartialEq, Eq)]
469pub enum Outcome {
470    /// The input satisfies the [Expression] and should be marked as being in
471    /// the result set.
472    T,
473    /// The input does not satisfy the filter [Expression] and should not be
474    /// included the result set.
475    F,
476    /// Likewise.
477    N,
478}
479
480impl fmt::Display for Outcome {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        match self {
483            Outcome::T => write!(f, "T"),
484            Outcome::F => write!(f, "F",),
485            Outcome::N => write!(f, "N"),
486        }
487    }
488}
489
490impl Outcome {
491    /// Constructor from an optional boolean.
492    pub fn new(flag: Option<&bool>) -> Self {
493        match flag {
494            Some(b) => match b {
495                true => Self::T,
496                false => Self::F,
497            },
498            None => Self::N,
499        }
500    }
501}