Fix PREPARE in multi-statement case.
authorTatsuo Ishii <ishii@sraoss.co.jp>
Wed, 12 Jul 2023 07:35:46 +0000 (16:35 +0900)
committerTatsuo Ishii <ishii@sraoss.co.jp>
Thu, 13 Jul 2023 02:19:37 +0000 (11:19 +0900)
If multi-statement query includes PREPARE in the second or latter
position, and subsequent bind message uses the prepared statement, it
fails with "unable to bind" error because the prepared statement is not
saved in sent messages.

To fix this if such a case found after parsing the statement, create a
query context for the named statement and add it to the sent message
list.
Discussion: https://www.pgpool.net/pipermail/pgpool-general/2023-July/008931.html

For this new regression test 079..multi_prepare is added.

src/protocol/pool_proto_modules.c
src/test/regression/tests/079.multi_prepare/expected.txt [new file with mode: 0644]
src/test/regression/tests/079.multi_prepare/pgproto.data [new file with mode: 0644]
src/test/regression/tests/079.multi_prepare/test.sh [new file with mode: 0755]

index 751c3da5c03210f7a2c03f12ffaa07f586366ecc..b1ed1c499b5c8eac50b8a4a863dcc5804bd3ad0a 100644 (file)
@@ -110,6 +110,8 @@ static bool check_transaction_state_and_abort(char *query, Node *node, POOL_CONN
 
 static bool multi_statement_query(char *buf);
 
+static void check_prepare(List *parse_tree_list, int len, char *contents);
+
 /*
  * This is the workhorse of processing the pg_terminate_backend function to
  * make sure that the use of function should not trigger the backend node failover.
@@ -348,6 +350,20 @@ SimpleQuery(POOL_CONNECTION * frontend,
                        query_context->is_parse_error = true;
                }
        }
+
+       if (query_context->is_multi_statement)
+       {
+               /*
+                * Check parse tree list and if it includes PREPARE statement in the
+                * second or subsequent parse tree, create "sent_message" entry so
+                * that bind message can find them later on (PREPARE is usually
+                * executed by EXECUTE, but it's possible that a client feels free to
+                * use PREPARE then bind and execute messages).  If PREPARE is in the
+                * first parse tree, it will be processed subsequent code path.
+                */
+               check_prepare(parse_tree_list, len, contents);
+       }
+
        MemoryContextSwitchTo(old_context);
 
        if (parse_tree_list != NIL)
@@ -4829,3 +4845,46 @@ bool multi_statement_query(char *queries)
 
        return num_semicolons > 1;
 }
+
+/*
+ * Check given parse tree list and if it is a multi statement and includes
+ * PREPARE statement in the second or subsequent parse tree, create
+ * "sent_message" entry so that bind message can find them later on.
+ * parse_tree_list: raw parse tree list (list of RawStmt)
+ * len: full query string length
+ * contents: full query string
+ */
+static void
+check_prepare(List *parse_tree_list, int len, char *contents)
+{
+       Node                            *node;
+       RawStmt                         *rstmt;
+       POOL_QUERY_CONTEXT      *query_context;
+       ListCell                        *l;
+       POOL_SENT_MESSAGE       *message;
+
+       /* sanity check */
+       if (list_length(parse_tree_list) <= 1)
+               return;
+
+       foreach (l, parse_tree_list)
+       {
+               if (l == list_head(parse_tree_list))    /* skip the first parse tree */
+                       continue;
+
+               rstmt = (RawStmt *) lfirst(l);
+               node = (Node *) rstmt->stmt;    /* pick one parse tree */
+
+               if (!IsA(node, PrepareStmt))    /* PREPARE? */
+                       continue;
+
+               query_context = pool_init_query_context();      /* initialize query context */
+               query_context->is_multi_statement = true;       /* this is a multi statement query */
+               pool_start_query(query_context, contents, len, node);   /* start query context */
+               pool_where_to_send(query_context, query_context->original_query,        /* set query destination */
+                                                  query_context->parse_tree);
+               message = pool_create_sent_message('Q', len, contents, 0,       /* create sent message */
+                                                                                  ((PrepareStmt *) node)->name, query_context);
+               pool_add_sent_message(message); /* add it to the sent message list */
+       }
+}
diff --git a/src/test/regression/tests/079.multi_prepare/expected.txt b/src/test/regression/tests/079.multi_prepare/expected.txt
new file mode 100644 (file)
index 0000000..58ac94f
--- /dev/null
@@ -0,0 +1,23 @@
+FE=> Query (query="PREPARE mark_rels_by_node(int8) AS SELECT $1;PREPARE mark_rels_by_way(int8) AS SELECT $1")
+<= BE CommandComplete(PREPARE)
+<= BE CommandComplete(PREPARE)
+<= BE ReadyForQuery(I)
+FE=> Bind(stmt="mark_rels_by_node", portal="")
+FE=> Describe(portal="")
+FE=> Execute(portal="")
+FE=> Sync
+<= BE BindComplete
+<= BE RowDescription
+<= BE DataRow
+<= BE CommandComplete(SELECT 1)
+<= BE ReadyForQuery(I)
+FE=> Bind(stmt="mark_rels_by_way", portal="")
+FE=> Describe(portal="")
+FE=> Execute(portal="")
+FE=> Sync
+<= BE BindComplete
+<= BE RowDescription
+<= BE DataRow
+<= BE CommandComplete(SELECT 1)
+<= BE ReadyForQuery(I)
+FE=> Terminate
diff --git a/src/test/regression/tests/079.multi_prepare/pgproto.data b/src/test/regression/tests/079.multi_prepare/pgproto.data
new file mode 100644 (file)
index 0000000..aeef45b
--- /dev/null
@@ -0,0 +1,16 @@
+'Q'    "PREPARE mark_rels_by_node(int8) AS SELECT $1;PREPARE mark_rels_by_way(int8) AS SELECT $1"
+'Y'
+# portal statement_name num_format_code (0 is text) num_params param_length "param"
+# num_return_value_format_code return_value_format_code
+'B'    ""      "mark_rels_by_node"     0       1       1       "2"     1       0
+'D'    'P'     ""
+'E'    ""      0
+'S'
+'Y'
+# error was: unable to bind D cannot get parse message "mark_rels_by_way"
+'B'    ""      "mark_rels_by_way"      0       1       1       "1"     1       0
+'D'    'P'     ""
+'E'    ""      0
+'S'
+'Y'
+'X'
diff --git a/src/test/regression/tests/079.multi_prepare/test.sh b/src/test/regression/tests/079.multi_prepare/test.sh
new file mode 100755 (executable)
index 0000000..970fca6
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------
+# Test script for multi statement query including PREPARE *and* bind is used later on.
+# Discussion: [pgpool-general: 8870] Prepared statements over pgpool ?
+#
+source $TESTLIBS
+TESTDIR=testdir
+PSQL=$PGBIN/psql
+PG_CTL=$PGBIN/pg_ctl
+PGPROTO=$PGPOOL_INSTALL_DIR/bin/pgproto
+export PGDATABASE=test
+
+#for mode in s
+for mode in s i r n
+do
+    rm -fr $TESTDIR
+    mkdir $TESTDIR
+    cd $TESTDIR
+
+    echo -n "creating test environment..."
+    $PGPOOL_SETUP -m $mode || exit 1
+    echo "done."
+    source ./bashrc.ports
+    ./startall
+    wait_for_pgpool_startup
+
+    $PGPROTO -d $PGDATABASE -p $PGPOOL_PORT -f ../pgproto.data > results.txt 2>&1
+    cmp ../expected.txt results.txt
+
+    if [ $? != 0 ];then
+       echo "test failed in mode: $mode".
+       ./shutdownall
+       exit 1
+    fi
+    ./shutdownall
+    cd ..
+done
+
+exit 0