Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7681415
feat(ruby): bootstrap gem + first connect test
dlt May 9, 2026
0d582f8
feat(ruby): connect block form auto-closes client
dlt May 9, 2026
4b66086
feat(ruby): bad DSN raises Pgque::ConnectionError
dlt May 9, 2026
45d2ef2
feat(ruby): close is a no-op for external connections
dlt May 9, 2026
d804ebd
feat(ruby): autocommit flag stored on client
dlt May 9, 2026
cf1b6ed
feat(ruby): close is idempotent
dlt May 9, 2026
fe75ed6
feat(ruby): send returns integer event id
dlt May 9, 2026
001fd00
feat(ruby): send accepts type: kwarg
dlt May 9, 2026
aedc6ed
feat(ruby): send accepts Pgque::Event payload
dlt May 9, 2026
7662033
feat(ruby): send passes through JSON-string payloads
dlt May 9, 2026
253b53f
feat(ruby): send nil payload stores JSON null
dlt May 9, 2026
5a5965f
feat(ruby): send_batch returns ids in input order
dlt May 9, 2026
6538580
feat(ruby): consumer-side primitive API (receive, ack, nack)
dlt May 9, 2026
2e7cd7a
feat(ruby): polling Consumer with LISTEN/NOTIFY wakeup
dlt May 9, 2026
e38e9c2
feat(ruby): cooperative consumers (experimental)
dlt May 9, 2026
e8a82c1
test(ruby): concurrency + PgQ snapshot visibility regressions
dlt May 9, 2026
d0a0a84
ci(ruby): add ruby-client-tests job
dlt May 9, 2026
4de8508
docs: add Ruby column to client parity matrix + README section
dlt May 9, 2026
775a853
docs(ruby): document __send__ workaround for Client#send
dlt May 9, 2026
dbafc30
fix(ruby): silence default Consumer logger by default
dlt May 9, 2026
c2bfc97
fix(ruby): drop autocommit: kwarg; document Ruby pg semantics
dlt May 9, 2026
14455b5
fix(ruby): coerce non-Hash/Array/String send payloads via to_s
dlt May 9, 2026
36615f2
test(ruby): rollback in with_queue cleanup before drop
dlt May 9, 2026
34a4299
test(ruby): rollback before coop cleanup
dlt May 9, 2026
a965625
fix(ruby): ignore invalid PGQUE_LOG_LEVEL values
dlt May 9, 2026
ba8ac35
fix(ruby): preserve SQL error causes
dlt May 9, 2026
835cbe7
refactor(ruby): use PG::Result accessors
dlt May 9, 2026
a9f835d
refactor(ruby): simplify client conditionals
dlt May 9, 2026
0dbb402
fix(ruby): update test FakeResult to match getvalue accessor
dlt May 9, 2026
b8757b2
feat(ruby/release): release-ruby.yml + RELEASE.md
dlt May 9, 2026
9727982
refactor(ruby): rename Consumer#set_running to running=
dlt May 9, 2026
755ffcb
fix(ruby): keep signal trap async-signal-safe
dlt May 11, 2026
6484972
fix(ruby): Consumer#start clears running? on exception
dlt May 11, 2026
ec6a75d
fix(ruby/release): wire up rake release for publish workflow
dlt May 11, 2026
3e2bb4f
docs(ruby): make Ruby quickstart self-complete
dlt May 11, 2026
ae4e4c7
docs: stop using v0.2.0-rc.1 for all four clients
dlt May 11, 2026
e17a31b
Merge branch 'main' into feat/ruby-client
dlt May 25, 2026
8aaccbb
feat(ruby): add ticker and ticker_all wrappers
dlt May 25, 2026
0e3a440
feat(ruby): add subscribe and unsubscribe wrappers
dlt May 25, 2026
54f8539
chore(ruby): bump gem version to 0.3.0.rc.1
dlt May 26, 2026
35d884f
docs(ruby): re-sync README with the Python README
dlt May 26, 2026
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
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,62 @@ jobs:
if: always()
run: docker rm -f pgque-ts-test

ruby-client-tests:
name: Ruby client tests
runs-on: ubuntu-latest
env:
PGQUE_TEST_DSN: postgresql://postgres:pgque_test@localhost/pgque_test
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Start PostgreSQL 18
run: |
set -Eeuo pipefail
docker run -d --name pgque-ruby-test \
-e POSTGRES_PASSWORD=pgque_test \
-e POSTGRES_DB=pgque_test \
-p 5432:5432 \
postgres:18

for i in $(seq 1 30); do
docker exec pgque-ruby-test pg_isready -U postgres && break
sleep 1
done || { echo "PG not ready after 30 seconds"; exit 1; }

- name: Build pgque
run: bash build/transform.sh

- name: Install pgque
run: |
set -Eeuo pipefail
PGPASSWORD=pgque_test psql -h localhost -U postgres -d pgque_test \
-v ON_ERROR_STOP=1 -f sql/pgque.sql

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
working-directory: clients/ruby
bundler-cache: true

- name: Ruby client test suite
run: |
set -Eeuo pipefail
cd clients/ruby
bundle exec rake test

- name: Ruby package smoke
run: |
set -Eeuo pipefail
cd clients/ruby
gem build pgque.gemspec

- name: Cleanup Ruby test DB
if: always()
run: docker rm -f pgque-ruby-test

verify:
runs-on: ubuntu-latest
steps:
Expand Down
127 changes: 127 additions & 0 deletions .github/workflows/release-ruby.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Release Ruby client

on:
workflow_dispatch:
inputs:
version:
description: "Version to publish, matching clients/ruby/lib/pgque/version.rb"
required: true
type: string
dry_run:
description: "Build and validate without publishing"
required: true
default: true
type: boolean

permissions:
contents: read

jobs:
build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
VERSION: ${{ inputs.version }}
defaults:
run:
working-directory: clients/ruby
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
working-directory: clients/ruby
bundler-cache: true

- name: Verify version input
run: |
set -Eeuo pipefail
# Reject malformed input early. Gem::Version is permissive
# (accepts 0.2.0, 0.2.0.rc.1, 0.2.0.alpha, 0.2.0.beta.1, ...)
# but rejects clear garbage like "x.y.z" or empty strings.
ruby -rrubygems -e "Gem::Version.new(ENV.fetch('VERSION'))"

# The version constant must already match the input -- bumping
# version.rb is the responsibility of the release-prep PR, not
# this workflow. Any drift here is a configuration error.
actual=$(ruby -e 'require_relative "lib/pgque/version"; print Pgque::VERSION')
test "$actual" = "$VERSION" || {
echo "version input ${VERSION} != lib/pgque/version.rb ${actual}"
exit 1
}

- name: Build gem
run: gem build pgque.gemspec

- name: Verify built gem installs
run: |
set -Eeuo pipefail
mkdir -p /tmp/pgque-gem-check
gem install --install-dir /tmp/pgque-gem-check --no-document \
"./pgque-${VERSION}.gem"
GEM_PATH=/tmp/pgque-gem-check GEM_HOME=/tmp/pgque-gem-check \
ruby -e '
require "pgque"
expected = ENV.fetch("VERSION")
raise "version mismatch: expected #{expected}, got #{Pgque::VERSION}" \
unless Pgque::VERSION == expected
raise "Pgque::Client missing" unless defined?(Pgque::Client)
raise "Pgque::Consumer missing" unless defined?(Pgque::Consumer)
puts "install verified: pgque #{Pgque::VERSION}"
'

publish-rubygems:
if: ${{ !inputs.dry_run }}
needs: build
runs-on: ubuntu-latest
environment: rubygems
permissions:
# contents:write is required so `rake release` (invoked by
# rubygems/release-gem) can push the v${VERSION} git tag back to
# origin. id-token:write is for the OIDC handshake with
# rubygems.org.
contents: write
id-token: write
defaults:
run:
working-directory: clients/ruby
steps:
# Check out the branch ref (not the bare SHA) so we land on an
# attached HEAD; bundler's release:source_control_push runs plain
# `git push`, which fails from detached HEAD. The build job (run
# immediately before this one) already verified the version
# against the dispatch SHA, so any race with main moving during
# the workflow would have to land a new commit AND a version
# bump in that window -- vanishingly small.
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
# Full history so existing tags are visible to release:guard_clean.
fetch-depth: 0

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
working-directory: clients/ruby
bundler-cache: true

- name: Configure git identity for tag push
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# rubygems/release-gem performs the OIDC handshake (no long-lived
# RUBYGEMS_API_KEY secret needed) and then runs
# `bundle exec rake release`, which depends on the gem tasks
# provided by `require "bundler/gem_tasks"` in clients/ruby/Rakefile.
# The chain is: build -> release:guard_clean -> release:source_control_push
# (tags v${VERSION} and pushes the tag) -> release:rubygem_push.
- name: Publish to RubyGems via OIDC
uses: rubygems/release-gem@v1
with:
working-directory: clients/ruby
setup-trusted-publisher: true
await-release: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ build/output/
clients/python/*.egg-info/
clients/typescript/node_modules/
clients/typescript/package-lock.json
clients/ruby/Gemfile.lock
clients/ruby/pkg/
clients/ruby/.bundle/
clients/ruby/*.gem

# Claude Code agent worktrees (ephemeral isolation per agent run)
.claude/worktrees/
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ Longer walkthrough in the [tutorial](docs/tutorial.md); patterns like fan-out, e

## Client libraries

PgQue is SQL-first, so any Postgres driver works. First-party client libraries live in this repo for **Python**, **Go**, and **TypeScript**, all published at `v0.2.0`.
PgQue is SQL-first, so any Postgres driver works. First-party client libraries live in this repo for **Python**, **Go**, and **TypeScript** (all published at `v0.2.0`), plus **Ruby**, shipping in v0.3.

### Python (`pgque-py`) — psycopg 3

Expand Down Expand Up @@ -410,6 +410,32 @@ try {
}
```

### Ruby (`pgque`) — pg gem

```bash
gem install pgque --pre # or pin: gem "pgque", "0.3.0.rc.1"
```

```ruby
require "pgque"

Pgque.connect("postgresql://localhost/mydb") do |client|
# one-time setup (typically in a migration)
client.conn.exec("select pgque.create_queue('orders')")
client.conn.exec("select pgque.subscribe('orders', 'processor')")

client.send("orders", { "order_id" => 42 }, type: "order.created")
end

consumer = Pgque::Consumer.new(
"postgresql://localhost/mydb",
queue: "orders",
name: "processor",
)
consumer.on("order.created") { |msg| process_order(msg.payload) }
consumer.start # blocks until SIGTERM/SIGINT; needs pgque.ticker() running
```

### Any language

```sql
Expand Down
51 changes: 25 additions & 26 deletions clients/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PgQue clients

PgQue ships three first-party clients. They are thin wrappers over `pgque.*`
PgQue ships four first-party clients. They are thin wrappers over `pgque.*`
SQL primitives. The matrix below tracks the public client API on current
`main`.

Expand Down Expand Up @@ -74,34 +74,33 @@ users to install `--pre`, `@rc`, or an `-rc` Go tag.

## Current parity matrix

| Capability | Python | Go | TypeScript |
| --- | :---: | :---: | :---: |
| `connect` / `close` | ✓ | ✓ | ✓ |
| Raw SQL escape hatch | ✓ (`conn`) | ✓ (`Pool()`) | ✓ (`rawPool`) |
| PgQue-classified errors | ✓ | ✓ | ✓ |
| Lossless PostgreSQL `bigint` IDs | ✓ (`int`) | ✓ (`int64`) | ✓ (`bigint`) |
| `send` | ✓ | ✓ | ✓ |
| `send_batch` / `SendBatch` / `sendBatch` | ✓ | ✓ | ✓ |
| `receive` | ✓ | ✓ | ✓ |
| `ack` returns SQL rowcount (0 stale, 1 success) | ✓ (int) | ✓ (int64) | ✓ (number) |
| `nack` | ✓ | ✓ | ✓ |
| `ticker` / `Ticker` / `ticker`, `ticker_all` / `TickerAll` / `tickerAll` | ✓ | ✓ | ✓ |
| `force_next_tick` / `ForceNextTick` / `forceNextTick` | ✓ | ✓ | ✓ |
| `nack` retry delay + reason options | ✓ | ✓ | ✓ |
| High-level `Consumer` | ✓ | ✓ | ✓ |
| Consumer wakeup model | polling + optional LISTEN/NOTIFY wakeup | polling | polling |
| `Consumer` poll interval option | ✓ | ✓ | ✓ |
| `Consumer` max-messages option | ✓ | ✓ | ✓ |
| `Consumer` retry delay option | ✓ | ✓ | ✓ |
| Unknown-type behavior avoids silent ack | ✓ | ✓ | ✓ |
| Configurable unknown-type policy | ✓ | ✓ | ✓ |
| `subscribe` / `unsubscribe` wrappers | ✓ | ✓ | ✓ |
| Cooperative consumers (experimental) [^coop] | ✓ | ✓ | ✓ |
| Capability | Python | Go | TypeScript | Ruby |
| --- | :---: | :---: | :---: | :---: |
| `connect` / `close` | ✓ | ✓ | ✓ | ✓ |
| Raw SQL escape hatch | ✓ (`conn`) | ✓ (`Pool()`) | ✓ (`rawPool`) | ✓ (`conn`) |
| PgQue-classified errors | ✓ | ✓ | ✓ | ✓ |
| Lossless PostgreSQL `bigint` IDs | ✓ (`int`) | ✓ (`int64`) | ✓ (`bigint`) | ✓ (`Integer`) |
| `send` | ✓ | ✓ | ✓ | ✓ |
| `send_batch` / `SendBatch` / `sendBatch` | ✓ | ✓ | ✓ | ✓ |
| `receive` | ✓ | ✓ | ✓ | ✓ |
| `ack` returns SQL rowcount (0 stale, 1 success) | ✓ (int) | ✓ (int64) | ✓ (number) | ✓ (Integer) |
| `nack` | ✓ | ✓ | ✓ | ✓ |
| `ticker` / `Ticker` / `ticker`, `ticker_all` / `TickerAll` / `tickerAll` | ✓ | ✓ | ✓ | ✓ |
| `force_next_tick` / `ForceNextTick` / `forceNextTick` | ✓ | ✓ | ✓ | ✓ |
| `nack` retry delay + reason options | ✓ | ✓ | ✓ | ✓ |
| High-level `Consumer` | ✓ | ✓ | ✓ | ✓ |
| Consumer wakeup model | polling + optional LISTEN/NOTIFY wakeup | polling | polling | polling + LISTEN/NOTIFY wakeup |
| `Consumer` poll interval option | ✓ | ✓ | ✓ | ✓ |
| `Consumer` max-messages option | ✓ | ✓ | ✓ | ✓ |
| `Consumer` retry delay option | ✓ | ✓ | ✓ | ✓ |
| Unknown-type behavior avoids silent ack | ✓ | ✓ | ✓ | ✓ |
| Configurable unknown-type policy | ✓ | ✓ | ✓ | ✓ |
| `subscribe` / `unsubscribe` wrappers | ✓ | ✓ | ✓ | ✓ |
| Cooperative consumers (experimental) [^coop] | ✓ | ✓ | ✓ | ✓ |

Legend: ✓ supported by the client API on `main`; ✗ not exposed as a
first-class client API. Lower-level SQL primitives remain available through raw
connection/pool escape hatches. Python, Go, and TypeScript expose ticker
convenience wrappers.
connection/pool escape hatches.

[^coop]: Experimental. Each supporting client exposes
`subscribe_subconsumer` / `unsubscribe_subconsumer` / `receive_coop` /
Expand Down
5 changes: 5 additions & 0 deletions clients/ruby/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/Gemfile.lock
/pkg/
/.bundle/
/coverage/
*.gem
3 changes: 3 additions & 0 deletions clients/ruby/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gemspec
Loading
Loading