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 | 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}