Skip to content

Commit ff0814a

Browse files
committed
feat: Allow regex operators
Uses the new custom operators feature from `sqlparser-rs` apache/datafusion-sqlparser-rs#868
1 parent c8d63c1 commit ff0814a

File tree

7 files changed

+113
-47
lines changed

7 files changed

+113
-47
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
REGEXP(name, 'Love')
2323
```
2424

25-
...though the exact form differs by dialect.
25+
...though the exact form differs by dialect; see the
26+
[Regex docs](https://prql-lang.org/book/language-features/regex.html) for more
27+
details.
2628

2729
**Fixes**:
2830

prql-compiler/src/sql/dialect.rs

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -162,49 +162,50 @@ pub(super) trait DialectHandler: Any + Debug {
162162
true
163163
}
164164

165-
fn regex_function(&self) -> Option<&'static str> {
166-
Some("REGEXP")
167-
}
168-
169165
fn translate_regex(
170166
&self,
171167
search: sql_ast::Expr,
172168
target: sql_ast::Expr,
173169
) -> anyhow::Result<sql_ast::Expr> {
174-
// When
175-
// https://github.com/sqlparser-rs/sqlparser-rs/issues/863#issuecomment-1537272570
176-
// is fixed, we can implement the infix for the other dialects. Until
177-
// then, we still only have the `regex_function` implementations. (But
178-
// I'd done the work to allow any Expr to be created before realizing
179-
// sqlparser-rs didn't support custom operators, so may as well leave it
180-
// in for when they do / we find another approach.)
181-
182-
let Some(regex_function) = self.regex_function() else {
183-
// TODO: name the dialect, but not immediately obvious how to actually
184-
// get the dialect string from a `DialectHandler` (though we could
185-
// add it to each impl...)
186-
//
187-
// MSSQL doesn't support them, MySQL & SQLite have a different construction.
188-
return Err(Error::new(
189-
crate::Reason::Simple("regex functions are not supported by this dialect (or PRQL doesn't yet implement this dialect)".to_string())
190-
).into())
191-
};
170+
self.translate_regex_with_function(search, target, "REGEXP")
171+
}
192172

173+
fn translate_regex_with_function(
174+
// This `self` isn't actually used, but it's require because of object
175+
// safety (open to better ways of doing this...)
176+
&self,
177+
search: sql_ast::Expr,
178+
target: sql_ast::Expr,
179+
function_name: &str,
180+
) -> anyhow::Result<sql_ast::Expr> {
193181
let args = [search, target]
194182
.into_iter()
195183
.map(FunctionArgExpr::Expr)
196184
.map(FunctionArg::Unnamed)
197185
.collect();
198186

199187
Ok(sql_ast::Expr::Function(Function {
200-
name: ObjectName(vec![sql_ast::Ident::new(regex_function)]),
188+
name: ObjectName(vec![sql_ast::Ident::new(function_name)]),
201189
args,
202190
over: None,
203191
distinct: false,
204192
special: false,
205193
order_by: vec![],
206194
}))
207195
}
196+
197+
fn translate_regex_with_operator(
198+
&self,
199+
search: sql_ast::Expr,
200+
target: sql_ast::Expr,
201+
operator: sql_ast::BinaryOperator,
202+
) -> anyhow::Result<sql_ast::Expr> {
203+
Ok(sql_ast::Expr::BinaryOp {
204+
left: Box::new(search),
205+
op: operator,
206+
right: Box::new(target),
207+
})
208+
}
208209
}
209210

210211
impl dyn DialectHandler {
@@ -220,8 +221,12 @@ impl DialectHandler for PostgresDialect {
220221
fn requires_quotes_intervals(&self) -> bool {
221222
true
222223
}
223-
fn regex_function(&self) -> std::option::Option<&'static str> {
224-
Some("REGEXP_LIKE")
224+
fn translate_regex(
225+
&self,
226+
search: sql_ast::Expr,
227+
target: sql_ast::Expr,
228+
) -> anyhow::Result<sql_ast::Expr> {
229+
self.translate_regex_with_operator(search, target, sql_ast::BinaryOperator::PGRegexMatch)
225230
}
226231
}
227232

@@ -242,21 +247,32 @@ impl DialectHandler for SQLiteDialect {
242247
false
243248
}
244249

245-
fn regex_function(&self) -> Option<&'static str> {
246-
// Sqlite has a different construction, using `REGEXP` as an operator.
247-
// (`foo REGEXP 'bar').
248-
//
249-
// TODO: change the construction of the function to allow this.
250-
None
250+
fn translate_regex(
251+
&self,
252+
search: sql_ast::Expr,
253+
target: sql_ast::Expr,
254+
) -> anyhow::Result<sql_ast::Expr> {
255+
self.translate_regex_with_operator(
256+
search,
257+
target,
258+
sql_ast::BinaryOperator::Custom("REGEXP".to_string()),
259+
)
251260
}
252261
}
253262

254263
impl DialectHandler for MsSqlDialect {
255264
fn use_top(&self) -> bool {
256265
true
257266
}
258-
fn regex_function(&self) -> Option<&'static str> {
259-
None
267+
268+
fn translate_regex(
269+
&self,
270+
_search: sql_ast::Expr,
271+
_target: sql_ast::Expr,
272+
) -> anyhow::Result<sql_ast::Expr> {
273+
Err(Error::new(crate::Reason::Simple(
274+
"regex functions are not supported by MsSql".to_string(),
275+
)))?
260276
}
261277

262278
// https://learn.microsoft.com/en-us/sql/t-sql/language-elements/set-operators-except-and-intersect-transact-sql?view=sql-server-ver16
@@ -279,12 +295,16 @@ impl DialectHandler for MySqlDialect {
279295
true
280296
}
281297

282-
fn regex_function(&self) -> Option<&'static str> {
283-
// MySQL has a different construction, using `REGEXP` as an operator.
284-
// (`foo REGEXP 'bar'). So
285-
//
286-
// TODO: change the construction of the function to allow this.
287-
None
298+
fn translate_regex(
299+
&self,
300+
search: sql_ast::Expr,
301+
target: sql_ast::Expr,
302+
) -> anyhow::Result<sql_ast::Expr> {
303+
self.translate_regex_with_operator(
304+
search,
305+
target,
306+
sql_ast::BinaryOperator::Custom("REGEXP".to_string()),
307+
)
288308
}
289309
}
290310

@@ -311,8 +331,12 @@ impl DialectHandler for BigQueryDialect {
311331
true
312332
}
313333

314-
fn regex_function(&self) -> Option<&'static str> {
315-
Some("REGEXP_CONTAINS")
334+
fn translate_regex(
335+
&self,
336+
search: sql_ast::Expr,
337+
target: sql_ast::Expr,
338+
) -> anyhow::Result<sql_ast::Expr> {
339+
self.translate_regex_with_function(search, target, "REGEXP_CONTAINS")
316340
}
317341
}
318342

@@ -339,8 +363,12 @@ impl DialectHandler for DuckDbDialect {
339363
false
340364
}
341365

342-
fn regex_function(&self) -> Option<&'static str> {
343-
Some("REGEXP_MATCHES")
366+
fn translate_regex(
367+
&self,
368+
search: sql_ast::Expr,
369+
target: sql_ast::Expr,
370+
) -> anyhow::Result<sql_ast::Expr> {
371+
self.translate_regex_with_function(search, target, "REGEXP_MATCHES")
344372
}
345373
}
346374

prql-compiler/src/tests/test_error_messages.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ fn test_hint_missing_args() {
130130
#[test]
131131
fn test_regex_dialect() {
132132
assert_display_snapshot!(compile(r###"
133-
prql target:sql.sqlite
133+
prql target:sql.mssql
134134
from foo
135135
filter bar ~= 'love'
136136
"###).unwrap_err(), @r###"
@@ -139,7 +139,7 @@ fn test_regex_dialect() {
139139
140140
4 │ filter bar ~= 'love'
141141
│ ──────┬──────
142-
│ ╰──────── regex functions are not supported by this dialect (or PRQL doesn't yet implement this dialect)
142+
│ ╰──────── regex functions are not supported by MsSql
143143
───╯
144144
"###)
145145
}

web/book/src/language-features/regex.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,17 @@ prql target:sql.postgres
3434
from tracks
3535
filter (name ~= "\\(I Can't Help\\) Falling")
3636
```
37+
38+
```prql no-fmt
39+
prql target:sql.mysql
40+
41+
from tracks
42+
filter (name ~= "With You")
43+
```
44+
45+
```prql no-fmt
46+
prql target:sql.sqlite
47+
48+
from tracks
49+
filter (name ~= "But Why Isn't Your Syntax More Similar\\?")
50+
```

web/book/tests/snapshots/snapshot__language-features__regex-3.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ SELECT
77
FROM
88
tracks
99
WHERE
10-
REGEXP_LIKE(name, '\(I Can''t Help\) Falling')
10+
name ~ '\(I Can''t Help\) Falling'
1111

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: web/book/tests/snapshot.rs
3+
expression: "prql target:sql.mysql\n\nfrom tracks\nfilter (name ~= \"With You\")\n"
4+
---
5+
SELECT
6+
*
7+
FROM
8+
tracks
9+
WHERE
10+
name REGEXP 'With You'
11+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: web/book/tests/snapshot.rs
3+
expression: "prql target:sql.sqlite\n\nfrom tracks\nfilter (name ~= \"But Why Isn't Your Syntax More Similar\\\\?\")\n"
4+
---
5+
SELECT
6+
*
7+
FROM
8+
tracks
9+
WHERE
10+
name REGEXP 'But Why Isn''t Your Syntax More Similar\?'
11+

0 commit comments

Comments
 (0)