Skip to content

Commit 54cca89

Browse files
authored
To duckdb sql (#70)
* initial support for duckdbsql * initial duckdb sql support * fix casei / accenti * warning message
1 parent 14b1ddc commit 54cca89

File tree

6 files changed

+536
-9
lines changed

6 files changed

+536
-9
lines changed

src/duckdb.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
use crate::expr::{ARITHOPS, ARRAYOPS, BOOLOPS, CMPOPS, EQOPS, SPATIALOPS, TEMPORALOPS};
2+
use crate::Error;
3+
use crate::Expr;
4+
use crate::Geometry;
5+
use pg_escape::{quote_identifier, quote_literal};
6+
7+
/// Traits for generating SQL for DuckDB with Spatial Extension
8+
pub trait ToDuckSQL {
9+
/// Convert Expression to SQL for DuckDB with Spatial Extension
10+
fn to_ducksql(&self) -> Result<String, Error>;
11+
}
12+
13+
fn lit_or_prop_to_ts(arg: &Expr) -> Result<String, Error> {
14+
match arg {
15+
Expr::Property { property } => Ok(quote_identifier(property).to_string()),
16+
Expr::Literal(v) => Ok(format!("TIMESTAMP {}", quote_literal(v))),
17+
_ => Err(Error::OperationError()),
18+
}
19+
}
20+
21+
impl ToDuckSQL for Expr {
22+
/// Converts this expression to DuckDB SQL.
23+
/// WARNING: This is an experimental feature with limited tests subject to change!
24+
///
25+
/// # Examples
26+
///
27+
/// ```
28+
/// use cql2::Expr;
29+
/// use cql2::ToDuckSQL;
30+
///
31+
/// let expr = Expr::Bool(true);
32+
/// assert_eq!(expr.to_ducksql().unwrap(), "true");
33+
/// ```
34+
/// ```
35+
/// use cql2::Expr;
36+
/// use cql2::ToDuckSQL;
37+
///
38+
/// let expr: Expr = "s_intersects(geom, POINT(0 0)) and foo >= 1 and bar='baz' and TIMESTAMP('2020-01-01 00:00:00Z') >= BoRk".parse().unwrap();
39+
/// assert_eq!(expr.to_ducksql().unwrap(), "ST_intersects(geom,ST_GeomFromText('POINT(0 0)')) and foo >= 1 and bar = 'baz' and TIMESTAMPTZ '2020-01-01 00:00:00Z' >= \"BoRk\"");
40+
/// ```
41+
/// ```
42+
/// use cql2::Expr;
43+
/// use cql2::ToDuckSQL;
44+
///
45+
/// let expr: Expr = "t_overlaps(interval(a,'2020-01-01T00:00:00Z'),interval('2020-01-01T00:00:00Z','2020-02-01T00:00:00Z'))".parse().unwrap();
46+
/// assert_eq!(expr.to_ducksql().unwrap(), "a < TIMESTAMP '2020-02-01T00:00:00Z' AND TIMESTAMP '2020-01-01T00:00:00Z' < TIMESTAMP '2020-01-01T00:00:00Z' AND TIMESTAMP '2020-01-01T00:00:00Z' < TIMESTAMP '2020-02-01T00:00:00Z'");
47+
/// ```
48+
/// ```
49+
/// use cql2::Expr;
50+
/// use cql2::ToDuckSQL;
51+
///
52+
/// let expr: Expr = "t_overlaps(interval(a,b),interval('2020-01-01T00:00:00Z','2020-02-01T00:00:00Z'))".parse().unwrap();
53+
/// assert_eq!(expr.to_ducksql().unwrap(), "a < TIMESTAMP '2020-02-01T00:00:00Z' AND TIMESTAMP '2020-01-01T00:00:00Z' < b AND b < TIMESTAMP '2020-02-01T00:00:00Z'");
54+
/// ```
55+
fn to_ducksql(&self) -> Result<String, Error> {
56+
Ok(match self {
57+
Expr::Bool(v) => {
58+
format!("{v}")
59+
}
60+
Expr::Float(v) => {
61+
format!("{v}")
62+
}
63+
Expr::Literal(v) => quote_literal(v).to_string(),
64+
Expr::Date { date } => {
65+
let s = date.to_ducksql()?;
66+
format!("DATE {s}")
67+
}
68+
Expr::Timestamp { timestamp } => {
69+
let s = timestamp.to_ducksql()?;
70+
format!("TIMESTAMPTZ {s}")
71+
}
72+
Expr::Interval { interval } => {
73+
let start = lit_or_prop_to_ts(interval[0].as_ref())?;
74+
let end = lit_or_prop_to_ts(interval[1].as_ref())?;
75+
format!("array_value({start}, {end})")
76+
}
77+
Expr::Geometry(v) => match v {
78+
Geometry::GeoJSON(v) => {
79+
let s = v.to_string();
80+
format!("ST_GeomFromGeoJSON({})", quote_literal(&s))
81+
}
82+
Geometry::Wkt(v) => {
83+
format!("ST_GeomFromText({})", quote_literal(v))
84+
}
85+
},
86+
87+
Expr::BBox { bbox } => {
88+
let array_els: Vec<String> = bbox
89+
.iter()
90+
.map(|a| a.to_ducksql())
91+
.collect::<Result<_, _>>()?;
92+
format!("[{}]", array_els.join(", "))
93+
}
94+
Expr::Array(v) => {
95+
let array_els: Vec<String> =
96+
v.iter().map(|a| a.to_ducksql()).collect::<Result<_, _>>()?;
97+
format!("array_value({})", array_els.join(", "))
98+
}
99+
Expr::Property { property } => format!("{}", quote_identifier(property)),
100+
Expr::Operation { op, args } => {
101+
let a: Vec<String> = args
102+
.iter()
103+
.map(|x| x.to_ducksql())
104+
.collect::<Result<_, _>>()?;
105+
let op = op.as_str();
106+
match op {
107+
"not" => format!("NOT {}", a[0]),
108+
"between" => format!("{} BETWEEN {} AND {}", a[0], a[1], a[2]),
109+
"in" => format!("IN ({})", a.join(",")),
110+
"like" => format!("{} LIKE {}", a[0], a[1]),
111+
"accenti" => format!("strip_accents({})", a[0]),
112+
"casei" => format!("lower({})", a[0]),
113+
_ => {
114+
if BOOLOPS.contains(&op) {
115+
let padded = format!(" {} ", op);
116+
a.join(&padded).to_string()
117+
} else if SPATIALOPS.contains(&op) {
118+
let sop = op.strip_prefix("s_").unwrap();
119+
format!("ST_{}({},{})", sop, a[0], a[1])
120+
} else if ARRAYOPS.contains(&op) {
121+
match op {
122+
"a_equals" => format!("{} = {}", a[0], a[1]),
123+
"a_contains" => format!("list_has_all({},{})", a[0], a[1]),
124+
"a_containedby" => format!("list_has_all({},{})", a[1], a[2]),
125+
"a_overlaps" => format!("list_has_any({},{})", a[0], a[1]),
126+
_ => unreachable!(),
127+
}
128+
} else if TEMPORALOPS.contains(&op) {
129+
let left_expr = *args[0].clone();
130+
let right_expr = *args[1].clone();
131+
132+
let left_start_init: String;
133+
let left_end_init: String;
134+
let right_start_init: String;
135+
let right_end_init: String;
136+
137+
if let Expr::Interval { interval } = left_expr {
138+
left_start_init = lit_or_prop_to_ts(&interval[0])?;
139+
left_end_init = lit_or_prop_to_ts(&interval[1])?;
140+
} else {
141+
unreachable!()
142+
}
143+
144+
if let Expr::Interval { interval } = right_expr {
145+
right_start_init = lit_or_prop_to_ts(&interval[0])?;
146+
right_end_init = lit_or_prop_to_ts(&interval[1])?;
147+
} else {
148+
unreachable!()
149+
}
150+
151+
let invop = match op {
152+
"t_after" => "t_before",
153+
"t_metby" => "t_meets",
154+
"t_overlappedby" => "t_overlaps",
155+
"t_startedby" => "t_starts",
156+
"t_contains" => "t_during",
157+
"t_finishedby" => "t_finishes",
158+
_ => op,
159+
};
160+
161+
let left_start: &str;
162+
let left_end: &str;
163+
let right_start: &str;
164+
let right_end: &str;
165+
166+
if invop == op {
167+
left_start = &left_start_init;
168+
left_end = &left_end_init;
169+
right_start = &right_start_init;
170+
right_end = &right_end_init;
171+
} else {
172+
right_start = &left_start_init;
173+
right_end = &left_end_init;
174+
left_start = &right_start_init;
175+
left_end = &right_end_init;
176+
}
177+
178+
match invop {
179+
"t_before" => format!("{left_end} < {right_start}"),
180+
"t_meets" => format!("{left_end} = {right_start}"),
181+
"t_overlaps" => {
182+
format!("{left_start} < {right_end} AND {right_start} < {left_end} AND {left_end} < {right_end}")
183+
}
184+
"t_starts" => format!("{left_start} = {right_start} AND {left_end} < {right_end}"),
185+
"t_during" => format!("{left_start} > {right_start} AND {left_end} < {right_end}"),
186+
"t_finishes" => format!("{left_start} > {right_start} AND {left_end} = {right_end}"),
187+
"t_equals" => format!("{left_start} = {right_start} AND {left_end} = {right_end}"),
188+
"t_disjoint" => format!("NOT ({left_start} <= {right_end} AND {left_end} >= {right_start})"),
189+
"t_intersects" | "anyinteracts" => format!("{left_start} <= {right_end} AND {left_end} >= {right_start}"),
190+
_ => unreachable!()
191+
}
192+
} else if CMPOPS.contains(&op)
193+
|| EQOPS.contains(&op)
194+
|| ARITHOPS.contains(&op)
195+
{
196+
format!("{} {} {}", a[0], op, a[1])
197+
} else {
198+
unreachable!()
199+
}
200+
}
201+
}
202+
}
203+
})
204+
}
205+
}

src/expr.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@ use std::str::FromStr;
1313
use unaccent::unaccent;
1414
use wkt::TryFromWkt;
1515

16-
const BOOLOPS: &[&str] = &["and", "or"];
17-
const EQOPS: &[&str] = &["=", "<>"];
18-
const CMPOPS: &[&str] = &[">", ">=", "<", "<="];
19-
const SPATIALOPS: &[&str] = &[
16+
/// Boolean Operators
17+
pub const BOOLOPS: &[&str] = &["and", "or"];
18+
19+
/// Equality Operators
20+
pub const EQOPS: &[&str] = &["=", "<>"];
21+
22+
/// Comparison Operators
23+
pub const CMPOPS: &[&str] = &[">", ">=", "<", "<="];
24+
25+
/// Spatial Operators
26+
pub const SPATIALOPS: &[&str] = &[
2027
"s_equals",
2128
"s_intersects",
2229
"s_disjoint",
@@ -26,7 +33,9 @@ const SPATIALOPS: &[&str] = &[
2633
"s_crosses",
2734
"s_contains",
2835
];
29-
const TEMPORALOPS: &[&str] = &[
36+
37+
/// Temporal Operators
38+
pub const TEMPORALOPS: &[&str] = &[
3039
"t_before",
3140
"t_after",
3241
"t_meets",
@@ -43,8 +52,12 @@ const TEMPORALOPS: &[&str] = &[
4352
"t_disjoint",
4453
"t_intersects",
4554
];
46-
const ARITHOPS: &[&str] = &["+", "-", "*", "/", "%", "^", "div"];
47-
const ARRAYOPS: &[&str] = &["a_equals", "a_contains", "a_containedby", "a_overlaps"];
55+
56+
/// Arithmetic Operators
57+
pub const ARITHOPS: &[&str] = &["+", "-", "*", "/", "%", "^", "div"];
58+
59+
/// Array Operators
60+
pub const ARRAYOPS: &[&str] = &["a_equals", "a_contains", "a_containedby", "a_overlaps"];
4861

4962
// todo: array ops, in, casei, accenti, between, not, like
5063

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,17 @@
3030
)]
3131
#![allow(clippy::result_large_err)]
3232

33+
mod duckdb;
3334
mod error;
3435
mod expr;
3536
mod geometry;
3637
mod parser;
3738
mod temporal;
3839
mod validator;
3940

41+
pub use duckdb::ToDuckSQL;
4042
pub use error::Error;
41-
pub use expr::Expr;
43+
pub use expr::*;
4244
pub use geometry::{spatial_op, Geometry};
4345
pub use parser::parse_text;
4446
use serde_derive::{Deserialize, Serialize};

0 commit comments

Comments
 (0)