diff --git a/CHANGELOG.md b/CHANGELOG.md index a385debd28a0..57e788027441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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**: diff --git a/prql-compiler/src/sql/dialect.rs b/prql-compiler/src/sql/dialect.rs index a5af88dbf95a..23ac872fb940 100644 --- a/prql-compiler/src/sql/dialect.rs +++ b/prql-compiler/src/sql/dialect.rs @@ -162,34 +162,22 @@ 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 { - // 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 { let args = [search, target] .into_iter() .map(FunctionArgExpr::Expr) @@ -197,7 +185,7 @@ pub(super) trait DialectHandler: Any + Debug { .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, @@ -205,6 +193,19 @@ pub(super) trait DialectHandler: Any + Debug { order_by: vec![], })) } + + fn translate_regex_with_operator( + &self, + search: sql_ast::Expr, + target: sql_ast::Expr, + operator: sql_ast::BinaryOperator, + ) -> anyhow::Result { + Ok(sql_ast::Expr::BinaryOp { + left: Box::new(search), + op: operator, + right: Box::new(target), + }) + } } impl dyn DialectHandler { @@ -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 { + self.translate_regex_with_operator(search, target, sql_ast::BinaryOperator::PGRegexMatch) } } @@ -242,12 +247,16 @@ 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 { + self.translate_regex_with_operator( + search, + target, + sql_ast::BinaryOperator::Custom("REGEXP".to_string()), + ) } } @@ -255,8 +264,15 @@ 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 { + 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 @@ -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 { + self.translate_regex_with_operator( + search, + target, + sql_ast::BinaryOperator::Custom("REGEXP".to_string()), + ) } } @@ -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 { + self.translate_regex_with_function(search, target, "REGEXP_CONTAINS") } } @@ -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 { + self.translate_regex_with_function(search, target, "REGEXP_MATCHES") } } diff --git a/prql-compiler/src/tests/test_error_messages.rs b/prql-compiler/src/tests/test_error_messages.rs index 450915605d5f..4c44eb7db2f0 100644 --- a/prql-compiler/src/tests/test_error_messages.rs +++ b/prql-compiler/src/tests/test_error_messages.rs @@ -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###" @@ -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 ───╯ "###) } diff --git a/web/book/src/language-features/regex.md b/web/book/src/language-features/regex.md index d296a4b54f74..1aef28bf9a94 100644 --- a/web/book/src/language-features/regex.md +++ b/web/book/src/language-features/regex.md @@ -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\\?") +``` diff --git a/web/book/tests/snapshots/snapshot__language-features__regex-3.snap b/web/book/tests/snapshots/snapshot__language-features__regex-3.snap index c11f2c732aa0..98187d734d07 100644 --- a/web/book/tests/snapshots/snapshot__language-features__regex-3.snap +++ b/web/book/tests/snapshots/snapshot__language-features__regex-3.snap @@ -7,5 +7,5 @@ SELECT FROM tracks WHERE - REGEXP_LIKE(name, '\(I Can''t Help\) Falling') + name ~ '\(I Can''t Help\) Falling' diff --git a/web/book/tests/snapshots/snapshot__language-features__regex-4.snap b/web/book/tests/snapshots/snapshot__language-features__regex-4.snap new file mode 100644 index 000000000000..1d492f28b166 --- /dev/null +++ b/web/book/tests/snapshots/snapshot__language-features__regex-4.snap @@ -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' + diff --git a/web/book/tests/snapshots/snapshot__language-features__regex-5.snap b/web/book/tests/snapshots/snapshot__language-features__regex-5.snap new file mode 100644 index 000000000000..48739f88db13 --- /dev/null +++ b/web/book/tests/snapshots/snapshot__language-features__regex-5.snap @@ -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\?' +