Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions integration/ci/apt.sh
Original file line number Diff line number Diff line change
@@ -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 <pkg1> <pkg2> ...
set -euo pipefail

if [[ "$(uname -s)" != "Linux" ]]; then
if [[ -z "${CI:-}" ]] || ! command -v apt-get &>/dev/null; then
exit 0
fi

Expand Down
4 changes: 0 additions & 4 deletions integration/prepared_statements_full/users.toml

This file was deleted.

47 changes: 47 additions & 0 deletions integration/ruby/common.sh
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 2 additions & 9 deletions integration/ruby/dev.sh
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions integration/ruby/prepared_disabled/dev.sh
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions integration/ruby/prepared_disabled/pgdog.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[general]
prepared_statements = "disabled"
default_pool_size = 2

[[databases]]
name = "pgdog"
host = "127.0.0.1"

[admin]
password = "pgdog"
112 changes: 112 additions & 0 deletions integration/ruby/prepared_disabled/prepared_spec.rb
Original file line number Diff line number Diff line change
@@ -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
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', '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', '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

# 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
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
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
6 changes: 6 additions & 0 deletions integration/ruby/prepared_disabled/run.sh
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions integration/ruby/prepared_disabled/users.toml
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions integration/ruby/prepared_full/dev.sh
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[general]
prepared_statements = "full"
default_pool_size = 2

[[databases]]
name = "pgdog"
Expand Down
105 changes: 105 additions & 0 deletions integration/ruby/prepared_full/prepared_spec.rb
Original file line number Diff line number Diff line change
@@ -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
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', '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
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', '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

# 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
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
6 changes: 6 additions & 0 deletions integration/ruby/prepared_full/run.sh
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions integration/ruby/prepared_full/users.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading