Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
REGEXP(name, 'Love')
```

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

**Fixes**:

Expand Down
114 changes: 71 additions & 43 deletions prql-compiler/src/sql/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,49 +162,50 @@ pub(super) trait DialectHandler: Any + Debug {
true
}

fn regex_function(&self) -> Option<&'static str> {
Some("REGEXP")
}

fn translate_regex(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
// When
// https://github.com/sqlparser-rs/sqlparser-rs/issues/863#issuecomment-1537272570
// is fixed, we can implement the infix for the other dialects. Until
// then, we still only have the `regex_function` implementations. (But
// I'd done the work to allow any Expr to be created before realizing
// sqlparser-rs didn't support custom operators, so may as well leave it
// in for when they do / we find another approach.)

let Some(regex_function) = self.regex_function() else {
// TODO: name the dialect, but not immediately obvious how to actually
// get the dialect string from a `DialectHandler` (though we could
// add it to each impl...)
//
// MSSQL doesn't support them, MySQL & SQLite have a different construction.
return Err(Error::new(
crate::Reason::Simple("regex functions are not supported by this dialect (or PRQL doesn't yet implement this dialect)".to_string())
).into())
};
self.translate_regex_with_function(search, target, "REGEXP")
}

fn translate_regex_with_function(
// This `self` isn't actually used, but it's require because of object
// safety (open to better ways of doing this...)
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
function_name: &str,
) -> anyhow::Result<sql_ast::Expr> {
let args = [search, target]
.into_iter()
.map(FunctionArgExpr::Expr)
.map(FunctionArg::Unnamed)
.collect();

Ok(sql_ast::Expr::Function(Function {
name: ObjectName(vec![sql_ast::Ident::new(regex_function)]),
name: ObjectName(vec![sql_ast::Ident::new(function_name)]),
args,
over: None,
distinct: false,
special: false,
order_by: vec![],
}))
}

fn translate_regex_with_operator(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
operator: sql_ast::BinaryOperator,
) -> anyhow::Result<sql_ast::Expr> {
Ok(sql_ast::Expr::BinaryOp {
left: Box::new(search),
op: operator,
right: Box::new(target),
})
}
}

impl dyn DialectHandler {
Expand All @@ -220,8 +221,12 @@ impl DialectHandler for PostgresDialect {
fn requires_quotes_intervals(&self) -> bool {
true
}
fn regex_function(&self) -> std::option::Option<&'static str> {
Some("REGEXP_LIKE")
fn translate_regex(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
self.translate_regex_with_operator(search, target, sql_ast::BinaryOperator::PGRegexMatch)
}
}

Expand All @@ -242,21 +247,32 @@ impl DialectHandler for SQLiteDialect {
false
}

fn regex_function(&self) -> Option<&'static str> {
// Sqlite has a different construction, using `REGEXP` as an operator.
// (`foo REGEXP 'bar').
//
// TODO: change the construction of the function to allow this.
None
fn translate_regex(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
self.translate_regex_with_operator(
search,
target,
sql_ast::BinaryOperator::Custom("REGEXP".to_string()),
)
}
}

impl DialectHandler for MsSqlDialect {
fn use_top(&self) -> bool {
true
}
fn regex_function(&self) -> Option<&'static str> {
None

fn translate_regex(
&self,
_search: sql_ast::Expr,
_target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
Err(Error::new(crate::Reason::Simple(
"regex functions are not supported by MsSql".to_string(),
)))?
}

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

fn regex_function(&self) -> Option<&'static str> {
// MySQL has a different construction, using `REGEXP` as an operator.
// (`foo REGEXP 'bar'). So
//
// TODO: change the construction of the function to allow this.
None
fn translate_regex(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
self.translate_regex_with_operator(
search,
target,
sql_ast::BinaryOperator::Custom("REGEXP".to_string()),
)
}
}

Expand All @@ -311,8 +331,12 @@ impl DialectHandler for BigQueryDialect {
true
}

fn regex_function(&self) -> Option<&'static str> {
Some("REGEXP_CONTAINS")
fn translate_regex(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
self.translate_regex_with_function(search, target, "REGEXP_CONTAINS")
}
}

Expand All @@ -339,8 +363,12 @@ impl DialectHandler for DuckDbDialect {
false
}

fn regex_function(&self) -> Option<&'static str> {
Some("REGEXP_MATCHES")
fn translate_regex(
&self,
search: sql_ast::Expr,
target: sql_ast::Expr,
) -> anyhow::Result<sql_ast::Expr> {
self.translate_regex_with_function(search, target, "REGEXP_MATCHES")
}
}

Expand Down
4 changes: 2 additions & 2 deletions prql-compiler/src/tests/test_error_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn test_hint_missing_args() {
#[test]
fn test_regex_dialect() {
assert_display_snapshot!(compile(r###"
prql target:sql.sqlite
prql target:sql.mssql
from foo
filter bar ~= 'love'
"###).unwrap_err(), @r###"
Expand All @@ -139,7 +139,7 @@ fn test_regex_dialect() {
4 │ filter bar ~= 'love'
│ ──────┬──────
│ ╰──────── regex functions are not supported by this dialect (or PRQL doesn't yet implement this dialect)
│ ╰──────── regex functions are not supported by MsSql
───╯
"###)
}
14 changes: 14 additions & 0 deletions web/book/src/language-features/regex.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,17 @@ prql target:sql.postgres
from tracks
filter (name ~= "\\(I Can't Help\\) Falling")
```

```prql
prql target:sql.mysql

from tracks
filter (name ~= "With You")
```

```prql no-fmt
prql target:sql.sqlite

from tracks
filter (name ~= "But Why Isn't Your Syntax More Similar\\?")
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ SELECT
FROM
tracks
WHERE
REGEXP_LIKE(name, '\(I Can''t Help\) Falling')
name ~ '\(I Can''t Help\) Falling'

11 changes: 11 additions & 0 deletions web/book/tests/snapshots/snapshot__language-features__regex-4.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: web/book/tests/snapshot.rs
expression: "prql target:sql.mysql\n\nfrom tracks\nfilter (name ~= \"With You\")\n"
---
SELECT
*
FROM
tracks
WHERE
name REGEXP 'With You'

11 changes: 11 additions & 0 deletions web/book/tests/snapshots/snapshot__language-features__regex-5.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: web/book/tests/snapshot.rs
expression: "prql target:sql.sqlite\n\nfrom tracks\nfilter (name ~= \"But Why Isn't Your Syntax More Similar\\\\?\")\n"
---
SELECT
*
FROM
tracks
WHERE
name REGEXP 'But Why Isn''t Your Syntax More Similar\?'