From 09b2525cf755cf3de3ad211dc2e3a20ac44c0a49 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Wed, 5 Aug 2026 18:23:57 +0200 Subject: [PATCH] Preserve PostgreSQL substring bounds during pushdown ClickHouse interprets non-positive substring starts and negative lengths differently from PostgreSQL. Directly pushing down substring() and substr() can therefore change results or suppress PostgreSQL's negative-length error. Deparse text and bytea substring calls with PostgreSQL-compatible start and length adjustments, preserving NULL strictness and raising the PostgreSQL error for negative lengths. Keep safe constant bounds on the direct ClickHouse path. Add regression coverage for text and bytea offsets, negative lengths, NULL arguments, and generic plans. --- src/custom_types.c | 6 ++- src/deparse.c | 80 +++++++++++++++++++++++++++++++++++ src/include/fdw.h | 1 + test/expected/functions.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_0.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_1.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_2.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_3.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_4.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_5.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_6.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_7.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_8.out | 80 +++++++++++++++++++++++++++++++++++ test/expected/functions_9.out | 80 +++++++++++++++++++++++++++++++++++ test/sql/functions.sql | 26 ++++++++++++ 15 files changed, 991 insertions(+), 2 deletions(-) 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;