Skip to content

Commit 3bd3192

Browse files
authored
fix: use cql2-json for pgstac (#513)
1 parent ac57183 commit 3bd3192

26 files changed

+182
-39
lines changed

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ bb8-postgres = "0.8.1"
4545
bytes = "1.7"
4646
chrono = "0.4.38"
4747
clap = "4.5"
48+
cql2 = "0.3.0"
4849
duckdb = "=1.0.0"
4950
fluent-uri = "0.3.1"
5051
futures = "0.3.31"
@@ -60,7 +61,7 @@ mime = "0.3.17"
6061
mockito = "1.5"
6162
object_store = "0.11.0"
6263
openssl = { version = "0.10.68", features = ["vendored"] }
63-
openssl-src = "=300.3.1" # joinked from https://github.com/iopsystems/rpc-perf/commit/705b290d2105af6f33150da04b217422c6d68701#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R41 to cross-compile Python
64+
openssl-src = "=300.3.1" # joinked from https://github.com/iopsystems/rpc-perf/commit/705b290d2105af6f33150da04b217422c6d68701#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R41 to cross-compile Python
6465
parquet = { version = "52.2", default-features = false }
6566
pgstac = { version = "0.2.1", path = "crates/pgstac" }
6667
pyo3 = "0.22.3"
@@ -89,6 +90,9 @@ tokio-test = "0.4.4"
8990
tower = "0.5.1"
9091
tower-http = "0.6.1"
9192
tracing = "0.1.40"
92-
tracing-subscriber = "0.3.18"
93+
tracing-subscriber = { version = "0.3.18", features = [
94+
"env-filter",
95+
"tracing-log",
96+
] }
9397
url = "2.3"
9498
webpki-roots = "0.26.6"

crates/api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ geo = ["dep:geo", "stac/geo"]
2525
[dependencies]
2626
async-stream = { workspace = true, optional = true }
2727
chrono.workspace = true
28+
cql2.workspace = true
2829
futures = { workspace = true, optional = true }
2930
http = { workspace = true, optional = true }
3031
reqwest = { workspace = true, features = ["json"], optional = true }

crates/api/src/client.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,7 @@ impl Client {
220220
if let Some(headers) = headers.into() {
221221
request = request.headers(headers);
222222
}
223-
let response = request.send().await?;
224-
if !response.status().is_success() {
225-
let status_code = response.status();
226-
let text = response.text().await.ok().unwrap_or_default();
227-
return Err(Error::Request { status_code, text });
228-
}
223+
let response = request.send().await?.error_for_status()?;
229224
response.json().await.map_err(Error::from)
230225
}
231226

@@ -361,8 +356,12 @@ fn stream_pages(
361356

362357
fn not_found_to_none<T>(result: Result<T>) -> Result<Option<T>> {
363358
let mut result = result.map(Some);
364-
if let Err(Error::Request { status_code, .. }) = result {
365-
if status_code == StatusCode::NOT_FOUND {
359+
if let Err(Error::Reqwest(ref err)) = result {
360+
if err
361+
.status()
362+
.map(|s| s == StatusCode::NOT_FOUND)
363+
.unwrap_or_default()
364+
{
366365
result = Ok(None);
367366
}
368367
}

crates/api/src/error.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ pub enum Error {
2121
#[error(transparent)]
2222
ChronoParse(#[from] chrono::ParseError),
2323

24+
/// [cql2::Error]
25+
#[error(transparent)]
26+
Cql2(#[from] cql2::Error),
27+
2428
/// [geojson::Error]
2529
#[error(transparent)]
2630
GeoJson(#[from] Box<geojson::Error>),
@@ -75,17 +79,6 @@ pub enum Error {
7579
#[cfg(feature = "client")]
7680
Reqwest(#[from] reqwest::Error),
7781

78-
/// A search error.
79-
#[error("HTTP status error ({status_code}): {text}")]
80-
#[cfg(feature = "client")]
81-
Request {
82-
/// The status code
83-
status_code: reqwest::StatusCode,
84-
85-
/// The text of the server response.
86-
text: String,
87-
},
88-
8982
/// A search has both bbox and intersects.
9083
#[error("search has bbox and intersects")]
9184
SearchHasBboxAndIntersects(Box<Search>),

crates/api/src/filter.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use std::{convert::Infallible, str::FromStr};
2-
1+
use crate::Result;
2+
use cql2::Expr;
33
use serde::{Deserialize, Serialize};
44
use serde_json::{Map, Value};
5+
use std::{convert::Infallible, str::FromStr};
56

67
/// The language of the filter expression.
78
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -16,6 +17,32 @@ pub enum Filter {
1617
Cql2Json(Map<String, Value>),
1718
}
1819

20+
impl Filter {
21+
/// Converts this filter to cql2-json.
22+
pub fn into_cql2_json(self) -> Result<Filter> {
23+
match self {
24+
Filter::Cql2Json(_) => Ok(self),
25+
Filter::Cql2Text(text) => {
26+
let expr = cql2::parse_text(&text)?;
27+
Ok(Filter::Cql2Json(serde_json::from_value(
28+
serde_json::to_value(expr)?,
29+
)?))
30+
}
31+
}
32+
}
33+
34+
/// Converts this filter to cql2-json.
35+
pub fn into_cql2_text(self) -> Result<Filter> {
36+
match self {
37+
Filter::Cql2Text(_) => Ok(self),
38+
Filter::Cql2Json(json) => {
39+
let expr: Expr = serde_json::from_value(Value::Object(json))?;
40+
Ok(Filter::Cql2Text(expr.to_text()?))
41+
}
42+
}
43+
}
44+
}
45+
1946
impl Default for Filter {
2047
fn default() -> Self {
2148
Filter::Cql2Json(Default::default())
@@ -24,7 +51,7 @@ impl Default for Filter {
2451

2552
impl FromStr for Filter {
2653
type Err = Infallible;
27-
fn from_str(s: &str) -> Result<Self, Self::Err> {
54+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
2855
Ok(Filter::Cql2Text(s.to_string()))
2956
}
3057
}

crates/api/src/items.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub struct Items {
3838
pub filter_crs: Option<String>,
3939

4040
/// CQL2 filter expression.
41-
#[serde(skip_serializing_if = "Option::is_none")]
41+
#[serde(skip_serializing_if = "Option::is_none", flatten)]
4242
pub filter: Option<Filter>,
4343

4444
/// Additional filtering based on properties.
@@ -291,6 +291,14 @@ impl Items {
291291
collections: Some(vec![collection_id.to_string()]),
292292
}
293293
}
294+
295+
/// Converts the filter to cql2-json, if it is set.
296+
pub fn into_cql2_json(mut self) -> Result<Items> {
297+
if let Some(filter) = self.filter {
298+
self.filter = Some(filter.into_cql2_json()?);
299+
}
300+
Ok(self)
301+
}
294302
}
295303

296304
impl TryFrom<Items> for GetItems {

crates/api/src/search.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ impl Search {
211211
Ok(true)
212212
}
213213
}
214+
215+
/// Converts this search's filter to cql2-json, if set.
216+
pub fn into_cql2_json(mut self) -> Result<Search> {
217+
self.items = self.items.into_cql2_json()?;
218+
Ok(self)
219+
}
214220
}
215221

216222
impl TryFrom<Search> for GetSearch {

crates/cli/src/args.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ pub struct Args {
7070
#[derive(Debug, clap::Subcommand, Clone)]
7171
#[allow(clippy::large_enum_variant)]
7272
pub enum Subcommand {
73+
/// Interact with a pgstac database
74+
#[cfg(feature = "pgstac")]
75+
Pgstac(crate::subcommand::pgstac::Args),
76+
7377
/// Search for STAC items
7478
Search(search::Args),
7579

@@ -99,6 +103,8 @@ impl Args {
99103
/// Runs whatever these arguments say that we should run.
100104
pub async fn run(self) -> Result<()> {
101105
match &self.subcommand {
106+
#[cfg(feature = "pgstac")]
107+
Subcommand::Pgstac(args) => self.pgstac(args).await,
102108
Subcommand::Search(args) => self.search(args).await,
103109
Subcommand::Serve(args) => self.serve(args).await,
104110
Subcommand::Translate(args) => self.translate(args).await,

crates/cli/src/subcommand/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "pgstac")]
2+
pub mod pgstac;
13
pub mod search;
24
pub mod serve;
35
pub mod translate;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use crate::Result;
2+
use stac_server::PgstacBackend;
3+
4+
#[derive(Debug, Clone, clap::Args)]
5+
pub struct Args {
6+
/// The pgstac subcommand
7+
#[command(subcommand)]
8+
subcommand: Subcommand,
9+
}
10+
11+
#[derive(clap::Subcommand, Debug, Clone)]
12+
pub enum Subcommand {
13+
/// Loads data into the pgstac database
14+
Load(LoadArgs),
15+
}
16+
17+
#[derive(clap::Args, Debug, Clone)]
18+
pub struct LoadArgs {
19+
/// The connection string.
20+
dsn: String,
21+
22+
/// Hrefs to load into the database.
23+
///
24+
/// If not provided or `-`, data will be read from standard input.
25+
hrefs: Vec<String>,
26+
27+
/// Load in all "item" links on collections.
28+
#[arg(short, long)]
29+
load_collection_items: bool,
30+
31+
/// Auto-generate collections for any collection-less items.
32+
#[arg(short, long)]
33+
create_collections: bool,
34+
}
35+
36+
impl crate::Args {
37+
pub async fn pgstac(&self, args: &Args) -> Result<()> {
38+
match &args.subcommand {
39+
Subcommand::Load(load_args) => {
40+
let mut backend = PgstacBackend::new_from_stringlike(&load_args.dsn).await?;
41+
let load = self
42+
.load(
43+
&mut backend,
44+
load_args.hrefs.iter().map(|h| h.as_str()),
45+
load_args.load_collection_items,
46+
load_args.create_collections,
47+
)
48+
.await?;
49+
eprintln!(
50+
"Loaded {} collection(s) and {} item(s)",
51+
load.collections, load.items
52+
);
53+
Ok(())
54+
}
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)