Summary
When a ScalarArrayOpExpr with a constant empty array is pushed down, the deparser emits an empty tuple after IN/NOT IN, producing remote SQL like (col IN ()). Older ClickHouse servers reject this: ≤22.x with DB::Exception: Syntax error: failed at position N (')'): ... Expected ... (SYNTAX_ERROR), 23.x–24.x with Function tuple requires at least one argument (NUMBER_OF_ARGUMENTS_DOESNT_MATCH), 24.8 with Function 'in' is supported only if second argument is constant or table expression (UNSUPPORTED_METHOD). ClickHouse ≥ ~25.x accepts IN () with correct semantics, so the failure is version-dependent on the remote side.
All references below are pinned to commit 94256f0.
Trigger conditions
SELECT * FROM ft WHERE col = ANY('{}'::int[]); → remote (col IN ())
SELECT * FROM ft WHERE col <> ALL('{}'::int[]); → remote (col NOT IN ())
- Parameterized queries
WHERE col = ANY($1) with an empty-array bind, whenever PostgreSQL uses a custom plan (default for the first five executions of a prepared statement, and later whenever estimated cheaper): eval_const_expressions folds the extern Param into a Const, which then takes the IN path. Under a generic plan the same query takes the has({pN:Array(T)}, col) path with a server-side bound parameter and works — so the same application query fails intermittently depending on plan choice.
Code walkthrough
deparseScalarArrayOpExpr (src/deparse.c:3749) dispatches = ANY(Const) (line 3772) and <> ALL(Const) (line 3798) to deparseAsIn.
deparseAsIn (src/deparse.c:3726-3742) appends " IN "/" NOT IN ", sets context->array_as_tuple = true, and deparses the array Const.
deparseConst (src/deparse.c:2296-2298) routes array output to deparseArray (src/deparse.c:2157); the 1-D branch (lines 2196-2218) writes (, loops over nitems elements, writes ) — for a zero-element array this is exactly (). There is no empty-array guard here, unlike deparseArrayExpr (src/deparse.c:3922-3932), which CAST-wraps empty ARRAY[] expressions.
- Shippability never blocks it:
T_Const is unconditionally shippable (src/deparse.c:362) and the T_ScalarArrayOpExpr check (src/deparse.c:484-498) only validates the operator.
Note: a Param right-hand side does not hit this path (deparseAsIn requires IsA(arg2, Const)); params are sent as native ClickHouse {pN:Type} placeholders with param_pN form parts (src/deparse.c:2431-2438, src/http_streaming.c:167-173), so only plan-time constants are affected.
Observed remote error (older ClickHouse)
DB::Exception: Syntax error: failed at position N (')'): ... Expected one of: <list varies by version> (SYNTAX_ERROR)
with the position pointing at the ) of the empty IN () mid-statement (observed in production at stable mid-statement offsets, consistent with constant-folded application queries).
Impact
Every occurrence is a hard query failure surfaced through remote-error handling — the query never executes remotely even though the PostgreSQL-side semantics are trivial (= ANY('{}') is constant false, <> ALL('{}') constant true). Applications that build = ANY(array) filters from possibly-empty lists (a common ORM pattern) hit this whenever the empty case is plan-time constant and the remote server is older than ~25.x. There is currently no regression test covering empty arrays in ANY/ALL pushdown.
Summary
When a
ScalarArrayOpExprwith a constant empty array is pushed down, the deparser emits an empty tuple afterIN/NOT IN, producing remote SQL like(col IN ()). Older ClickHouse servers reject this: ≤22.x withDB::Exception: Syntax error: failed at position N (')'): ... Expected ...(SYNTAX_ERROR), 23.x–24.x withFunction tuple requires at least one argument(NUMBER_OF_ARGUMENTS_DOESNT_MATCH), 24.8 withFunction 'in' is supported only if second argument is constant or table expression(UNSUPPORTED_METHOD). ClickHouse ≥ ~25.x acceptsIN ()with correct semantics, so the failure is version-dependent on the remote side.All references below are pinned to commit
94256f0.Trigger conditions
SELECT * FROM ft WHERE col = ANY('{}'::int[]);→ remote(col IN ())SELECT * FROM ft WHERE col <> ALL('{}'::int[]);→ remote(col NOT IN ())WHERE col = ANY($1)with an empty-array bind, whenever PostgreSQL uses a custom plan (default for the first five executions of a prepared statement, and later whenever estimated cheaper):eval_const_expressionsfolds the extern Param into a Const, which then takes the IN path. Under a generic plan the same query takes thehas({pN:Array(T)}, col)path with a server-side bound parameter and works — so the same application query fails intermittently depending on plan choice.Code walkthrough
deparseScalarArrayOpExpr(src/deparse.c:3749) dispatches= ANY(Const)(line 3772) and<> ALL(Const)(line 3798) todeparseAsIn.deparseAsIn(src/deparse.c:3726-3742) appends" IN "/" NOT IN ", setscontext->array_as_tuple = true, and deparses the array Const.deparseConst(src/deparse.c:2296-2298) routes array output todeparseArray(src/deparse.c:2157); the 1-D branch (lines 2196-2218) writes(, loops overnitemselements, writes)— for a zero-element array this is exactly(). There is no empty-array guard here, unlikedeparseArrayExpr(src/deparse.c:3922-3932), which CAST-wraps emptyARRAY[]expressions.T_Constis unconditionally shippable (src/deparse.c:362) and theT_ScalarArrayOpExprcheck (src/deparse.c:484-498) only validates the operator.Note: a
Paramright-hand side does not hit this path (deparseAsInrequiresIsA(arg2, Const)); params are sent as native ClickHouse{pN:Type}placeholders withparam_pNform parts (src/deparse.c:2431-2438,src/http_streaming.c:167-173), so only plan-time constants are affected.Observed remote error (older ClickHouse)
with the position pointing at the
)of the emptyIN ()mid-statement (observed in production at stable mid-statement offsets, consistent with constant-folded application queries).Impact
Every occurrence is a hard query failure surfaced through remote-error handling — the query never executes remotely even though the PostgreSQL-side semantics are trivial (
= ANY('{}')is constant false,<> ALL('{}')constant true). Applications that build= ANY(array)filters from possibly-empty lists (a common ORM pattern) hit this whenever the empty case is plan-time constant and the remote server is older than ~25.x. There is currently no regression test covering empty arrays in ANY/ALL pushdown.