From 41f02be6116acc3f8a7c89b53c65ebb3cca71973 Mon Sep 17 00:00:00 2001 From: meskill <8974488+meskill@users.noreply.github.com> Date: Fri, 8 May 2026 14:42:26 +0000 Subject: [PATCH 1/3] test: base for prepared_statements configuration --- .github/workflows/ci.yml | 2 + integration/ci/apt.sh | 7 +- .../prepared_statements_disabled/Gemfile | 8 + .../prepared_statements_disabled/Gemfile.lock | 276 ++++++++++++++++++ .../prepared_statements_disabled/dev.sh | 12 + .../prepared_statements_disabled/pgdog.toml | 10 + .../prepared_spec.rb | 112 +++++++ .../rspec_helper.rb | 79 +++++ .../prepared_statements_disabled/run.sh | 15 + .../prepared_statements_disabled/users.toml | 12 + integration/prepared_statements_full/Gemfile | 8 + .../prepared_statements_full/Gemfile.lock | 276 ++++++++++++++++++ integration/prepared_statements_full/dev.sh | 12 + .../prepared_statements_full/prepared_spec.rb | 105 +++++++ .../prepared_statements_full/rspec_helper.rb | 79 +++++ integration/prepared_statements_full/run.sh | 15 + .../prepared_statements_full/users.toml | 7 + integration/ruby/prepared_spec.rb | 107 +++++++ 18 files changed, 1138 insertions(+), 4 deletions(-) create mode 100644 integration/prepared_statements_disabled/Gemfile create mode 100644 integration/prepared_statements_disabled/Gemfile.lock create mode 100755 integration/prepared_statements_disabled/dev.sh create mode 100644 integration/prepared_statements_disabled/pgdog.toml create mode 100644 integration/prepared_statements_disabled/prepared_spec.rb create mode 100644 integration/prepared_statements_disabled/rspec_helper.rb create mode 100755 integration/prepared_statements_disabled/run.sh create mode 100644 integration/prepared_statements_disabled/users.toml create mode 100644 integration/prepared_statements_full/Gemfile create mode 100644 integration/prepared_statements_full/Gemfile.lock create mode 100755 integration/prepared_statements_full/dev.sh create mode 100644 integration/prepared_statements_full/prepared_spec.rb create mode 100644 integration/prepared_statements_full/rspec_helper.rb create mode 100755 integration/prepared_statements_full/run.sh create mode 100644 integration/ruby/prepared_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2544e4d9..b54782f58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,8 @@ jobs: - { name: go, script: integration/go/run.sh } - { name: js, script: integration/js/pg_tests/run.sh } - { name: ruby, script: integration/ruby/run.sh } + - { name: prepared-statements-disabled, script: integration/prepared_statements_disabled/run.sh } + - { name: prepared-statements-full, script: integration/prepared_statements_full/run.sh } - { name: java, script: integration/java/run.sh } - { name: mirror, script: integration/mirror/run.sh } - { name: sql, script: integration/sql/run.sh } diff --git a/integration/ci/apt.sh b/integration/ci/apt.sh index f3f289ea5..9a77f25e1 100755 --- a/integration/ci/apt.sh +++ b/integration/ci/apt.sh @@ -1,12 +1,11 @@ #!/usr/bin/env bash -# Thin `apt-get install` wrapper that no-ops on non-Linux platforms (macOS -# developers running these scripts locally already have the deps installed -# via their own package manager). +# Thin `apt-get install` wrapper used in CI only. +# No-ops when CI is not set (local dev) or apt-get is unavailable. # # Usage: integration/ci/apt.sh ... set -euo pipefail -if [[ "$(uname -s)" != "Linux" ]]; then +if [[ -z "${CI:-}" ]] || ! command -v apt-get &>/dev/null; then exit 0 fi diff --git a/integration/prepared_statements_disabled/Gemfile b/integration/prepared_statements_disabled/Gemfile new file mode 100644 index 000000000..6f147c82f --- /dev/null +++ b/integration/prepared_statements_disabled/Gemfile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' +gem 'pg' +gem 'rails' +gem 'rspec', '~> 3.4' +gem 'rubocop' +gem 'toxiproxy' diff --git a/integration/prepared_statements_disabled/Gemfile.lock b/integration/prepared_statements_disabled/Gemfile.lock new file mode 100644 index 000000000..a9e17f4d2 --- /dev/null +++ b/integration/prepared_statements_disabled/Gemfile.lock @@ -0,0 +1,276 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.18) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + ast (2.4.3) + base64 (0.3.0) + bigdecimal (4.1.1) + builder (3.3.0) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + date (3.5.1) + diff-lcs (1.6.2) + drb (2.2.3) + erb (6.0.2) + erubi (1.13.1) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.19.3) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + mini_mime (1.1.5) + minitest (6.0.3) + drb (~> 2.0) + prism (~> 1.5) + net-imap (0.6.3) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.2-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.2-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-musl) + racc (~> 1.4) + parallel (1.28.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + pg (1.6.3) + pg (1.6.3-aarch64-linux) + pg (1.6.3-aarch64-linux-musl) + pg (1.6.3-arm64-darwin) + pg (1.6.3-x86_64-darwin) + pg (1.6.3-x86_64-linux) + pg (1.6.3-x86_64-linux-musl) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + rubocop (1.86.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + stringio (3.2.0) + thor (1.5.0) + timeout (0.6.1) + toxiproxy (2.0.2) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.5) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + pg + rails + rspec (~> 3.4) + rubocop + toxiproxy + +BUNDLED WITH + 2.7.2 diff --git a/integration/prepared_statements_disabled/dev.sh b/integration/prepared_statements_disabled/dev.sh new file mode 100755 index 000000000..f36274590 --- /dev/null +++ b/integration/prepared_statements_disabled/dev.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd ${SCRIPT_DIR} + +export GEM_HOME=~/.gem +mkdir -p ${GEM_HOME} +bundle install +bundle exec rspec *_spec.rb + +popd diff --git a/integration/prepared_statements_disabled/pgdog.toml b/integration/prepared_statements_disabled/pgdog.toml new file mode 100644 index 000000000..e133d29ec --- /dev/null +++ b/integration/prepared_statements_disabled/pgdog.toml @@ -0,0 +1,10 @@ +[general] +prepared_statements = "disabled" +default_pool_size = 2 + +[[databases]] +name = "pgdog" +host = "127.0.0.1" + +[admin] +password = "pgdog" diff --git a/integration/prepared_statements_disabled/prepared_spec.rb b/integration/prepared_statements_disabled/prepared_spec.rb new file mode 100644 index 000000000..a1d9a7aba --- /dev/null +++ b/integration/prepared_statements_disabled/prepared_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative 'rspec_helper' + +# With prepared_statements = "disabled" pgdog forwards protocol messages as-is +# without rewriting or caching. +describe 'prepared_statements = disabled' do + after { ensure_done } + + # Anonymous statements (empty name) are a single Parse+Bind+Execute+Sync + # cycle on one backend — no state needs to survive across cycles. + it 'executes anonymous parameterized queries' do + conn = connect_pgdog + 10.times do |i| + res = conn.exec_params('SELECT $1::bigint * 2 AS val', [i]) + expect(res[0]['val'].to_i).to eq(i * 2) + end + conn.close + end + + # Session mode pins one backend for the connection lifetime; pass-through + # is sufficient because prepare and execute always reach the same backend. + it 'passes named statements through in session mode' do + conn = connect_pgdog(user: 'pgdog_session') + conn.prepare('session_stmt', 'SELECT $1::bigint AS val') + 10.times do |i| + res = conn.exec_prepared('session_stmt', [i]) + expect(res[0]['val'].to_i).to eq(i) + end + conn.close + end + + # Session mode gives each client its own dedicated backend, so two session + # connections are guaranteed to land on different backends. Without a global + # cache the execute on conn2 reaches a backend that never saw the prepare. + it 'does not share statements across connections' do + conn1 = connect_pgdog(user: 'pgdog_session') + conn2 = connect_pgdog(user: 'pgdog_session') + conn1.prepare('cross_stmt', 'SELECT $1::bigint AS val') + expect do + conn2.exec_prepared('cross_stmt', [7]) + end.to raise_error(PG::Error) + conn1.close + conn2.close + end + + # In transaction pool mode each query can land on a different backend. + # disabled mode forwards Parse and Bind as-is with no global cache, so a + # Bind that arrives on a backend that never saw the Parse fails. + # + # Sequential tests cannot force a crossing: a single connection always + # returns the backend to the LIFO top between queries, so the next query + # gets the same backend. Concurrent threads make the pool hand out both + # backends simultaneously. With 5 threads and pool_size = 2, the pigeonhole + # principle guarantees that at least 3 threads will attempt PREPARE on a + # backend that already holds the statement, producing 'already exists' + # errors — regardless of timing. + # + # Mirror: full/'executes named extended-protocol statements in + # transaction pool mode' — same structure, opposite expectation. + it 'fails named extended-protocol statements in transaction pool mode' do + errors = [] + mutex = Mutex.new + + threads = 5.times.map do + Thread.new do + conn = connect_pgdog + begin + conn.prepare('ext_stmt', 'SELECT $1::bigint AS val') + 20.times { conn.exec_prepared('ext_stmt', [42]) } + rescue PG::Error => e + mutex.synchronize { errors << e.message } + ensure + conn.close rescue nil + end + end + end + + threads.each(&:join) + expect(errors).not_to be_empty + end + + # Same mechanism as the extended-protocol test above, but for the + # simple-protocol PREPARE/EXECUTE path. disabled mode forwards the SQL + # statement text as-is, so EXECUTE on a backend that never saw the + # PREPARE fails with 'prepared statement does not exist' or, if two + # threads hit the same backend, 'already exists'. + # + # Mirror: full/'rewrites simple-protocol PREPARE / EXECUTE in + # transaction pool mode' — same structure, opposite expectation. + it 'fails SQL PREPARE/EXECUTE in transaction pool mode' do + errors = [] + mutex = Mutex.new + + threads = 5.times.map do + Thread.new do + conn = connect_pgdog + begin + conn.exec('PREPARE sql_stmt AS SELECT $1::bigint * 2') + 20.times { |i| conn.exec("EXECUTE sql_stmt(#{i})") } + rescue PG::Error => e + mutex.synchronize { errors << e.message } + ensure + conn.close rescue nil + end + end + end + + threads.each(&:join) + expect(errors).not_to be_empty + end +end diff --git a/integration/prepared_statements_disabled/rspec_helper.rb b/integration/prepared_statements_disabled/rspec_helper.rb new file mode 100644 index 000000000..146d3a3b2 --- /dev/null +++ b/integration/prepared_statements_disabled/rspec_helper.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'rspec' +require 'pg' + +def ensure_done + deadline = Time.now + 2 + pools = [] + clients = [] + servers = [] + pg_clients = [] + current_client_id = nil + + loop do + conn = PG.connect(dbname: 'admin', user: 'admin', password: 'pgdog', port: 6432, host: '127.0.0.1') + begin + pools = conn.exec 'SHOW POOLS' + current_client_id = conn.backend_pid + clients = conn.exec 'SHOW CLIENTS' + servers = conn.exec 'SHOW SERVERS' + ensure + conn.close + end + + pg_conn = PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'pgdog', port: 5432, host: '127.0.0.1') + begin + pg_clients = pg_conn.exec 'SELECT state FROM pg_stat_activity'\ + " WHERE datname IN ('pgdog', 'shard_0', 'shard_1')"\ + " AND backend_type = 'client backend'"\ + " AND query NOT LIKE '%pg_stat_activity%'" + ensure + pg_conn.close + end + + pools_ready = pools.all? do |pool| + pool['sv_active'] == '0' && pool['cl_waiting'] == '0' && pool['out_of_sync'] == '0' + end + clients_ready = clients.all? do |client| + client['id'].to_i == current_client_id || client['state'] == 'idle' + end + servers_ready = servers + .select { |server| server['application_name'] != 'PgDog Pub/Sub Listener' } + .all? { |server| server['state'] == 'idle' } + pg_clients_ready = pg_clients.all? { |client| client['state'] == 'idle' } + + break if pools_ready && clients_ready && servers_ready && pg_clients_ready + break if Time.now >= deadline + + sleep 0.05 + end + + pools.each do |pool| + expect(pool['sv_active']).to eq('0') + expect(pool['cl_waiting']).to eq('0') + expect(pool['out_of_sync']).to eq('0') + end + + clients.each do |client| + next if client['id'].to_i == current_client_id + expect(client['state']).to eq('idle') + end + + servers + .select do |server| + server['application_name'] != 'PgDog Pub/Sub Listener' + end + .each do |server| + expect(server['state']).to eq('idle') + end + + pg_clients.each do |client| + expect(client['state']).to eq('idle') + end +end + + +def connect_pgdog(user: 'pgdog') + PG.connect(dbname: 'pgdog', user:, password: 'pgdog', port: 6432, host: '127.0.0.1') +end \ No newline at end of file diff --git a/integration/prepared_statements_disabled/run.sh b/integration/prepared_statements_disabled/run.sh new file mode 100755 index 000000000..3fec7612c --- /dev/null +++ b/integration/prepared_statements_disabled/run.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source ${SCRIPT_DIR}/../common.sh + +# Native gem extensions (psych, pg) need yaml + libpq headers. +bash ${SCRIPT_DIR}/../ci/apt.sh ruby-dev libyaml-dev libpq-dev build-essential +command -v bundle >/dev/null || sudo gem install bundler --no-document + +run_pgdog $SCRIPT_DIR +wait_for_pgdog + +bash ${SCRIPT_DIR}/dev.sh + +stop_pgdog diff --git a/integration/prepared_statements_disabled/users.toml b/integration/prepared_statements_disabled/users.toml new file mode 100644 index 000000000..828fdedf7 --- /dev/null +++ b/integration/prepared_statements_disabled/users.toml @@ -0,0 +1,12 @@ +[[users]] +database = "pgdog" +name = "pgdog" +password = "pgdog" + + +[[users]] +name = "pgdog_session" +database = "pgdog" +password = "pgdog" +server_user = "pgdog" +pooler_mode = "session" \ No newline at end of file diff --git a/integration/prepared_statements_full/Gemfile b/integration/prepared_statements_full/Gemfile new file mode 100644 index 000000000..6f147c82f --- /dev/null +++ b/integration/prepared_statements_full/Gemfile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' +gem 'pg' +gem 'rails' +gem 'rspec', '~> 3.4' +gem 'rubocop' +gem 'toxiproxy' diff --git a/integration/prepared_statements_full/Gemfile.lock b/integration/prepared_statements_full/Gemfile.lock new file mode 100644 index 000000000..a9e17f4d2 --- /dev/null +++ b/integration/prepared_statements_full/Gemfile.lock @@ -0,0 +1,276 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.18) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + ast (2.4.3) + base64 (0.3.0) + bigdecimal (4.1.1) + builder (3.3.0) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + date (3.5.1) + diff-lcs (1.6.2) + drb (2.2.3) + erb (6.0.2) + erubi (1.13.1) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.19.3) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + mini_mime (1.1.5) + minitest (6.0.3) + drb (~> 2.0) + prism (~> 1.5) + net-imap (0.6.3) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.2-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.2-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-musl) + racc (~> 1.4) + parallel (1.28.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + pg (1.6.3) + pg (1.6.3-aarch64-linux) + pg (1.6.3-aarch64-linux-musl) + pg (1.6.3-arm64-darwin) + pg (1.6.3-x86_64-darwin) + pg (1.6.3-x86_64-linux) + pg (1.6.3-x86_64-linux-musl) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + rubocop (1.86.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + stringio (3.2.0) + thor (1.5.0) + timeout (0.6.1) + toxiproxy (2.0.2) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.5) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + pg + rails + rspec (~> 3.4) + rubocop + toxiproxy + +BUNDLED WITH + 2.7.2 diff --git a/integration/prepared_statements_full/dev.sh b/integration/prepared_statements_full/dev.sh new file mode 100755 index 000000000..f36274590 --- /dev/null +++ b/integration/prepared_statements_full/dev.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd ${SCRIPT_DIR} + +export GEM_HOME=~/.gem +mkdir -p ${GEM_HOME} +bundle install +bundle exec rspec *_spec.rb + +popd diff --git a/integration/prepared_statements_full/prepared_spec.rb b/integration/prepared_statements_full/prepared_spec.rb new file mode 100644 index 000000000..bae679a94 --- /dev/null +++ b/integration/prepared_statements_full/prepared_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require_relative 'rspec_helper' + +describe 'prepared_statements = full' do + after { ensure_done } + + # Mirror of disabled suite: anonymous statements carry no per-backend state, + # so they work identically regardless of the prepared_statements setting. + it 'executes anonymous parameterized queries' do + conn = connect_pgdog + 10.times do |i| + res = conn.exec_params('SELECT $1::bigint * 2 AS val', [i]) + expect(res[0]['val'].to_i).to eq(i * 2) + end + conn.close + end + + # Mirror of disabled suite: session mode pins the client to one backend for + # the connection lifetime, so named statements always reach the backend that + # holds them regardless of the prepared_statements setting. + it 'passes named statements in session mode' do + conn = connect_pgdog(user: 'pgdog_session') + conn.prepare('session_stmt', 'SELECT $1::bigint AS val') + 10.times do |i| + res = conn.exec_prepared('session_stmt', [i]) + expect(res[0]['val'].to_i).to eq(i) + end + conn.close + end + + # Mirror of disabled/'fails SQL PREPARE/EXECUTE in transaction pool mode'. + # full mode intercepts PREPARE, renames the statement to an internal name + # (__pgdog_N), and replays the PREPARE on any backend that hasn't seen it + # before executing. 5 threads × 20 iterations with pool_size = 2 generates + # the same backend crossings as the disabled test, but all succeed. + it 'rewrites simple-protocol PREPARE / EXECUTE in transaction pool mode' do + errors = [] + mutex = Mutex.new + + threads = 5.times.map do + Thread.new do + conn = connect_pgdog + begin + conn.exec('PREPARE sql_stmt AS SELECT $1::bigint * 2') + 20.times { |i| conn.exec("EXECUTE sql_stmt(#{i})") } + rescue PG::Error => e + mutex.synchronize { errors << e.message } + ensure + conn.close rescue nil + end + end + end + + threads.each(&:join) + expect(errors).to be_empty + end + + # Session mode gives each client its own dedicated backend, so two session + # connections are guaranteed to land on different backends. Without a global + # cache the execute on conn2 reaches a backend that never saw the prepare. + it 'does not share statements across connections' do + conn1 = connect_pgdog(user: 'pgdog_session') + conn2 = connect_pgdog(user: 'pgdog_session') + conn1.prepare('cross_stmt', 'SELECT $1::bigint AS val') + expect do + conn2.exec_prepared('cross_stmt', [7]) + end.to raise_error(PG::Error) + conn1.close + conn2.close + end + + # Mirror of disabled/'fails named extended-protocol statements in + # transaction pool mode'. full mode renames each frontend's Parse to an + # internal name (__pgdog_N, unique per frontend) and replays it on any + # backend before the Bind. 5 threads × 20 iterations with pool_size = 2 + # forces genuine crossings \ the replay ensures all succeed. + # Result values are also verified to guard against silent data corruption. + it 'executes named extended-protocol statements in transaction pool mode' do + errors = [] + mutex = Mutex.new + + threads = 5.times.map do + Thread.new do + conn = connect_pgdog + begin + conn.prepare('ext_stmt', 'SELECT $1::bigint AS val') + 20.times do |i| + res = conn.exec_prepared('ext_stmt', [i]) + val = res[0]['val'].to_i + raise "expected #{i}, got #{val}" unless val == i + end + rescue => e + mutex.synchronize { errors << e.message } + ensure + conn.close rescue nil + end + end + end + + threads.each(&:join) + expect(errors).to be_empty + end + +end diff --git a/integration/prepared_statements_full/rspec_helper.rb b/integration/prepared_statements_full/rspec_helper.rb new file mode 100644 index 000000000..146d3a3b2 --- /dev/null +++ b/integration/prepared_statements_full/rspec_helper.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'rspec' +require 'pg' + +def ensure_done + deadline = Time.now + 2 + pools = [] + clients = [] + servers = [] + pg_clients = [] + current_client_id = nil + + loop do + conn = PG.connect(dbname: 'admin', user: 'admin', password: 'pgdog', port: 6432, host: '127.0.0.1') + begin + pools = conn.exec 'SHOW POOLS' + current_client_id = conn.backend_pid + clients = conn.exec 'SHOW CLIENTS' + servers = conn.exec 'SHOW SERVERS' + ensure + conn.close + end + + pg_conn = PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'pgdog', port: 5432, host: '127.0.0.1') + begin + pg_clients = pg_conn.exec 'SELECT state FROM pg_stat_activity'\ + " WHERE datname IN ('pgdog', 'shard_0', 'shard_1')"\ + " AND backend_type = 'client backend'"\ + " AND query NOT LIKE '%pg_stat_activity%'" + ensure + pg_conn.close + end + + pools_ready = pools.all? do |pool| + pool['sv_active'] == '0' && pool['cl_waiting'] == '0' && pool['out_of_sync'] == '0' + end + clients_ready = clients.all? do |client| + client['id'].to_i == current_client_id || client['state'] == 'idle' + end + servers_ready = servers + .select { |server| server['application_name'] != 'PgDog Pub/Sub Listener' } + .all? { |server| server['state'] == 'idle' } + pg_clients_ready = pg_clients.all? { |client| client['state'] == 'idle' } + + break if pools_ready && clients_ready && servers_ready && pg_clients_ready + break if Time.now >= deadline + + sleep 0.05 + end + + pools.each do |pool| + expect(pool['sv_active']).to eq('0') + expect(pool['cl_waiting']).to eq('0') + expect(pool['out_of_sync']).to eq('0') + end + + clients.each do |client| + next if client['id'].to_i == current_client_id + expect(client['state']).to eq('idle') + end + + servers + .select do |server| + server['application_name'] != 'PgDog Pub/Sub Listener' + end + .each do |server| + expect(server['state']).to eq('idle') + end + + pg_clients.each do |client| + expect(client['state']).to eq('idle') + end +end + + +def connect_pgdog(user: 'pgdog') + PG.connect(dbname: 'pgdog', user:, password: 'pgdog', port: 6432, host: '127.0.0.1') +end \ No newline at end of file diff --git a/integration/prepared_statements_full/run.sh b/integration/prepared_statements_full/run.sh new file mode 100755 index 000000000..3fec7612c --- /dev/null +++ b/integration/prepared_statements_full/run.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source ${SCRIPT_DIR}/../common.sh + +# Native gem extensions (psych, pg) need yaml + libpq headers. +bash ${SCRIPT_DIR}/../ci/apt.sh ruby-dev libyaml-dev libpq-dev build-essential +command -v bundle >/dev/null || sudo gem install bundler --no-document + +run_pgdog $SCRIPT_DIR +wait_for_pgdog + +bash ${SCRIPT_DIR}/dev.sh + +stop_pgdog diff --git a/integration/prepared_statements_full/users.toml b/integration/prepared_statements_full/users.toml index 9a8205f04..e6e431643 100644 --- a/integration/prepared_statements_full/users.toml +++ b/integration/prepared_statements_full/users.toml @@ -2,3 +2,10 @@ database = "pgdog" name = "pgdog" password = "pgdog" + +[[users]] +name = "pgdog_session" +database = "pgdog" +password = "pgdog" +server_user = "pgdog" +pooler_mode = "session" diff --git a/integration/ruby/prepared_spec.rb b/integration/ruby/prepared_spec.rb new file mode 100644 index 000000000..52e528498 --- /dev/null +++ b/integration/ruby/prepared_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require_relative 'rspec_helper' + +# Uses the main integration pgdog.toml which sets prepared_statements = "extended". +# "extended" rewrites and replays named extended-protocol statements (Parse/Bind) +# across backends, but does NOT intercept SQL PREPARE / EXECUTE. + + +describe 'prepared_statements = extended' do + after { ensure_done } + + # Anonymous statements (empty name) are a single Parse+Bind+Execute+Sync + # cycle on one backend — no state needs to survive across cycles. + it 'executes anonymous parameterized queries' do + conn = connect + 10.times do |i| + res = conn.exec_params('SELECT $1::bigint * 2 AS val', [i]) + expect(res[0]['val'].to_i).to eq(i * 2) + end + conn.close + end + + # Session mode pins one backend for the connection lifetime; pass-through + # is sufficient because prepare and execute always reach the same backend. + it 'passes named statements in session mode' do + conn = connect('pgdog', 'pgdog_session') + conn.prepare('session_stmt', 'SELECT $1::bigint AS val') + 10.times do |i| + res = conn.exec_prepared('session_stmt', [i]) + expect(res[0]['val'].to_i).to eq(i) + end + conn.close + end + + # The rewrite is per-frontend (per client connection). conn2 has no mapping + # for 'cross_stmt', so its Bind is forwarded with the original name to a + # backend that never saw the Parse for that name. + it 'does not share statements across connections' do + conn1 = connect('pgdog', 'pgdog_session') + conn2 = connect('pgdog', 'pgdog_session') + conn1.prepare('cross_stmt', 'SELECT $1::bigint AS val') + expect do + conn2.exec_prepared('cross_stmt', [7]) + end.to raise_error(PG::Error) + conn1.close + conn2.close + end + + # extended mode renames each frontend's Parse to an internal name + # (__pgdog_N, unique per frontend) and replays it on any backend before + # the Bind. 5 threads × 20 iterations with a pool of 10 forces genuine + # crossings; the replay ensures all succeed. + # Result values are verified to guard against silent data corruption. + it 'executes named extended-protocol statements in transaction pool mode' do + errors = [] + mutex = Mutex.new + + threads = 5.times.map do + Thread.new do + conn = connect + begin + conn.prepare('ext_stmt', 'SELECT $1::bigint AS val') + 20.times do |i| + res = conn.exec_prepared('ext_stmt', [i]) + val = res[0]['val'].to_i + raise "expected #{i}, got #{val}" unless val == i + end + rescue => e + mutex.synchronize { errors << e.message } + ensure + conn.close rescue nil + end + end + end + + threads.each(&:join) + expect(errors).to be_empty + end + + # extended mode does NOT intercept SQL PREPARE / EXECUTE — those are + # forwarded as-is. With 5 threads and a pool of 10, at least some threads + # will prepare 'sql_stmt' on a backend that already holds it ('already + # exists') or execute on a backend that has never seen the PREPARE ('does + # not exist'). Either way, errors accumulate. + it 'fails SQL PREPARE/EXECUTE in transaction pool mode' do + errors = [] + mutex = Mutex.new + + threads = 5.times.map do + Thread.new do + conn = connect + begin + conn.exec('PREPARE sql_stmt AS SELECT $1::bigint * 2') + 20.times { |i| conn.exec("EXECUTE sql_stmt(#{i})") } + rescue PG::Error => e + mutex.synchronize { errors << e.message } + ensure + conn.close rescue nil + end + end + end + + threads.each(&:join) + expect(errors).not_to be_empty + end +end From fc1f456d5f30d2938b2859e7dcecd80ecf42b05c Mon Sep 17 00:00:00 2001 From: meskill <8974488+meskill@users.noreply.github.com> Date: Fri, 8 May 2026 19:16:12 +0000 Subject: [PATCH 2/3] fix(prepared_statements): full rewrites extended as well --- pgdog-config/src/pooling.rs | 9 +++++++++ pgdog/src/frontend/client/query_engine/rewrite.rs | 13 +++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pgdog-config/src/pooling.rs b/pgdog-config/src/pooling.rs index 3fbef5207..6d15b24b3 100644 --- a/pgdog-config/src/pooling.rs +++ b/pgdog-config/src/pooling.rs @@ -31,6 +31,15 @@ impl PreparedStatements { pub fn rewrite_anonymous(&self) -> bool { matches!(self, PreparedStatements::ExtendedAnonymous) } + + pub fn handles_extended(&self) -> bool { + matches!( + self, + PreparedStatements::Extended + | PreparedStatements::ExtendedAnonymous + | PreparedStatements::Full + ) + } } impl FromStr for PreparedStatements { diff --git a/pgdog/src/frontend/client/query_engine/rewrite.rs b/pgdog/src/frontend/client/query_engine/rewrite.rs index 907b81922..a8e143694 100644 --- a/pgdog/src/frontend/client/query_engine/rewrite.rs +++ b/pgdog/src/frontend/client/query_engine/rewrite.rs @@ -1,7 +1,4 @@ -use crate::{ - config::PreparedStatements, - frontend::router::parser::{AstContext, Cache}, -}; +use crate::frontend::router::parser::{AstContext, Cache}; use super::*; @@ -14,12 +11,8 @@ impl QueryEngine { for message in context.client_request.iter_mut() { if message.is_extended() { let level = context.prepared_statements.level; - match (level, message.anonymous()) { - (PreparedStatements::ExtendedAnonymous, _) - | (PreparedStatements::Extended, false) => { - context.prepared_statements.maybe_rewrite(message)? - } - _ => (), + if level.handles_extended() && (level.rewrite_anonymous() || !message.anonymous()) { + context.prepared_statements.maybe_rewrite(message)?; } } } From 702d0dcba102352d8ee96b8cde0b25b71a6ece30 Mon Sep 17 00:00:00 2001 From: meskill <8974488+meskill@users.noreply.github.com> Date: Tue, 19 May 2026 10:42:28 +0000 Subject: [PATCH 3/3] test: refactor ruby tests --- .github/workflows/ci.yml | 2 - .../prepared_statements_disabled/Gemfile | 8 - .../prepared_statements_disabled/Gemfile.lock | 276 ------------------ .../prepared_statements_disabled/dev.sh | 12 - .../rspec_helper.rb | 79 ----- .../prepared_statements_disabled/run.sh | 15 - integration/prepared_statements_full/Gemfile | 8 - .../prepared_statements_full/Gemfile.lock | 276 ------------------ integration/prepared_statements_full/dev.sh | 12 - .../prepared_statements_full/rspec_helper.rb | 79 ----- integration/prepared_statements_full/run.sh | 15 - integration/ruby/common.sh | 47 +++ integration/ruby/dev.sh | 11 +- integration/ruby/prepared_disabled/dev.sh | 5 + .../prepared_disabled}/pgdog.toml | 0 .../prepared_disabled}/prepared_spec.rb | 14 +- integration/ruby/prepared_disabled/run.sh | 6 + .../prepared_disabled}/users.toml | 1 + integration/ruby/prepared_full/dev.sh | 5 + .../prepared_full}/pgdog.toml | 1 + .../prepared_full}/prepared_spec.rb | 14 +- integration/ruby/prepared_full/run.sh | 6 + .../prepared_full}/users.toml | 2 +- integration/ruby/prepared_spec.rb | 16 +- integration/ruby/rspec_helper.rb | 64 +++- integration/ruby/run.sh | 15 +- 26 files changed, 153 insertions(+), 836 deletions(-) delete mode 100644 integration/prepared_statements_disabled/Gemfile delete mode 100644 integration/prepared_statements_disabled/Gemfile.lock delete mode 100755 integration/prepared_statements_disabled/dev.sh delete mode 100644 integration/prepared_statements_disabled/rspec_helper.rb delete mode 100755 integration/prepared_statements_disabled/run.sh delete mode 100644 integration/prepared_statements_full/Gemfile delete mode 100644 integration/prepared_statements_full/Gemfile.lock delete mode 100755 integration/prepared_statements_full/dev.sh delete mode 100644 integration/prepared_statements_full/rspec_helper.rb delete mode 100755 integration/prepared_statements_full/run.sh create mode 100644 integration/ruby/common.sh create mode 100755 integration/ruby/prepared_disabled/dev.sh rename integration/{prepared_statements_disabled => ruby/prepared_disabled}/pgdog.toml (100%) rename integration/{prepared_statements_disabled => ruby/prepared_disabled}/prepared_spec.rb (93%) create mode 100755 integration/ruby/prepared_disabled/run.sh rename integration/{prepared_statements_full => ruby/prepared_disabled}/users.toml (99%) create mode 100755 integration/ruby/prepared_full/dev.sh rename integration/{prepared_statements_full => ruby/prepared_full}/pgdog.toml (84%) rename integration/{prepared_statements_full => ruby/prepared_full}/prepared_spec.rb (92%) create mode 100755 integration/ruby/prepared_full/run.sh rename integration/{prepared_statements_disabled => ruby/prepared_full}/users.toml (86%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b54782f58..c2544e4d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,8 +62,6 @@ jobs: - { name: go, script: integration/go/run.sh } - { name: js, script: integration/js/pg_tests/run.sh } - { name: ruby, script: integration/ruby/run.sh } - - { name: prepared-statements-disabled, script: integration/prepared_statements_disabled/run.sh } - - { name: prepared-statements-full, script: integration/prepared_statements_full/run.sh } - { name: java, script: integration/java/run.sh } - { name: mirror, script: integration/mirror/run.sh } - { name: sql, script: integration/sql/run.sh } diff --git a/integration/prepared_statements_disabled/Gemfile b/integration/prepared_statements_disabled/Gemfile deleted file mode 100644 index 6f147c82f..000000000 --- a/integration/prepared_statements_disabled/Gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' -gem 'pg' -gem 'rails' -gem 'rspec', '~> 3.4' -gem 'rubocop' -gem 'toxiproxy' diff --git a/integration/prepared_statements_disabled/Gemfile.lock b/integration/prepared_statements_disabled/Gemfile.lock deleted file mode 100644 index a9e17f4d2..000000000 --- a/integration/prepared_statements_disabled/Gemfile.lock +++ /dev/null @@ -1,276 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - action_text-trix (2.1.18) - railties - actioncable (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) - mail (>= 2.8.0) - actionmailer (8.1.3) - actionpack (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activesupport (= 8.1.3) - mail (>= 2.8.0) - rails-dom-testing (~> 2.2) - actionpack (8.1.3) - actionview (= 8.1.3) - activesupport (= 8.1.3) - nokogiri (>= 1.8.5) - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - useragent (~> 0.16) - actiontext (8.1.3) - action_text-trix (~> 2.1.15) - actionpack (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (8.1.3) - activesupport (= 8.1.3) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (8.1.3) - activesupport (= 8.1.3) - globalid (>= 0.3.6) - activemodel (8.1.3) - activesupport (= 8.1.3) - activerecord (8.1.3) - activemodel (= 8.1.3) - activesupport (= 8.1.3) - timeout (>= 0.4.0) - activestorage (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activesupport (= 8.1.3) - marcel (~> 1.0) - activesupport (8.1.3) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - json - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - ast (2.4.3) - base64 (0.3.0) - bigdecimal (4.1.1) - builder (3.3.0) - concurrent-ruby (1.3.6) - connection_pool (3.0.2) - crass (1.0.6) - date (3.5.1) - diff-lcs (1.6.2) - drb (2.2.3) - erb (6.0.2) - erubi (1.13.1) - globalid (1.3.0) - activesupport (>= 6.1) - i18n (1.14.8) - concurrent-ruby (~> 1.0) - io-console (0.8.2) - irb (1.17.0) - pp (>= 0.6.0) - prism (>= 1.3.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - json (2.19.3) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - logger (1.7.0) - loofah (2.25.1) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.9.0) - logger - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.1.0) - mini_mime (1.1.5) - minitest (6.0.3) - drb (~> 2.0) - prism (~> 1.5) - net-imap (0.6.3) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.1) - net-protocol - nio4r (2.7.5) - nokogiri (1.19.2-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-aarch64-linux-musl) - racc (~> 1.4) - nokogiri (1.19.2-arm-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-arm-linux-musl) - racc (~> 1.4) - nokogiri (1.19.2-arm64-darwin) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-musl) - racc (~> 1.4) - parallel (1.28.0) - parser (3.3.11.1) - ast (~> 2.4.1) - racc - pg (1.6.3) - pg (1.6.3-aarch64-linux) - pg (1.6.3-aarch64-linux-musl) - pg (1.6.3-arm64-darwin) - pg (1.6.3-x86_64-darwin) - pg (1.6.3-x86_64-linux) - pg (1.6.3-x86_64-linux-musl) - pp (0.6.3) - prettyprint - prettyprint (0.2.0) - prism (1.9.0) - psych (5.3.1) - date - stringio - racc (1.8.1) - rack (3.2.6) - rack-session (2.1.1) - base64 (>= 0.1.0) - rack (>= 3.0.0) - rack-test (2.2.0) - rack (>= 1.3) - rackup (2.3.1) - rack (>= 3) - rails (8.1.3) - actioncable (= 8.1.3) - actionmailbox (= 8.1.3) - actionmailer (= 8.1.3) - actionpack (= 8.1.3) - actiontext (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activemodel (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) - bundler (>= 1.15.0) - railties (= 8.1.3) - rails-dom-testing (2.3.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.7.0) - loofah (~> 2.25) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) - irb (~> 1.13) - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - tsort (>= 0.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.3.1) - rdoc (7.2.0) - erb - psych (>= 4.0.0) - tsort - regexp_parser (2.12.0) - reline (0.6.3) - io-console (~> 0.5) - rspec (3.13.2) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) - rspec-core (3.13.6) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.8) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-support (3.13.7) - rubocop (1.86.0) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.49.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.1) - parser (>= 3.3.7.2) - prism (~> 1.7) - ruby-progressbar (1.13.0) - securerandom (0.4.1) - stringio (3.2.0) - thor (1.5.0) - timeout (0.6.1) - toxiproxy (2.0.2) - tsort (0.2.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.2.0) - uri (1.1.1) - useragent (0.16.11) - websocket-driver (0.8.0) - base64 - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.7.5) - -PLATFORMS - aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - pg - rails - rspec (~> 3.4) - rubocop - toxiproxy - -BUNDLED WITH - 2.7.2 diff --git a/integration/prepared_statements_disabled/dev.sh b/integration/prepared_statements_disabled/dev.sh deleted file mode 100755 index f36274590..000000000 --- a/integration/prepared_statements_disabled/dev.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -pushd ${SCRIPT_DIR} - -export GEM_HOME=~/.gem -mkdir -p ${GEM_HOME} -bundle install -bundle exec rspec *_spec.rb - -popd diff --git a/integration/prepared_statements_disabled/rspec_helper.rb b/integration/prepared_statements_disabled/rspec_helper.rb deleted file mode 100644 index 146d3a3b2..000000000 --- a/integration/prepared_statements_disabled/rspec_helper.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'rspec' -require 'pg' - -def ensure_done - deadline = Time.now + 2 - pools = [] - clients = [] - servers = [] - pg_clients = [] - current_client_id = nil - - loop do - conn = PG.connect(dbname: 'admin', user: 'admin', password: 'pgdog', port: 6432, host: '127.0.0.1') - begin - pools = conn.exec 'SHOW POOLS' - current_client_id = conn.backend_pid - clients = conn.exec 'SHOW CLIENTS' - servers = conn.exec 'SHOW SERVERS' - ensure - conn.close - end - - pg_conn = PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'pgdog', port: 5432, host: '127.0.0.1') - begin - pg_clients = pg_conn.exec 'SELECT state FROM pg_stat_activity'\ - " WHERE datname IN ('pgdog', 'shard_0', 'shard_1')"\ - " AND backend_type = 'client backend'"\ - " AND query NOT LIKE '%pg_stat_activity%'" - ensure - pg_conn.close - end - - pools_ready = pools.all? do |pool| - pool['sv_active'] == '0' && pool['cl_waiting'] == '0' && pool['out_of_sync'] == '0' - end - clients_ready = clients.all? do |client| - client['id'].to_i == current_client_id || client['state'] == 'idle' - end - servers_ready = servers - .select { |server| server['application_name'] != 'PgDog Pub/Sub Listener' } - .all? { |server| server['state'] == 'idle' } - pg_clients_ready = pg_clients.all? { |client| client['state'] == 'idle' } - - break if pools_ready && clients_ready && servers_ready && pg_clients_ready - break if Time.now >= deadline - - sleep 0.05 - end - - pools.each do |pool| - expect(pool['sv_active']).to eq('0') - expect(pool['cl_waiting']).to eq('0') - expect(pool['out_of_sync']).to eq('0') - end - - clients.each do |client| - next if client['id'].to_i == current_client_id - expect(client['state']).to eq('idle') - end - - servers - .select do |server| - server['application_name'] != 'PgDog Pub/Sub Listener' - end - .each do |server| - expect(server['state']).to eq('idle') - end - - pg_clients.each do |client| - expect(client['state']).to eq('idle') - end -end - - -def connect_pgdog(user: 'pgdog') - PG.connect(dbname: 'pgdog', user:, password: 'pgdog', port: 6432, host: '127.0.0.1') -end \ No newline at end of file diff --git a/integration/prepared_statements_disabled/run.sh b/integration/prepared_statements_disabled/run.sh deleted file mode 100755 index 3fec7612c..000000000 --- a/integration/prepared_statements_disabled/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -e -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -source ${SCRIPT_DIR}/../common.sh - -# Native gem extensions (psych, pg) need yaml + libpq headers. -bash ${SCRIPT_DIR}/../ci/apt.sh ruby-dev libyaml-dev libpq-dev build-essential -command -v bundle >/dev/null || sudo gem install bundler --no-document - -run_pgdog $SCRIPT_DIR -wait_for_pgdog - -bash ${SCRIPT_DIR}/dev.sh - -stop_pgdog diff --git a/integration/prepared_statements_full/Gemfile b/integration/prepared_statements_full/Gemfile deleted file mode 100644 index 6f147c82f..000000000 --- a/integration/prepared_statements_full/Gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' -gem 'pg' -gem 'rails' -gem 'rspec', '~> 3.4' -gem 'rubocop' -gem 'toxiproxy' diff --git a/integration/prepared_statements_full/Gemfile.lock b/integration/prepared_statements_full/Gemfile.lock deleted file mode 100644 index a9e17f4d2..000000000 --- a/integration/prepared_statements_full/Gemfile.lock +++ /dev/null @@ -1,276 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - action_text-trix (2.1.18) - railties - actioncable (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) - mail (>= 2.8.0) - actionmailer (8.1.3) - actionpack (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activesupport (= 8.1.3) - mail (>= 2.8.0) - rails-dom-testing (~> 2.2) - actionpack (8.1.3) - actionview (= 8.1.3) - activesupport (= 8.1.3) - nokogiri (>= 1.8.5) - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - useragent (~> 0.16) - actiontext (8.1.3) - action_text-trix (~> 2.1.15) - actionpack (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (8.1.3) - activesupport (= 8.1.3) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (8.1.3) - activesupport (= 8.1.3) - globalid (>= 0.3.6) - activemodel (8.1.3) - activesupport (= 8.1.3) - activerecord (8.1.3) - activemodel (= 8.1.3) - activesupport (= 8.1.3) - timeout (>= 0.4.0) - activestorage (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activesupport (= 8.1.3) - marcel (~> 1.0) - activesupport (8.1.3) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - json - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - ast (2.4.3) - base64 (0.3.0) - bigdecimal (4.1.1) - builder (3.3.0) - concurrent-ruby (1.3.6) - connection_pool (3.0.2) - crass (1.0.6) - date (3.5.1) - diff-lcs (1.6.2) - drb (2.2.3) - erb (6.0.2) - erubi (1.13.1) - globalid (1.3.0) - activesupport (>= 6.1) - i18n (1.14.8) - concurrent-ruby (~> 1.0) - io-console (0.8.2) - irb (1.17.0) - pp (>= 0.6.0) - prism (>= 1.3.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - json (2.19.3) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - logger (1.7.0) - loofah (2.25.1) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.9.0) - logger - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.1.0) - mini_mime (1.1.5) - minitest (6.0.3) - drb (~> 2.0) - prism (~> 1.5) - net-imap (0.6.3) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.1) - net-protocol - nio4r (2.7.5) - nokogiri (1.19.2-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-aarch64-linux-musl) - racc (~> 1.4) - nokogiri (1.19.2-arm-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-arm-linux-musl) - racc (~> 1.4) - nokogiri (1.19.2-arm64-darwin) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-musl) - racc (~> 1.4) - parallel (1.28.0) - parser (3.3.11.1) - ast (~> 2.4.1) - racc - pg (1.6.3) - pg (1.6.3-aarch64-linux) - pg (1.6.3-aarch64-linux-musl) - pg (1.6.3-arm64-darwin) - pg (1.6.3-x86_64-darwin) - pg (1.6.3-x86_64-linux) - pg (1.6.3-x86_64-linux-musl) - pp (0.6.3) - prettyprint - prettyprint (0.2.0) - prism (1.9.0) - psych (5.3.1) - date - stringio - racc (1.8.1) - rack (3.2.6) - rack-session (2.1.1) - base64 (>= 0.1.0) - rack (>= 3.0.0) - rack-test (2.2.0) - rack (>= 1.3) - rackup (2.3.1) - rack (>= 3) - rails (8.1.3) - actioncable (= 8.1.3) - actionmailbox (= 8.1.3) - actionmailer (= 8.1.3) - actionpack (= 8.1.3) - actiontext (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activemodel (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) - bundler (>= 1.15.0) - railties (= 8.1.3) - rails-dom-testing (2.3.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.7.0) - loofah (~> 2.25) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) - irb (~> 1.13) - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - tsort (>= 0.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.3.1) - rdoc (7.2.0) - erb - psych (>= 4.0.0) - tsort - regexp_parser (2.12.0) - reline (0.6.3) - io-console (~> 0.5) - rspec (3.13.2) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) - rspec-core (3.13.6) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.8) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-support (3.13.7) - rubocop (1.86.0) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.49.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.1) - parser (>= 3.3.7.2) - prism (~> 1.7) - ruby-progressbar (1.13.0) - securerandom (0.4.1) - stringio (3.2.0) - thor (1.5.0) - timeout (0.6.1) - toxiproxy (2.0.2) - tsort (0.2.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.2.0) - uri (1.1.1) - useragent (0.16.11) - websocket-driver (0.8.0) - base64 - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.7.5) - -PLATFORMS - aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - pg - rails - rspec (~> 3.4) - rubocop - toxiproxy - -BUNDLED WITH - 2.7.2 diff --git a/integration/prepared_statements_full/dev.sh b/integration/prepared_statements_full/dev.sh deleted file mode 100755 index f36274590..000000000 --- a/integration/prepared_statements_full/dev.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -pushd ${SCRIPT_DIR} - -export GEM_HOME=~/.gem -mkdir -p ${GEM_HOME} -bundle install -bundle exec rspec *_spec.rb - -popd diff --git a/integration/prepared_statements_full/rspec_helper.rb b/integration/prepared_statements_full/rspec_helper.rb deleted file mode 100644 index 146d3a3b2..000000000 --- a/integration/prepared_statements_full/rspec_helper.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'rspec' -require 'pg' - -def ensure_done - deadline = Time.now + 2 - pools = [] - clients = [] - servers = [] - pg_clients = [] - current_client_id = nil - - loop do - conn = PG.connect(dbname: 'admin', user: 'admin', password: 'pgdog', port: 6432, host: '127.0.0.1') - begin - pools = conn.exec 'SHOW POOLS' - current_client_id = conn.backend_pid - clients = conn.exec 'SHOW CLIENTS' - servers = conn.exec 'SHOW SERVERS' - ensure - conn.close - end - - pg_conn = PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'pgdog', port: 5432, host: '127.0.0.1') - begin - pg_clients = pg_conn.exec 'SELECT state FROM pg_stat_activity'\ - " WHERE datname IN ('pgdog', 'shard_0', 'shard_1')"\ - " AND backend_type = 'client backend'"\ - " AND query NOT LIKE '%pg_stat_activity%'" - ensure - pg_conn.close - end - - pools_ready = pools.all? do |pool| - pool['sv_active'] == '0' && pool['cl_waiting'] == '0' && pool['out_of_sync'] == '0' - end - clients_ready = clients.all? do |client| - client['id'].to_i == current_client_id || client['state'] == 'idle' - end - servers_ready = servers - .select { |server| server['application_name'] != 'PgDog Pub/Sub Listener' } - .all? { |server| server['state'] == 'idle' } - pg_clients_ready = pg_clients.all? { |client| client['state'] == 'idle' } - - break if pools_ready && clients_ready && servers_ready && pg_clients_ready - break if Time.now >= deadline - - sleep 0.05 - end - - pools.each do |pool| - expect(pool['sv_active']).to eq('0') - expect(pool['cl_waiting']).to eq('0') - expect(pool['out_of_sync']).to eq('0') - end - - clients.each do |client| - next if client['id'].to_i == current_client_id - expect(client['state']).to eq('idle') - end - - servers - .select do |server| - server['application_name'] != 'PgDog Pub/Sub Listener' - end - .each do |server| - expect(server['state']).to eq('idle') - end - - pg_clients.each do |client| - expect(client['state']).to eq('idle') - end -end - - -def connect_pgdog(user: 'pgdog') - PG.connect(dbname: 'pgdog', user:, password: 'pgdog', port: 6432, host: '127.0.0.1') -end \ No newline at end of file diff --git a/integration/prepared_statements_full/run.sh b/integration/prepared_statements_full/run.sh deleted file mode 100755 index 3fec7612c..000000000 --- a/integration/prepared_statements_full/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -e -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -source ${SCRIPT_DIR}/../common.sh - -# Native gem extensions (psych, pg) need yaml + libpq headers. -bash ${SCRIPT_DIR}/../ci/apt.sh ruby-dev libyaml-dev libpq-dev build-essential -command -v bundle >/dev/null || sudo gem install bundler --no-document - -run_pgdog $SCRIPT_DIR -wait_for_pgdog - -bash ${SCRIPT_DIR}/dev.sh - -stop_pgdog diff --git a/integration/ruby/common.sh b/integration/ruby/common.sh new file mode 100644 index 000000000..5abe24a94 --- /dev/null +++ b/integration/ruby/common.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Shared helpers for ruby integration suites. +# Source this file; do not execute directly. +# +RUBY_COMMON_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${RUBY_COMMON_DIR}/../common.sh" + +# Install system packages and the bundler gem. Call once per CI run. +function install_deps() { + # Native gem extensions (psych, pg) need yaml + libpq headers. + bash ${RUBY_COMMON_DIR}/../ci/apt.sh ruby-dev libyaml-dev libpq-dev build-essential + command -v bundle >/dev/null || sudo gem install bundler --no-document +} + +# Run bundle install and rspec in TARGET_DIR using the shared Gemfile. +# Defaults to RUBY_COMMON_DIR when called with no argument. +function dev_suite() { + local target_dir="${1:-$RUBY_COMMON_DIR}" + + export BUNDLE_GEMFILE="${RUBY_COMMON_DIR}/Gemfile" + export GEM_HOME=~/.gem + mkdir -p ${GEM_HOME} + + pushd "${target_dir}" + bundle install + bundle exec rspec *_spec.rb + popd +} + +# Full CI cycle for a single suite: start pgdog, run tests, stop. +# CONFIG_DIR is optional — omit to use the default integration/ config. +# Call install_deps before the first run_suite in a process. +function run_suite() { + local config_dir="${1:-}" + + if [ -n "$config_dir" ]; then + run_pgdog "$config_dir" + else + run_pgdog + fi + wait_for_pgdog + + dev_suite "$config_dir" + + stop_pgdog +} diff --git a/integration/ruby/dev.sh b/integration/ruby/dev.sh index f36274590..14d01caff 100644 --- a/integration/ruby/dev.sh +++ b/integration/ruby/dev.sh @@ -1,12 +1,5 @@ #!/bin/bash set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -pushd ${SCRIPT_DIR} - -export GEM_HOME=~/.gem -mkdir -p ${GEM_HOME} -bundle install -bundle exec rspec *_spec.rb - -popd +source "${SCRIPT_DIR}/common.sh" +dev_suite diff --git a/integration/ruby/prepared_disabled/dev.sh b/integration/ruby/prepared_disabled/dev.sh new file mode 100755 index 000000000..31c74fa4c --- /dev/null +++ b/integration/ruby/prepared_disabled/dev.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${SCRIPT_DIR}/../common.sh" +dev_suite "$SCRIPT_DIR" diff --git a/integration/prepared_statements_disabled/pgdog.toml b/integration/ruby/prepared_disabled/pgdog.toml similarity index 100% rename from integration/prepared_statements_disabled/pgdog.toml rename to integration/ruby/prepared_disabled/pgdog.toml diff --git a/integration/prepared_statements_disabled/prepared_spec.rb b/integration/ruby/prepared_disabled/prepared_spec.rb similarity index 93% rename from integration/prepared_statements_disabled/prepared_spec.rb rename to integration/ruby/prepared_disabled/prepared_spec.rb index a1d9a7aba..2b73f8083 100644 --- a/integration/prepared_statements_disabled/prepared_spec.rb +++ b/integration/ruby/prepared_disabled/prepared_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'rspec_helper' +require_relative '../rspec_helper' # With prepared_statements = "disabled" pgdog forwards protocol messages as-is # without rewriting or caching. @@ -10,7 +10,7 @@ # Anonymous statements (empty name) are a single Parse+Bind+Execute+Sync # cycle on one backend — no state needs to survive across cycles. it 'executes anonymous parameterized queries' do - conn = connect_pgdog + conn = connect 10.times do |i| res = conn.exec_params('SELECT $1::bigint * 2 AS val', [i]) expect(res[0]['val'].to_i).to eq(i * 2) @@ -21,7 +21,7 @@ # Session mode pins one backend for the connection lifetime; pass-through # is sufficient because prepare and execute always reach the same backend. it 'passes named statements through in session mode' do - conn = connect_pgdog(user: 'pgdog_session') + conn = connect('pgdog', 'pgdog_session') conn.prepare('session_stmt', 'SELECT $1::bigint AS val') 10.times do |i| res = conn.exec_prepared('session_stmt', [i]) @@ -34,8 +34,8 @@ # connections are guaranteed to land on different backends. Without a global # cache the execute on conn2 reaches a backend that never saw the prepare. it 'does not share statements across connections' do - conn1 = connect_pgdog(user: 'pgdog_session') - conn2 = connect_pgdog(user: 'pgdog_session') + conn1 = connect('pgdog', 'pgdog_session') + conn2 = connect('pgdog', 'pgdog_session') conn1.prepare('cross_stmt', 'SELECT $1::bigint AS val') expect do conn2.exec_prepared('cross_stmt', [7]) @@ -64,7 +64,7 @@ threads = 5.times.map do Thread.new do - conn = connect_pgdog + conn = connect begin conn.prepare('ext_stmt', 'SELECT $1::bigint AS val') 20.times { conn.exec_prepared('ext_stmt', [42]) } @@ -94,7 +94,7 @@ threads = 5.times.map do Thread.new do - conn = connect_pgdog + conn = connect begin conn.exec('PREPARE sql_stmt AS SELECT $1::bigint * 2') 20.times { |i| conn.exec("EXECUTE sql_stmt(#{i})") } diff --git a/integration/ruby/prepared_disabled/run.sh b/integration/ruby/prepared_disabled/run.sh new file mode 100755 index 000000000..dc3e993eb --- /dev/null +++ b/integration/ruby/prepared_disabled/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${SCRIPT_DIR}/../common.sh" +install_deps +run_suite "$SCRIPT_DIR" diff --git a/integration/prepared_statements_full/users.toml b/integration/ruby/prepared_disabled/users.toml similarity index 99% rename from integration/prepared_statements_full/users.toml rename to integration/ruby/prepared_disabled/users.toml index e6e431643..6ecd9dce0 100644 --- a/integration/prepared_statements_full/users.toml +++ b/integration/ruby/prepared_disabled/users.toml @@ -3,6 +3,7 @@ database = "pgdog" name = "pgdog" password = "pgdog" + [[users]] name = "pgdog_session" database = "pgdog" diff --git a/integration/ruby/prepared_full/dev.sh b/integration/ruby/prepared_full/dev.sh new file mode 100755 index 000000000..31c74fa4c --- /dev/null +++ b/integration/ruby/prepared_full/dev.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${SCRIPT_DIR}/../common.sh" +dev_suite "$SCRIPT_DIR" diff --git a/integration/prepared_statements_full/pgdog.toml b/integration/ruby/prepared_full/pgdog.toml similarity index 84% rename from integration/prepared_statements_full/pgdog.toml rename to integration/ruby/prepared_full/pgdog.toml index 37ae8403d..77ec881eb 100644 --- a/integration/prepared_statements_full/pgdog.toml +++ b/integration/ruby/prepared_full/pgdog.toml @@ -1,5 +1,6 @@ [general] prepared_statements = "full" +default_pool_size = 2 [[databases]] name = "pgdog" diff --git a/integration/prepared_statements_full/prepared_spec.rb b/integration/ruby/prepared_full/prepared_spec.rb similarity index 92% rename from integration/prepared_statements_full/prepared_spec.rb rename to integration/ruby/prepared_full/prepared_spec.rb index bae679a94..69defacb5 100644 --- a/integration/prepared_statements_full/prepared_spec.rb +++ b/integration/ruby/prepared_full/prepared_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'rspec_helper' +require_relative '../rspec_helper' describe 'prepared_statements = full' do after { ensure_done } @@ -8,7 +8,7 @@ # Mirror of disabled suite: anonymous statements carry no per-backend state, # so they work identically regardless of the prepared_statements setting. it 'executes anonymous parameterized queries' do - conn = connect_pgdog + conn = connect 10.times do |i| res = conn.exec_params('SELECT $1::bigint * 2 AS val', [i]) expect(res[0]['val'].to_i).to eq(i * 2) @@ -20,7 +20,7 @@ # the connection lifetime, so named statements always reach the backend that # holds them regardless of the prepared_statements setting. it 'passes named statements in session mode' do - conn = connect_pgdog(user: 'pgdog_session') + conn = connect('pgdog', 'pgdog_session') conn.prepare('session_stmt', 'SELECT $1::bigint AS val') 10.times do |i| res = conn.exec_prepared('session_stmt', [i]) @@ -40,7 +40,7 @@ threads = 5.times.map do Thread.new do - conn = connect_pgdog + conn = connect begin conn.exec('PREPARE sql_stmt AS SELECT $1::bigint * 2') 20.times { |i| conn.exec("EXECUTE sql_stmt(#{i})") } @@ -60,8 +60,8 @@ # connections are guaranteed to land on different backends. Without a global # cache the execute on conn2 reaches a backend that never saw the prepare. it 'does not share statements across connections' do - conn1 = connect_pgdog(user: 'pgdog_session') - conn2 = connect_pgdog(user: 'pgdog_session') + conn1 = connect('pgdog', 'pgdog_session') + conn2 = connect('pgdog', 'pgdog_session') conn1.prepare('cross_stmt', 'SELECT $1::bigint AS val') expect do conn2.exec_prepared('cross_stmt', [7]) @@ -82,7 +82,7 @@ threads = 5.times.map do Thread.new do - conn = connect_pgdog + conn = connect begin conn.prepare('ext_stmt', 'SELECT $1::bigint AS val') 20.times do |i| diff --git a/integration/ruby/prepared_full/run.sh b/integration/ruby/prepared_full/run.sh new file mode 100755 index 000000000..dc3e993eb --- /dev/null +++ b/integration/ruby/prepared_full/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${SCRIPT_DIR}/../common.sh" +install_deps +run_suite "$SCRIPT_DIR" diff --git a/integration/prepared_statements_disabled/users.toml b/integration/ruby/prepared_full/users.toml similarity index 86% rename from integration/prepared_statements_disabled/users.toml rename to integration/ruby/prepared_full/users.toml index 828fdedf7..6ecd9dce0 100644 --- a/integration/prepared_statements_disabled/users.toml +++ b/integration/ruby/prepared_full/users.toml @@ -9,4 +9,4 @@ name = "pgdog_session" database = "pgdog" password = "pgdog" server_user = "pgdog" -pooler_mode = "session" \ No newline at end of file +pooler_mode = "session" diff --git a/integration/ruby/prepared_spec.rb b/integration/ruby/prepared_spec.rb index 52e528498..be23e1aa2 100644 --- a/integration/ruby/prepared_spec.rb +++ b/integration/ruby/prepared_spec.rb @@ -49,14 +49,14 @@ # extended mode renames each frontend's Parse to an internal name # (__pgdog_N, unique per frontend) and replays it on any backend before - # the Bind. 5 threads × 20 iterations with a pool of 10 forces genuine - # crossings; the replay ensures all succeed. + # the Bind. 15 threads × 20 iterations with a default pool of 10 guarantees + # genuine crossings via the pigeonhole principle; the replay ensures all succeed. # Result values are verified to guard against silent data corruption. it 'executes named extended-protocol statements in transaction pool mode' do errors = [] mutex = Mutex.new - threads = 5.times.map do + threads = 15.times.map do Thread.new do conn = connect begin @@ -79,15 +79,15 @@ end # extended mode does NOT intercept SQL PREPARE / EXECUTE — those are - # forwarded as-is. With 5 threads and a pool of 10, at least some threads - # will prepare 'sql_stmt' on a backend that already holds it ('already - # exists') or execute on a backend that has never seen the PREPARE ('does - # not exist'). Either way, errors accumulate. + # forwarded as-is. With 15 threads and a default pool of 10, the pigeonhole + # principle guarantees crossings: at least 5 threads hit a backend that + # already holds 'sql_stmt' ('already exists') or one that never saw the + # PREPARE ('does not exist'). Either way, errors accumulate. it 'fails SQL PREPARE/EXECUTE in transaction pool mode' do errors = [] mutex = Mutex.new - threads = 5.times.map do + threads = 15.times.map do Thread.new do conn = connect begin diff --git a/integration/ruby/rspec_helper.rb b/integration/ruby/rspec_helper.rb index 8a7cf2990..f0c0015d0 100644 --- a/integration/ruby/rspec_helper.rb +++ b/integration/ruby/rspec_helper.rb @@ -50,22 +50,67 @@ def admin_stats(database, column = nil) stats end +def connect(db = 'pgdog', user = 'pgdog') + PG.connect(dbname: db, user: user, password: 'pgdog', port: 6432, host: '127.0.0.1') +end + def ensure_done - conn = PG.connect(dbname: 'admin', user: 'admin', password: 'pgdog', port: 6432, host: '127.0.0.1') - pools = conn.exec 'SHOW POOLS' + deadline = Time.now + 2 + pools = [] + clients = [] + servers = [] + pg_clients = [] + current_client_id = nil + + loop do + conn = PG.connect(dbname: 'admin', user: 'admin', password: 'pgdog', port: 6432, host: '127.0.0.1') + begin + pools = conn.exec 'SHOW POOLS' + current_client_id = conn.backend_pid + clients = conn.exec 'SHOW CLIENTS' + servers = conn.exec 'SHOW SERVERS' + ensure + conn.close + end + + pg_conn = PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'pgdog', port: 5432, host: '127.0.0.1') + begin + pg_clients = pg_conn.exec 'SELECT state FROM pg_stat_activity'\ + " WHERE datname IN ('pgdog', 'shard_0', 'shard_1')"\ + " AND backend_type = 'client backend'"\ + " AND query NOT LIKE '%pg_stat_activity%'" + ensure + pg_conn.close + end + + pools_ready = pools.all? do |pool| + pool['sv_active'] == '0' && pool['cl_waiting'] == '0' && pool['out_of_sync'] == '0' + end + clients_ready = clients.all? do |client| + client['id'].to_i == current_client_id || client['state'] == 'idle' + end + servers_ready = servers + .select { |server| server['application_name'] != 'PgDog Pub/Sub Listener' } + .all? { |server| server['state'] == 'idle' } + pg_clients_ready = pg_clients.all? { |client| client['state'] == 'idle' } + + break if pools_ready && clients_ready && servers_ready && pg_clients_ready + break if Time.now >= deadline + + sleep 0.05 + end + pools.each do |pool| expect(pool['sv_active']).to eq('0') expect(pool['cl_waiting']).to eq('0') expect(pool['out_of_sync']).to eq('0') end - current_client_id = conn.backend_pid - clients = conn.exec 'SHOW CLIENTS' + clients.each do |client| next if client['id'].to_i == current_client_id - expect(client['state']).to eq('idle') end - servers = conn.exec 'SHOW SERVERS' + servers .select do |server| server['application_name'] != 'PgDog Pub/Sub Listener' @@ -74,12 +119,7 @@ def ensure_done expect(server['state']).to eq('idle') end - conn = PG.connect(dbname: 'pgdog', user: 'pgdog', password: 'pgdog', port: 5432, host: '127.0.0.1') - clients = conn.exec 'SELECT state FROM pg_stat_activity'\ - " WHERE datname IN ('pgdog', 'shard_0', 'shard_1')"\ - " AND backend_type = 'client backend'"\ - " AND query NOT LIKE '%pg_stat_activity%'" - clients.each do |client| + pg_clients.each do |client| expect(client['state']).to eq('idle') end end diff --git a/integration/ruby/run.sh b/integration/ruby/run.sh index 792b35971..e827d6857 100644 --- a/integration/ruby/run.sh +++ b/integration/ruby/run.sh @@ -1,15 +1,10 @@ #!/bin/bash set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -source ${SCRIPT_DIR}/../common.sh +source "${SCRIPT_DIR}/common.sh" -# Native gem extensions (psych, pg) need yaml + libpq headers. -bash ${SCRIPT_DIR}/../ci/apt.sh ruby-dev libyaml-dev libpq-dev build-essential -command -v bundle >/dev/null || sudo gem install bundler --no-document +install_deps -run_pgdog -wait_for_pgdog - -bash ${SCRIPT_DIR}/dev.sh - -stop_pgdog +run_suite +run_suite "${SCRIPT_DIR}/prepared_disabled" +run_suite "${SCRIPT_DIR}/prepared_full"