diff --git a/ext/gvl_wrappers.c b/ext/gvl_wrappers.c index 3a1dae88..ba1daa60 100644 --- a/ext/gvl_wrappers.c +++ b/ext/gvl_wrappers.c @@ -5,6 +5,14 @@ #include "pg.h" + +#ifdef HAVE_PQSETCHUNKEDROWSMODE +PGresult *PQclosePrepared(PGconn *conn, const char *stmtName){return NULL;} +PGresult *PQclosePortal(PGconn *conn, const char *portalName){return NULL;} +int PQsendClosePrepared(PGconn *conn, const char *stmtName){return 0;} +int PQsendClosePortal(PGconn *conn, const char *portalName){return 0;} +#endif + #ifdef ENABLE_GVL_UNLOCK FOR_EACH_BLOCKING_FUNCTION( DEFINE_GVL_WRAPPER_STRUCT ); FOR_EACH_BLOCKING_FUNCTION( DEFINE_GVL_SKELETON ); diff --git a/ext/gvl_wrappers.h b/ext/gvl_wrappers.h index ce226c8b..0c2c5b84 100644 --- a/ext/gvl_wrappers.h +++ b/ext/gvl_wrappers.h @@ -149,6 +149,12 @@ #define FOR_EACH_PARAM_OF_PQdescribePortal(param) \ param(PGconn *, conn) +#define FOR_EACH_PARAM_OF_PQclosePrepared(param) \ + param(PGconn *, conn) + +#define FOR_EACH_PARAM_OF_PQclosePortal(param) \ + param(PGconn *, conn) + #define FOR_EACH_PARAM_OF_PQgetResult(param) #define FOR_EACH_PARAM_OF_PQputCopyData(param) \ @@ -196,6 +202,12 @@ #define FOR_EACH_PARAM_OF_PQsendDescribePortal(param) \ param(PGconn *, conn) +#define FOR_EACH_PARAM_OF_PQsendClosePrepared(param) \ + param(PGconn *, conn) + +#define FOR_EACH_PARAM_OF_PQsendClosePortal(param) \ + param(PGconn *, conn) + #define FOR_EACH_PARAM_OF_PQsetClientEncoding(param) \ param(PGconn *, conn) @@ -225,6 +237,8 @@ function(PQprepare, GVL_TYPE_NONVOID, PGresult *, const Oid *, paramTypes) \ function(PQdescribePrepared, GVL_TYPE_NONVOID, PGresult *, const char *, stmtName) \ function(PQdescribePortal, GVL_TYPE_NONVOID, PGresult *, const char *, portalName) \ + function(PQclosePrepared, GVL_TYPE_NONVOID, PGresult *, const char *, stmtName) \ + function(PQclosePortal, GVL_TYPE_NONVOID, PGresult *, const char *, portalName) \ function(PQgetResult, GVL_TYPE_NONVOID, PGresult *, PGconn *, conn) \ function(PQputCopyData, GVL_TYPE_NONVOID, int, int, nbytes) \ function(PQputCopyEnd, GVL_TYPE_NONVOID, int, const char *, errormsg) \ @@ -236,6 +250,8 @@ function(PQsendQueryPrepared, GVL_TYPE_NONVOID, int, int, resultFormat) \ function(PQsendDescribePrepared, GVL_TYPE_NONVOID, int, const char *, stmt) \ function(PQsendDescribePortal, GVL_TYPE_NONVOID, int, const char *, portal) \ + function(PQsendClosePrepared, GVL_TYPE_NONVOID, int, const char *, stmt) \ + function(PQsendClosePortal, GVL_TYPE_NONVOID, int, const char *, portal) \ function(PQsetClientEncoding, GVL_TYPE_NONVOID, int, const char *, encoding) \ function(PQisBusy, GVL_TYPE_NONVOID, int, PGconn *, conn) \ function(PQencryptPasswordConn, GVL_TYPE_NONVOID, char *, const char *, algorithm) \ diff --git a/ext/pg_connection.c b/ext/pg_connection.c index 5276844c..b9564efd 100644 --- a/ext/pg_connection.c +++ b/ext/pg_connection.c @@ -1567,6 +1567,54 @@ pgconn_sync_describe_portal(VALUE self, VALUE stmt_name) } +#ifdef HAVE_PQSETCHUNKEDROWSMODE +/* + * call-seq: + * conn.sync_close_prepared( stmt_name ) -> PG::Result + * + * This function has the same behavior as #async_close_prepared, but is implemented using the synchronous command processing API of libpq. + * See #async_exec for the differences between the two API variants. + * It's not recommended to use explicit sync or async variants but #close_prepared instead, unless you have a good reason to do so. + * + * Available since PostgreSQL-17. + */ +static VALUE +pgconn_sync_close_prepared(VALUE self, VALUE stmt_name) +{ + PGresult *result; + VALUE rb_pgresult; + t_pg_connection *this = pg_get_connection_safe( self ); + const char *stmt = NIL_P(stmt_name) ? NULL : pg_cstr_enc(stmt_name, this->enc_idx); + result = gvl_PQclosePrepared(this->pgconn, stmt); + rb_pgresult = pg_new_result(result, self); + pg_result_check(rb_pgresult); + return rb_pgresult; +} + +/* + * call-seq: + * conn.sync_close_portal( portal_name ) -> PG::Result + * + * This function has the same behavior as #async_close_portal, but is implemented using the synchronous command processing API of libpq. + * See #async_exec for the differences between the two API variants. + * It's not recommended to use explicit sync or async variants but #close_portal instead, unless you have a good reason to do so. + * + * Available since PostgreSQL-17. + */ +static VALUE +pgconn_sync_close_portal(VALUE self, VALUE stmt_name) +{ + PGresult *result; + VALUE rb_pgresult; + t_pg_connection *this = pg_get_connection_safe( self ); + const char *stmt = NIL_P(stmt_name) ? NULL : pg_cstr_enc(stmt_name, this->enc_idx); + result = gvl_PQclosePortal(this->pgconn, stmt); + rb_pgresult = pg_new_result(result, self); + pg_result_check(rb_pgresult); + return rb_pgresult; +} +#endif + /* * call-seq: * conn.make_empty_pgresult( status ) -> PG::Result @@ -2140,12 +2188,56 @@ pgconn_send_describe_portal(VALUE self, VALUE portal) t_pg_connection *this = pg_get_connection_safe( self ); /* returns 0 on failure */ if(gvl_PQsendDescribePortal(this->pgconn, pg_cstr_enc(portal, this->enc_idx)) == 0) - pg_raise_conn_error( rb_eUnableToSend, self, "%s", PQerrorMessage(this->pgconn)); + pg_raise_conn_error( rb_eUnableToSend, self, "PQsendDescribePortal %s", PQerrorMessage(this->pgconn)); pgconn_wait_for_flush( self ); return Qnil; } +#ifdef HAVE_PQSETCHUNKEDROWSMODE +/* + * call-seq: + * conn.send_close_prepared( statement_name ) -> nil + * + * Asynchronously send _command_ to the server. Does not block. + * Use in combination with +conn.get_result+. + * + * Available since PostgreSQL-17. + */ +static VALUE +pgconn_send_close_prepared(VALUE self, VALUE stmt_name) +{ + t_pg_connection *this = pg_get_connection_safe( self ); + /* returns 0 on failure */ + if(gvl_PQsendClosePrepared(this->pgconn, pg_cstr_enc(stmt_name, this->enc_idx)) == 0) + pg_raise_conn_error( rb_eUnableToSend, self, "PQsendClosePrepared %s", PQerrorMessage(this->pgconn)); + + pgconn_wait_for_flush( self ); + return Qnil; +} + + +/* + * call-seq: + * conn.send_close_portal( portal_name ) -> nil + * + * Asynchronously send _command_ to the server. Does not block. + * Use in combination with +conn.get_result+. + * + * Available since PostgreSQL-17. + */ +static VALUE +pgconn_send_close_portal(VALUE self, VALUE portal) +{ + t_pg_connection *this = pg_get_connection_safe( self ); + /* returns 0 on failure */ + if(gvl_PQsendClosePortal(this->pgconn, pg_cstr_enc(portal, this->enc_idx)) == 0) + pg_raise_conn_error( rb_eUnableToSend, self, "PQsendClosePortal %s", PQerrorMessage(this->pgconn)); + + pgconn_wait_for_flush( self ); + return Qnil; +} +#endif static VALUE pgconn_sync_get_result(VALUE self) @@ -3536,6 +3628,67 @@ pgconn_async_describe_prepared(VALUE self, VALUE stmt_name) return rb_pgresult; } +#ifdef HAVE_PQSETCHUNKEDROWSMODE +/* + * call-seq: + * conn.close_prepared( statement_name ) -> PG::Result + * + * Submits a request to close the specified prepared statement, and waits for completion. + * close_prepared allows an application to close a previously prepared statement. + * Closing a statement releases all of its associated resources on the server and allows its name to be reused. + * + * statement_name can be "" or +nil+ to reference the unnamed statement. + * It is fine if no statement exists with this name, in that case the operation is a no-op. + * On success, a PG::Result with status PGRES_COMMAND_OK is returned. + * + * See also corresponding {libpq function}[https://www.postgresql.org/docs/current/libpq-exec.html#LIBPQ-PQCLOSEPREPARED]. + * + * Available since PostgreSQL-17. + */ +static VALUE +pgconn_async_close_prepared(VALUE self, VALUE stmt_name) +{ + VALUE rb_pgresult = Qnil; + + pgconn_discard_results( self ); + pgconn_send_close_prepared( self, stmt_name ); + rb_pgresult = pgconn_async_get_last_result( self ); + + if ( rb_block_given_p() ) { + return rb_ensure( rb_yield, rb_pgresult, pg_result_clear, rb_pgresult ); + } + return rb_pgresult; +} + +/* + * call-seq: + * conn.close_portal( portal_name ) -> PG::Result + * + * Submits a request to close the specified portal, and waits for completion. + * + * close_portal allows an application to trigger a close of a previously created portal. + * Closing a portal releases all of its associated resources on the server and allows its name to be reused. + * (pg does not provide any direct access to portals, but you can use this function to close a cursor created with a DECLARE CURSOR SQL command.) + * + * See also corresponding {libpq function}[https://www.postgresql.org/docs/current/libpq-exec.html#LIBPQ-PQCLOSEPORTAL]. + * + * Available since PostgreSQL-17. + */ +static VALUE +pgconn_async_close_portal(VALUE self, VALUE portal) +{ + VALUE rb_pgresult = Qnil; + + pgconn_discard_results( self ); + pgconn_send_close_portal( self, portal ); + rb_pgresult = pgconn_async_get_last_result( self ); + + if ( rb_block_given_p() ) { + return rb_ensure( rb_yield, rb_pgresult, pg_result_clear, rb_pgresult ); + } + return rb_pgresult; +} +#endif /* * call-seq: @@ -4559,6 +4712,10 @@ init_pg_connection(void) rb_define_method(rb_cPGconn, "sync_exec_prepared", pgconn_sync_exec_prepared, -1); rb_define_method(rb_cPGconn, "sync_describe_prepared", pgconn_sync_describe_prepared, 1); rb_define_method(rb_cPGconn, "sync_describe_portal", pgconn_sync_describe_portal, 1); +#ifdef HAVE_PQSETCHUNKEDROWSMODE + rb_define_method(rb_cPGconn, "sync_close_prepared", pgconn_sync_close_prepared, 1); + rb_define_method(rb_cPGconn, "sync_close_portal", pgconn_sync_close_portal, 1); +#endif rb_define_method(rb_cPGconn, "exec", pgconn_async_exec, -1); rb_define_method(rb_cPGconn, "exec_params", pgconn_async_exec_params, -1); @@ -4566,6 +4723,10 @@ init_pg_connection(void) rb_define_method(rb_cPGconn, "exec_prepared", pgconn_async_exec_prepared, -1); rb_define_method(rb_cPGconn, "describe_prepared", pgconn_async_describe_prepared, 1); rb_define_method(rb_cPGconn, "describe_portal", pgconn_async_describe_portal, 1); +#ifdef HAVE_PQSETCHUNKEDROWSMODE + rb_define_method(rb_cPGconn, "close_prepared", pgconn_async_close_prepared, 1); + rb_define_method(rb_cPGconn, "close_portal", pgconn_async_close_portal, 1); +#endif rb_define_alias(rb_cPGconn, "async_exec", "exec"); rb_define_alias(rb_cPGconn, "async_query", "async_exec"); @@ -4574,6 +4735,10 @@ init_pg_connection(void) rb_define_alias(rb_cPGconn, "async_exec_prepared", "exec_prepared"); rb_define_alias(rb_cPGconn, "async_describe_prepared", "describe_prepared"); rb_define_alias(rb_cPGconn, "async_describe_portal", "describe_portal"); +#ifdef HAVE_PQSETCHUNKEDROWSMODE + rb_define_alias(rb_cPGconn, "async_close_prepared", "close_prepared"); + rb_define_alias(rb_cPGconn, "async_close_portal", "close_portal"); +#endif rb_define_method(rb_cPGconn, "make_empty_pgresult", pgconn_make_empty_pgresult, 1); rb_define_method(rb_cPGconn, "escape_string", pgconn_s_escape, 1); diff --git a/lib/pg/connection.rb b/lib/pg/connection.rb index c0fd1b2a..04613f10 100644 --- a/lib/pg/connection.rb +++ b/lib/pg/connection.rb @@ -921,6 +921,12 @@ def ping(*args) :encrypt_password => [:async_encrypt_password, :sync_encrypt_password], } private_constant :REDIRECT_METHODS + if PG::Connection.instance_methods.include? :async_close_prepared + REDIRECT_METHODS.merge!({ + :close_prepared => [:async_close_prepared, :sync_close_prepared], + :close_portal => [:async_close_portal, :sync_close_portal], + }) + end PG.make_shareable(REDIRECT_METHODS) def async_send_api=(enable) diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index c21d3a8b..df810cc5 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -2369,6 +2369,7 @@ def wait_check_socket(conn) @conn.prepare("weiß2", "VALUES(123)") r = @conn.describe_prepared("weiß2".encode("utf-16be")) expect( r.nfields ).to eq( 1 ) + expect { @conn.prepare("weiß2", "VALUES(123)") }.to raise_error(PG::DuplicatePstatement) end it "should convert strings to #describe_portal" do @@ -2377,6 +2378,22 @@ def wait_check_socket(conn) expect( r.nfields ).to eq( 3 ) end + it "should convert strings to #close_prepared", :postgresql_17 do + @conn.prepare("weiß5", "VALUES(123)") + r = @conn.close_prepared("weiß5".encode("utf-16be")) + expect( r.nfields ).to eq( 1 ) + @conn.prepare("weiß5", "VALUES(123)") + r = @conn.close_prepared("weiß5".encode("utf-16be")) + end + + it "should convert strings to #close_portal", :postgresql_17 do + @conn.exec "DECLARE cörsör5 CURSOR FOR VALUES(1,2,3)" + r = @conn.close_portal("cörsör5".encode("utf-16le")) + expect( r.nfields ).to eq( 3 ) + @conn.exec "DECLARE cörsör5 CURSOR FOR VALUES(1,2,3)" + r = @conn.close_portal("cörsör5".encode("utf-16le")) + end + it "should convert query string to #send_query" do @conn.send_query("VALUES('grün')".encode("utf-16be")) expect( @conn.get_last_result.values ).to eq( [['grün']] )