Fix #7242, CALL(@0) crash backend (#7288)

When executing a prepared CALL, which is not pure SQL but available with
some drivers like npgsql and jpgdbc, Citus entered a code path where a
plan is not defined, while trying to increase its cost. Thus SIG11 when
plan is a NULL pointer.

Fix by only increasing plan cost when plan is not null.

However, it is a bit suspicious to get here with a NULL plan and maybe a
better change will be to not call
ShardPlacementForFunctionColocatedWithDistTable() with a NULL plan at
all (in call.c:134)

bug hit with for example:
```
CallableStatement proc = con.prepareCall("{CALL p(?)}");
proc.registerOutParameter(1, java.sql.Types.BIGINT);
proc.setInt(1, -100);
proc.execute();
```

where `p(bigint)` is a distributed "function" and the param the
distribution key (also in a distributed table), see #7242 for details

Fixes #7242

(cherry picked from commit 0678a2fd89)
pull/7989/head
Cédric Villemain 2023-11-02 13:15:24 +01:00 committed by naisila
parent 5ec2501e7b
commit 6ddd862e23
4 changed files with 49 additions and 2 deletions

View File

@ -703,6 +703,7 @@ DissuadePlannerFromUsingPlan(PlannedStmt *plan)
* Arbitrarily high cost, but low enough that it can be added up * Arbitrarily high cost, but low enough that it can be added up
* without overflowing by choose_custom_plan(). * without overflowing by choose_custom_plan().
*/ */
Assert(plan != NULL);
plan->planTree->total_cost = FLT_MAX / 100000000; plan->planTree->total_cost = FLT_MAX / 100000000;
} }

View File

@ -531,8 +531,16 @@ ShardPlacementForFunctionColocatedWithDistTable(DistObjectCacheEntry *procedure,
if (partitionParam->paramkind == PARAM_EXTERN) if (partitionParam->paramkind == PARAM_EXTERN)
{ {
/* Don't log a message, we should end up here again without a parameter */ /*
DissuadePlannerFromUsingPlan(plan); * Don't log a message, we should end up here again without a
* parameter.
* Note that "plan" can be null, for example when a CALL statement
* is prepared.
*/
if (plan)
{
DissuadePlannerFromUsingPlan(plan);
}
return NULL; return NULL;
} }
} }

View File

@ -581,6 +581,14 @@ class QueryRunner(ABC):
with self.cur(**kwargs) as cur: with self.cur(**kwargs) as cur:
cur.execute(query, params=params) cur.execute(query, params=params)
def sql_prepared(self, query, params=None, **kwargs):
"""Run an SQL query, with prepare=True
This opens a new connection and closes it once the query is done
"""
with self.cur(**kwargs) as cur:
cur.execute(query, params=params, prepare=True)
def sql_row(self, query, params=None, allow_empty_result=False, **kwargs): def sql_row(self, query, params=None, allow_empty_result=False, **kwargs):
"""Run an SQL query that returns a single row and returns this row """Run an SQL query that returns a single row and returns this row

View File

@ -0,0 +1,30 @@
def test_call_param(cluster):
# create a distributed table and an associated distributed procedure
# to ensure parameterized CALL succeed, even when the param is the
# distribution key.
coord = cluster.coordinator
coord.sql("CREATE TABLE test(i int)")
coord.sql(
"""
CREATE PROCEDURE p(_i INT) LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO test(i) VALUES (_i);
END; $$
"""
)
sql = "CALL p(%s)"
# prepare/exec before distributing
coord.sql_prepared(sql, (1,))
coord.sql("SELECT create_distributed_table('test', 'i')")
coord.sql(
"SELECT create_distributed_function('p(int)', distribution_arg_name := '_i', colocate_with := 'test')"
)
# prepare/exec after distribution
coord.sql_prepared(sql, (2,))
sum_i = coord.sql_value("select sum(i) from test;")
assert sum_i == 3