diff --git a/src/custom_types.c b/src/custom_types.c index 76942c64..e9fb6189 100644 --- a/src/custom_types.c +++ b/src/custom_types.c @@ -489,14 +489,16 @@ lookup_builtin_func(Oid funcid, builtin_func_def* def) { case F_SUBSTR_TEXT_INT4: case F_SUBSTRING_TEXT_INT4_INT4: case F_SUBSTRING_TEXT_INT4: - def->ch_name = "substringUTF8"; + def->cf_type = CF_SUBSTRING; + def->ch_name = "\1"; return true; /* PG substring(text, ...) counts code points */ case F_SUBSTR_BYTEA_INT4_INT4: case F_SUBSTR_BYTEA_INT4: case F_SUBSTRING_BYTEA_INT4_INT4: case F_SUBSTRING_BYTEA_INT4: - def->ch_name = "substring"; + def->cf_type = CF_SUBSTRING; + def->ch_name = "\1"; return true; /* bytea variant is byte-based; CH substring matches */ case F_REGEXP_LIKE_TEXT_TEXT: diff --git a/src/deparse.c b/src/deparse.c index e6826028..a00f1150 100644 --- a/src/deparse.c +++ b/src/deparse.c @@ -277,6 +277,8 @@ static void deparseArrayExpr(ArrayExpr* node, deparse_expr_cxt* context); static void deparseArrayList(ArrayExpr* node, deparse_expr_cxt* context); +static char* +deparseExprToString(Expr* expr, deparse_expr_cxt* context); static void printRemoteParam( int paramindex, @@ -4718,6 +4720,84 @@ deparseFuncExpr(FuncExpr* node, deparse_expr_cxt* context) { pfree(format); return; } + case CF_SUBSTRING: { + Expr* value = (Expr*)linitial(node->args); + Expr* start = (Expr*)lsecond(node->args); + bool safe_bounds; + const char* function_name = + node->funcresulttype == BYTEAOID ? "substring" : "substringUTF8"; + char* valuesql = deparseExprToString(value, context); + char* startsql = deparseExprToString(start, context); + char* adjusted_start_sql; + + Assert(list_length(node->args) == 2 || list_length(node->args) == 3); + + safe_bounds = IsA(start, Const) && !((Const*)start)->constisnull && + DatumGetInt32(((Const*)start)->constvalue) >= 1; + if (safe_bounds && list_length(node->args) == 3) { + Expr* length = (Expr*)lthird(node->args); + + safe_bounds = IsA(length, Const) && !((Const*)length)->constisnull && + DatumGetInt32(((Const*)length)->constvalue) >= 0; + } + + if (safe_bounds) { + appendStringInfo(buf, "%s(%s, %s", function_name, valuesql, startsql); + if (list_length(node->args) == 3) { + appendStringInfoString(buf, ", "); + deparseExpr((Expr*)lthird(node->args), context); + } + appendStringInfoChar(buf, ')'); + + pfree(startsql); + pfree(valuesql); + return; + } + + /* ClickHouse treats negative starts as offsets from the end. */ + adjusted_start_sql = psprintf("if(%1$s < 1, 1, %1$s)", startsql); + + if (list_length(node->args) == 2) { + appendStringInfo( + buf, "%s(%s, %s)", function_name, valuesql, adjusted_start_sql + ); + } else { + Expr* length = (Expr*)lthird(node->args); + char* lengthsql = deparseExprToString(length, context); + char* adjusted_length_sql = psprintf( + "if(toInt64(%2$s) + toInt64(%1$s) > 2147483647, 2147483647, " + "if(toInt64(%2$s) + toInt64(%1$s) < 1, 0, " + "toInt64(%2$s) + toInt64(%1$s) - %3$s))", + lengthsql, + startsql, + adjusted_start_sql + ); + + /* PostgreSQL's substring functions are strict. */ + appendStringInfo( + buf, + "if(throwIf(isNotNull(%s) AND isNotNull(%s) AND isNotNull(%s) " + "AND %s < 0, 'negative substring length not allowed'), NULL, " + "%s(%s, %s, %s))", + valuesql, + startsql, + lengthsql, + lengthsql, + function_name, + valuesql, + adjusted_start_sql, + adjusted_length_sql + ); + + pfree(adjusted_length_sql); + pfree(lengthsql); + } + + pfree(adjusted_start_sql); + pfree(startsql); + pfree(valuesql); + return; + } default: break; } diff --git a/src/include/fdw.h b/src/include/fdw.h index c63afa8d..f4858b88 100644 --- a/src/include/fdw.h +++ b/src/include/fdw.h @@ -446,6 +446,7 @@ typedef enum { CF_PARAM_LIST_AGG, /* ordered set agg with array prams → parametric * list aggregate function */ CF_ENCODE, /* encode(bytea, fmt) → hex/base64 family */ + CF_SUBSTRING, /* substring/substr with PostgreSQL-compatible bounds */ } custom_object_type; typedef enum { diff --git a/test/expected/functions.out b/test/expected/functions.out index 61e1d39c..3c88fbe1 100644 --- a/test/expected/functions.out +++ b/test/expected/functions.out @@ -2700,6 +2700,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_0.out b/test/expected/functions_0.out index 05f94594..9a3d565f 100644 --- a/test/expected/functions_0.out +++ b/test/expected/functions_0.out @@ -2700,6 +2700,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_1.out b/test/expected/functions_1.out index 733a6474..65eeef2f 100644 --- a/test/expected/functions_1.out +++ b/test/expected/functions_1.out @@ -2700,6 +2700,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_2.out b/test/expected/functions_2.out index 89433525..ccc263dd 100644 --- a/test/expected/functions_2.out +++ b/test/expected/functions_2.out @@ -2700,6 +2700,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_3.out b/test/expected/functions_3.out index 8a0a802c..a05fea6e 100644 --- a/test/expected/functions_3.out +++ b/test/expected/functions_3.out @@ -2688,6 +2688,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_4.out b/test/expected/functions_4.out index 15b9f691..f3e9a510 100644 --- a/test/expected/functions_4.out +++ b/test/expected/functions_4.out @@ -2696,6 +2696,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_5.out b/test/expected/functions_5.out index 283a2d82..a53254f7 100644 --- a/test/expected/functions_5.out +++ b/test/expected/functions_5.out @@ -2684,6 +2684,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_6.out b/test/expected/functions_6.out index a3095973..01a8de94 100644 --- a/test/expected/functions_6.out +++ b/test/expected/functions_6.out @@ -2684,6 +2684,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_7.out b/test/expected/functions_7.out index d4ab979b..b623f87d 100644 --- a/test/expected/functions_7.out +++ b/test/expected/functions_7.out @@ -2674,6 +2674,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_8.out b/test/expected/functions_8.out index 82b41789..e438653a 100644 --- a/test/expected/functions_8.out +++ b/test/expected/functions_8.out @@ -2658,6 +2658,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/expected/functions_9.out b/test/expected/functions_9.out index b2de027e..8b4582dd 100644 --- a/test/expected/functions_9.out +++ b/test/expected/functions_9.out @@ -2651,6 +2651,86 @@ SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; val1 (1 row) +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if((-1) < 1, 1, (-1))) = 'val1')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; + val +------ + val1 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(0) AND isNotNull(2) AND 2 < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(0 < 1, 1, 0), if(toInt64(0) + toInt64(2) > 2147483647, 2147483647, if(toInt64(0) + toInt64(2) < 1, 0, toInt64(0) + toInt64(2) - if(0 < 1, 1, 0))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; + val +------ + val1 + val2 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(CAST(val AS bytea(0))) AND isNotNull((-1)) AND isNotNull(3) AND 3 < 0, 'negative substring length not allowed'), NULL, substring(CAST(val AS bytea(0)), if((-1) < 1, 1, (-1)), if(toInt64((-1)) + toInt64(3) > 2147483647, 2147483647, if(toInt64((-1)) + toInt64(3) < 1, 0, toInt64((-1)) + toInt64(3) - if((-1) < 1, 1, (-1)))))) = 'v')) +(3 rows) + +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + val +------ + val1 + val2 +(2 rows) + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((if(throwIf(isNotNull(val) AND isNotNull(2) AND isNotNull((-1)) AND (-1) < 0, 'negative substring length not allowed'), NULL, substringUTF8(val, if(2 < 1, 1, 2), if(toInt64(2) + toInt64((-1)) > 2147483647, 2147483647, if(toInt64(2) + toInt64((-1)) < 1, 0, toInt64(2) + toInt64((-1)) - if(2 < 1, 1, 2))))) = 'never')) +(3 rows) + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Foreign Scan on public.t4 + Output: val + Remote SQL: SELECT val FROM functions_test.t4 WHERE ((substringUTF8(val, if({p1:Int32} < 1, 1, {p1:Int32})) = 'al1')) +(3 rows) + +EXECUTE substring_offset(2); + val +------ + val1 +(1 row) + +DEALLOCATE substring_offset; +RESET plan_cache_mode; -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3; diff --git a/test/sql/functions.sql b/test/sql/functions.sql index 74b467b7..933f1293 100644 --- a/test/sql/functions.sql +++ b/test/sql/functions.sql @@ -719,6 +719,32 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; SELECT val FROM t4 WHERE substr(val::bytea, 2) = 'al1'::bytea; +-- Nonpositive or dynamic substring bounds push down with PostgreSQL-compatible +-- normalization. PostgreSQL clamps the start to 1, while ClickHouse counts +-- negative starts from the end. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; +SELECT val FROM t4 WHERE substring(val FROM -1) = 'val1'; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; +SELECT val FROM t4 WHERE substring(val FROM 0 FOR 2) = 'v'; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; +SELECT val FROM t4 WHERE substring(val::bytea FROM -1 FOR 3) = 'v'::bytea; + +-- PostgreSQL errors for negative lengths; preserve that behavior remotely. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT val FROM t4 WHERE substring(val FROM 2 FOR -1) = 'never'; + +-- A parameterized offset also uses the remote normalization. +SET plan_cache_mode = force_generic_plan; +PREPARE substring_offset(int) AS + SELECT val FROM t4 WHERE substring(val FROM $1) = 'al1'; +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE substring_offset(2); +EXECUTE substring_offset(2); +DEALLOCATE substring_offset; +RESET plan_cache_mode; + -- length(text) pushes down as lengthUTF8 (counts code points). EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM t4 WHERE length(val) = 3;