Skip to content
Open
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
28 changes: 28 additions & 0 deletions crates/polyglot-sql/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20171,6 +20171,14 @@ impl Generator {
self.generate_expression(&f.this)?;
self.write(" ~ ");
self.generate_expression(&f.pattern)?;
} else if matches!(self.config.dialect, Some(DialectType::ClickHouse)) && f.flags.is_none()
{
// ClickHouse has no REGEXP_LIKE; the regex-match operator is match(haystack, pattern).
self.write("match(");
self.generate_expression(&f.this)?;
self.write(", ");
self.generate_expression(&f.pattern)?;
self.write(")");
} else if matches!(self.config.dialect, Some(DialectType::Exasol)) && f.flags.is_none() {
// Exasol uses REGEXP_LIKE as infix binary operator
self.generate_expression(&f.this)?;
Expand Down Expand Up @@ -34936,6 +34944,26 @@ impl Generator {
self.generate_expression(&e.this)?;
self.write(" ~* ");
self.generate_expression(&e.expression)?;
} else if matches!(self.config.dialect, Some(DialectType::ClickHouse)) && e.flag.is_none() {
// ClickHouse has no case-insensitive regex operator; use match() with an inline
// (?i) flag. Inline it into a string-literal pattern for readable output, and fall
// back to concat() so the flag also applies to a non-literal pattern expression.
self.write("match(");
self.generate_expression(&e.this)?;
self.write(", ");
if let Expression::Literal(lit) = e.expression.as_ref() {
if let Literal::String(s) = lit.as_ref() {
let insensitive =
Expression::Literal(Box::new(Literal::String(format!("(?i){s}"))));
self.generate_expression(&insensitive)?;
self.write(")");
return Ok(());
}
}
self.write("concat('(?i)', ");
self.generate_expression(&e.expression)?;
self.write("))");
return Ok(());
} else if matches!(self.config.dialect, Some(DialectType::Snowflake)) {
// Snowflake uses REGEXP_LIKE(x, pattern, 'i')
self.write_keyword("REGEXP_LIKE");
Expand Down
70 changes: 70 additions & 0 deletions crates/polyglot-sql/tests/postgres_regexp_clickhouse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Regression tests: PostgreSQL regex-match operators transpiled to ClickHouse.
//!
//! ClickHouse has no `REGEXP_LIKE` function and no case-insensitive regex operator; the
//! regex-match primitive is `match(haystack, pattern)`. Previously the PostgreSQL operators
//! `~`, `~*`, `!~`, `!~*` were emitted as `REGEXP_LIKE(...)` / an invalid `x REGEXP_ILIKE y`
//! infix form, both of which ClickHouse rejects. They now map to `match`, with case-insensitive
//! matching expressed via an inline `(?i)` flag.

use polyglot_sql::{transpile, DialectType};

fn pg_ch(sql: &str) -> String {
transpile(sql, DialectType::PostgreSQL, DialectType::ClickHouse)
.expect("PostgreSQL -> ClickHouse transpilation should succeed")
.join("; ")
}

#[test]
fn regex_match_operator_maps_to_match() {
let cases = [
("SELECT x ~ 'ab.*' FROM t", "SELECT match(x, 'ab.*') FROM t"),
(
"SELECT * FROM t WHERE name ~ '^A'",
"SELECT * FROM t WHERE match(name, '^A')",
),
];
for (input, expected) in cases {
assert_eq!(pg_ch(input), expected, "input: {input}");
}
}

#[test]
fn case_insensitive_regex_operator_uses_inline_flag() {
// A string-literal pattern gets the (?i) flag inlined for readable output.
assert_eq!(
pg_ch("SELECT x ~* 'ab.*' FROM t"),
"SELECT match(x, '(?i)ab.*') FROM t"
);
// A non-literal pattern falls back to concat() so the flag still applies.
assert_eq!(
pg_ch("SELECT x ~* y FROM t"),
"SELECT match(x, concat('(?i)', y)) FROM t"
);
}

#[test]
fn negated_regex_operators_map_to_not_match() {
assert_eq!(
pg_ch("SELECT x !~ 'ab.*' FROM t"),
"SELECT NOT (match(x, 'ab.*')) FROM t"
);
assert_eq!(
pg_ch("SELECT x !~* 'ab.*' FROM t"),
"SELECT NOT (match(x, '(?i)ab.*')) FROM t"
);
}

#[test]
fn mysql_rlike_also_maps_to_clickhouse_match() {
// The mapping is on the RegexpLike node, so RLIKE/REGEXP sources benefit too.
assert_eq!(
transpile(
"SELECT x RLIKE 'a' FROM t",
DialectType::MySQL,
DialectType::ClickHouse
)
.expect("MySQL -> ClickHouse transpilation should succeed")
.join("; "),
"SELECT match(x, 'a') FROM t"
);
}