Skip to content

Commit

Permalink
Correct handling parameter (#491)
Browse files Browse the repository at this point in the history
DuckDB complains when it receives parameters that were not actually used
in the query. From Postgres we get all supplied parameters though. So
this changes the code to filter the unused ones out. To do this we lot
DuckDB parse the query and tell us which parameters it expects, and then
we remove the unexpected ones.

Fixes #411 
Fixes #234

Co-authored-by: Zhou Sun <[email protected]>
  • Loading branch information
Leo-XM-Zeng and zhousun authored Dec 16, 2024
1 parent 44b292a commit 04be407
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 4 deletions.
15 changes: 11 additions & 4 deletions src/pgduckdb_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ ExecuteQuery(DuckdbScanState *state) {
auto &connection = state->duckdb_connection;
auto pg_params = state->params;
const auto num_params = pg_params ? pg_params->numParams : 0;
duckdb::vector<duckdb::Value> duckdb_params;
duckdb::case_insensitive_map_t<duckdb::BoundParameterData> named_values;

for (int i = 0; i < num_params; i++) {
ParamExternData *pg_param;
ParamExternData tmp_workspace;
duckdb::Value duckdb_param;

/* give hook a chance in case parameter is dynamic */
if (pg_params->paramFetch != NULL) {
Expand All @@ -113,18 +115,23 @@ ExecuteQuery(DuckdbScanState *state) {
pg_param = &pg_params->params[i];
}

if (prepared.named_param_map.count(duckdb::to_string(i + 1)) == 0){
continue;
}

if (pg_param->isnull) {
duckdb_params.push_back(duckdb::Value());
duckdb_param = duckdb::Value();
} else if (OidIsValid(pg_param->ptype)) {
duckdb_params.push_back(pgduckdb::ConvertPostgresParameterToDuckValue(pg_param->value, pg_param->ptype));
duckdb_param = pgduckdb::ConvertPostgresParameterToDuckValue(pg_param->value, pg_param->ptype);
} else {
std::ostringstream oss;
oss << "parameter '" << i << "' has an invalid type (" << pg_param->ptype << ") during query execution";
throw duckdb::Exception(duckdb::ExceptionType::EXECUTOR, oss.str().c_str());
}
named_values[duckdb::to_string(i + 1)] = duckdb::BoundParameterData(duckdb_param);
}

auto pending = prepared.PendingQuery(duckdb_params, true);
auto pending = prepared.PendingQuery(named_values, true);
if (pending->HasError()) {
return pending->ThrowError();
}
Expand Down
76 changes: 76 additions & 0 deletions test/regression/expected/function.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
CREATE TABLE ta(a integer);
INSERT INTO ta VALUES(125);
-- test procedure
CREATE OR REPLACE PROCEDURE protest(b INT)
LANGUAGE plpgsql
AS $$
DECLARE
va integer;
BEGIN
SELECT * INTO va FROM ta WHERE a = b;
RAISE NOTICE '%', va * 2;
END;
$$;
CALL protest(1); -- null
NOTICE: <NULL>
CALL protest(125); -- 250
NOTICE: 250
-- test function
CREATE OR REPLACE FUNCTION functest(b INT)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
va integer;
BEGIN
SELECT * INTO va FROM ta WHERE a = b;
RETURN va * 2;
END;
$$;
SELECT functest(124); -- null
functest
----------

(1 row)

SELECT functest(125); -- 250
functest
----------
250
(1 row)

CREATE TABLE tb(a int DEFAULT 1, b text, c varchar DEFAULT 'pg_duckdb');
INSERT INTO tb(a, b) VALUES(1, 'test');
INSERT INTO tb VALUES(2, 'test2', 'pg_duckdb_test');
CREATE OR REPLACE FUNCTION functest2(va INT, vc varchar)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
vb text;
BEGIN
SELECT b INTO vb FROM tb WHERE a = va and c = vc;
RETURN vb;
END;
$$;
SELECT functest2(1, 'pg_duckdb'); -- test
functest2
-----------
test
(1 row)

CREATE OR REPLACE PROCEDURE protest2(va INT, vb text)
LANGUAGE plpgsql
AS $$
DECLARE
vc varchar;
BEGIN
SELECT c INTO vc FROM tb WHERE a = va and b = vb;
RAISE NOTICE '%', vc;
END;
$$;
CALL protest2(2, 'test2'); -- pg_duckdb_test
NOTICE: pg_duckdb_test
DROP TABLE ta, tb;
DROP FUNCTION functest, functest2;
DROP PROCEDURE protest, protest2;
206 changes: 206 additions & 0 deletions test/regression/expected/prepare.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
-- the view should return the empty
SELECT name, statement, parameter_types FROM pg_prepared_statements;
name | statement | parameter_types
------+-----------+-----------------
(0 rows)

CREATE TABLE copy_database(datname text, datistemplate boolean, datallowconn boolean);
INSERT INTO copy_database SELECT datname, datistemplate, datallowconn FROM pg_database;
-- parameterized queries
PREPARE q1(text) AS
SELECT datname, datistemplate, datallowconn
FROM copy_database WHERE datname = $1;
EXECUTE q1('postgres');
datname | datistemplate | datallowconn
----------+---------------+--------------
postgres | f | t
(1 row)

-- q1
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+-----------------------------------------------------+-----------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn+|
| FROM copy_database WHERE datname = $1; |
(1 row)

CREATE TABLE ta(a int);
INSERT INTO ta(a) SELECT * FROM generate_series(1, 1000);
SELECT COUNT(*) FROM ta;
count
-------
1000
(1 row)

PREPARE q2 AS SELECT COUNT(*) FROM ta;
EXECUTE q2;
count
-------
1000
(1 row)

-- q1 q2
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+-----------------------------------------------------+-----------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn+|
| FROM copy_database WHERE datname = $1; |
q2 | PREPARE q2 AS SELECT COUNT(*) FROM ta; | {}
(2 rows)

PREPARE q3(int) AS
SELECT * FROM ta WHERE a = $1;
CREATE TEMPORARY TABLE q3_prep_results AS EXECUTE q3(200);
SELECT * FROM q3_prep_results;
a
-----
200
(1 row)

-- q1 q2 q3
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+-----------------------------------------------------+-----------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn+|
| FROM copy_database WHERE datname = $1; |
q2 | PREPARE q2 AS SELECT COUNT(*) FROM ta; | {}
q3 | PREPARE q3(int) AS +| {integer}
| SELECT * FROM ta WHERE a = $1; |
(3 rows)

CREATE TABLE tb (a int DEFAULT 1, b int, c varchar DEFAULT 'pg_duckdb');
INSERT INTO tb(b) VALUES(2);
PREPARE q4(int, varchar) AS
SELECT b FROM tb WHERE a = $1 AND c = $2;
EXECUTE q4(1, 'pg_duckdb');
b
---
2
(1 row)

EXECUTE q4(1, 'pg_duckdb');
b
---
2
(1 row)

-- q1 q2 q3 q4
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+-----------------------------------------------------+-------------------------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn+|
| FROM copy_database WHERE datname = $1; |
q2 | PREPARE q2 AS SELECT COUNT(*) FROM ta; | {}
q3 | PREPARE q3(int) AS +| {integer}
| SELECT * FROM ta WHERE a = $1; |
q4 | PREPARE q4(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND c = $2; |
(4 rows)

TRUNCATE tb;
INSERT INTO tb VALUES(10, 10, 'test');
INSERT INTO tb VALUES(100, 100, 'pg_duckdb');
PREPARE q5(int, varchar) AS
SELECT b FROM tb WHERE a = $1 AND b = $1 AND c = $2;
EXECUTE q5(10, 'test');
b
----
10
(1 row)

EXECUTE q5(100, 'pg_duckdb');
b
-----
100
(1 row)

-- q1 q2 q3 q4 q5
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+--------------------------------------------------------------+-------------------------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn +|
| FROM copy_database WHERE datname = $1; |
q2 | PREPARE q2 AS SELECT COUNT(*) FROM ta; | {}
q3 | PREPARE q3(int) AS +| {integer}
| SELECT * FROM ta WHERE a = $1; |
q4 | PREPARE q4(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND c = $2; |
q5 | PREPARE q5(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND b = $1 AND c = $2; |
(5 rows)

PREPARE q6(int, varchar) AS
SELECT b FROM tb WHERE c = $2;
EXECUTE q6(10, 'test');
b
----
10
(1 row)

-- q1 q2 q3 q4 q5 q6
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+--------------------------------------------------------------+-------------------------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn +|
| FROM copy_database WHERE datname = $1; |
q2 | PREPARE q2 AS SELECT COUNT(*) FROM ta; | {}
q3 | PREPARE q3(int) AS +| {integer}
| SELECT * FROM ta WHERE a = $1; |
q4 | PREPARE q4(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND c = $2; |
q5 | PREPARE q5(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND b = $1 AND c = $2; |
q6 | PREPARE q6(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE c = $2; |
(6 rows)

PREPARE q7(int, varchar) AS
SELECT b FROM tb WHERE a = $1;
EXECUTE q7(100, 'pg_duckdb');
b
-----
100
(1 row)

-- q1 q2 q3 q4 q5 q6 q7
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+--------------------------------------------------------------+-------------------------------
q1 | PREPARE q1(text) AS +| {text}
| SELECT datname, datistemplate, datallowconn +|
| FROM copy_database WHERE datname = $1; |
q2 | PREPARE q2 AS SELECT COUNT(*) FROM ta; | {}
q3 | PREPARE q3(int) AS +| {integer}
| SELECT * FROM ta WHERE a = $1; |
q4 | PREPARE q4(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND c = $2; |
q5 | PREPARE q5(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1 AND b = $1 AND c = $2; |
q6 | PREPARE q6(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE c = $2; |
q7 | PREPARE q7(int, varchar) AS +| {integer,"character varying"}
| SELECT b FROM tb WHERE a = $1; |
(7 rows)

-- test DEALLOCATE ALL;
DEALLOCATE ALL;
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
name | statement | parameter_types
------+-----------+-----------------
(0 rows)

DROP TABLE copy_database, ta, tb;
2 changes: 2 additions & 0 deletions test/regression/schedule
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ test: altered_tables
test: transactions
test: transaction_errors
test: secrets
test: prepare
test: function
68 changes: 68 additions & 0 deletions test/regression/sql/function.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
CREATE TABLE ta(a integer);
INSERT INTO ta VALUES(125);

-- test procedure
CREATE OR REPLACE PROCEDURE protest(b INT)
LANGUAGE plpgsql
AS $$
DECLARE
va integer;
BEGIN
SELECT * INTO va FROM ta WHERE a = b;
RAISE NOTICE '%', va * 2;
END;
$$;

CALL protest(1); -- null
CALL protest(125); -- 250

-- test function
CREATE OR REPLACE FUNCTION functest(b INT)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
va integer;
BEGIN
SELECT * INTO va FROM ta WHERE a = b;
RETURN va * 2;
END;
$$;

SELECT functest(124); -- null
SELECT functest(125); -- 250

CREATE TABLE tb(a int DEFAULT 1, b text, c varchar DEFAULT 'pg_duckdb');
INSERT INTO tb(a, b) VALUES(1, 'test');
INSERT INTO tb VALUES(2, 'test2', 'pg_duckdb_test');

CREATE OR REPLACE FUNCTION functest2(va INT, vc varchar)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
vb text;
BEGIN
SELECT b INTO vb FROM tb WHERE a = va and c = vc;
RETURN vb;
END;
$$;

SELECT functest2(1, 'pg_duckdb'); -- test

CREATE OR REPLACE PROCEDURE protest2(va INT, vb text)
LANGUAGE plpgsql
AS $$
DECLARE
vc varchar;
BEGIN
SELECT c INTO vc FROM tb WHERE a = va and b = vb;
RAISE NOTICE '%', vc;
END;
$$;

CALL protest2(2, 'test2'); -- pg_duckdb_test

DROP TABLE ta, tb;
DROP FUNCTION functest, functest2;
DROP PROCEDURE protest, protest2;
Loading

0 comments on commit 04be407

Please sign in to comment.