Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ cleanbuild:
.PHONY: install
install: src/proxysql
install -m 0755 src/proxysql /usr/bin
# proxysql-cli is the same binary; main() dispatches on argv[0] and never
# starts the daemon when invoked under this name.
ln -sf proxysql /usr/bin/proxysql-cli
install -m 0600 etc/proxysql.cnf /etc
if [ ! -d /var/lib/proxysql ]; then mkdir /var/lib/proxysql ; fi
if [ -f plugins/mysqlx/ProxySQL_MySQLX_Plugin.so ]; then \
Expand Down Expand Up @@ -635,6 +638,7 @@ endif
.PHONY: uninstall
uninstall:
if [ -f /etc/proxysql.cnf ]; then rm /etc/proxysql.cnf ; fi
if [ -L /usr/bin/proxysql-cli ]; then rm /usr/bin/proxysql-cli ; fi
if [ -f /usr/bin/proxysql ]; then rm /usr/bin/proxysql ; fi
if [ -f /usr/lib/proxysql/plugins/ProxySQL_MySQLX_Plugin.so ]; then rm /usr/lib/proxysql/plugins/ProxySQL_MySQLX_Plugin.so ; fi
if [ -f /usr/lib/proxysql/plugins/ProxySQL_GenAI_Plugin.so ]; then rm /usr/lib/proxysql/plugins/ProxySQL_GenAI_Plugin.so ; fi
Expand Down
236 changes: 236 additions & 0 deletions doc/PGBOUNCER_COMPAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# PgBouncer compatibility

ProxySQL can read an existing PgBouncer deployment's configuration and can answer
PgBouncer's `SHOW` commands on its PostgreSQL admin port. Together these let a
PgBouncer installation be replaced without rewriting its configuration by hand and
without changing the monitoring that reads from it.

The implementation lives in `lib/pgbouncer_compat/` and is built into
`libproxysql.a` for every tier — there is no feature flag.

> This document describes the behaviour that is actually implemented. Where a
> PgBouncer feature has no ProxySQL equivalent it is listed as such rather than
> approximated, because a silent approximation is worse than a reported gap.

---

## 1. Importing a PgBouncer configuration

Two entry points, one conversion engine.

### `proxysql-cli` (offline)

`proxysql-cli` is the `proxysql` binary under a second name. `main()` compares
`basename(argv[0])` against `proxysql-cli` and dispatches to the CLI without ever
starting the daemon. Packages install it as a symlink next to `proxysql`.

```bash
# Print the SQL that would be applied, with an explanatory comment per statement
proxysql-cli import-pgbouncer /etc/pgbouncer/pgbouncer.ini --dry-run

# Warn on unmappable parameters instead of failing
proxysql-cli import-pgbouncer /etc/pgbouncer/pgbouncer.ini --ignore-warnings

# Apply against a running instance
proxysql-cli import-pgbouncer /etc/pgbouncer/pgbouncer.ini \
| psql -h 127.0.0.1 -p 6132 -U admin
```

### `IMPORT PGBOUNCER CONFIG` (online)

From the admin interface:

```sql
IMPORT PGBOUNCER CONFIG FROM '/etc/pgbouncer/pgbouncer.ini';
IMPORT PGBOUNCER CONFIG FROM '/etc/pgbouncer/pgbouncer.ini' DRY RUN;
IMPORT PGBOUNCER CONFIG FROM '/etc/pgbouncer/pgbouncer.ini' IGNORE WARNINGS;
IMPORT PGBOUNCER CONFIG FROM '/etc/pgbouncer/pgbouncer.ini' DRY RUN IGNORE WARNINGS;
```

The path is read by the **server**, not the client.

### Import-once, strict by default

The import bootstraps ProxySQL; from then on the admin interface owns the
configuration. There is no continuous sync back to `pgbouncer.ini`.

By default the import is **strict**: any parameter that cannot be mapped is an
error and nothing is applied. `IGNORE WARNINGS` / `--ignore-warnings` downgrades
those to warnings and applies the rest. Strictness is deliberate — a partially
imported pooler configuration is a production incident waiting to happen.

The converter rewrites `pgsql_servers`, `pgsql_users` and `pgsql_query_rules`
from scratch (each is `DELETE`d before its `INSERT`s), so an import replaces the
PostgreSQL-side configuration rather than merging into it.

---

## 2. What the parser reads

| File | Parsed by | Notes |
|---|---|---|
| `pgbouncer.ini` | `PgBouncer_ConfigParser` | `[pgbouncer]`, `[databases]`, `[users]`, `[peers]`; `%include` to 10 levels |
| `userlist.txt` | `PgBouncer_AuthFileParser` | `"user" "password"`; detects plain / MD5 / SCRAM |
| `pg_hba.conf` | `PgBouncer_HBAParser` | followed when `auth_hba_file` is set |

Quoting follows PgBouncer: a single-quoted value in `[pgbouncer]` protects spaces
and `#`/`;`, and `''` is a literal quote. In `pg_hba.conf`, `""` inside a
double-quoted token is a literal quote, and an unterminated quote is an error
rather than a silently truncated field.

---

## 3. Configuration mapping

### Global settings

| PgBouncer | ProxySQL | Note |
|---|---|---|
| `listen_addr` + `listen_port` | `pgsql-interfaces` | |
| `auth_type` | `pgsql-authentication_method` | `plain`/`password`→1, `md5`→2, `scram-sha-256`→3 |
| `max_client_conn` | `pgsql-max_connections` | |
| `server_connect_timeout` | `pgsql-connect_timeout_server` | |
| `server_lifetime` | `pgsql-connection_max_age_ms` | s → ms |
| `client_idle_timeout` | `pgsql-wait_timeout` | s → ms |
| `log_min_duration` | `pgsql-long_query_time` | |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The mapping table lists log_min_duration as mapping to pgsql-long_query_time, but the converter maps query_timeout to that variable (PgBouncer_ConfigConverter.cpp:361-364); log_min_duration is not mapped at all. Rename the parameter so the documented mapping matches the implementation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At doc/PGBOUNCER_COMPAT.md, line 95:

<comment>The mapping table lists `log_min_duration` as mapping to `pgsql-long_query_time`, but the converter maps `query_timeout` to that variable (PgBouncer_ConfigConverter.cpp:361-364); `log_min_duration` is not mapped at all. Rename the parameter so the documented mapping matches the implementation.</comment>

<file context>
@@ -0,0 +1,236 @@
+| `server_connect_timeout` | `pgsql-connect_timeout_server` | |
+| `server_lifetime` | `pgsql-connection_max_age_ms` | s → ms |
+| `client_idle_timeout` | `pgsql-wait_timeout` | s → ms |
+| `log_min_duration` | `pgsql-long_query_time` | |
+| `idle_transaction_timeout` | `pgsql-max_transaction_idle_time` | s → ms |
+| `transaction_timeout` | `pgsql-max_transaction_time` | s → ms |
</file context>
Suggested change
| `log_min_duration` | `pgsql-long_query_time` | |
| `query_timeout` | `pgsql-long_query_time` | |

| `idle_transaction_timeout` | `pgsql-max_transaction_idle_time` | s → ms |
| `transaction_timeout` | `pgsql-max_transaction_time` | s → ms |
| `max_prepared_statements` | `pgsql-max_stmts_per_connection` | |
| `server_tls_sslmode` | `use_ssl` on all imported servers | `require`/`verify-ca`/`verify-full` |
| `server_tls_ca_file` | `pgsql-ssl_p2s_ca` | |
| `server_tls_cert_file` | `pgsql-ssl_p2s_cert` | |
| `server_tls_key_file` | `pgsql-ssl_p2s_key` | |
| `server_check_query` | `pgsql-monitor_enabled=1` | |
| `server_check_delay` | `pgsql-monitor_ping_interval` | s → ms |
| `tcp_keepalive` | `pgsql-use_tcp_keepalive` | |
| `tcp_keepidle` | `pgsql-tcp_keepalive_time` | |

### `[databases]`

Each entry becomes a hostgroup with one `pgsql_servers` row per host (a
comma-separated `host=` list yields several rows in the same hostgroup), plus a
`pgsql_query_rules` row routing that database name to the hostgroup. The `*`
wildcard database becomes the default hostgroup and gets no routing rule.
`pool_size` maps to `max_connections` on the server row.

### `[users]` and `userlist.txt`

Each user becomes a `pgsql_users` row. `pool_mode` maps as:

| PgBouncer `pool_mode` | ProxySQL |
|---|---|
| `session` | `fast_forward=1` |
| `transaction` | `transaction_persistent=1` |
| `statement` | neither flag |

### `pg_hba.conf`

`host`/`hostssl` records with `trust`, `md5`, `scram-sha-256` or `password`
become `pgsql_firewall_whitelist_rules` entries and enable
`pgsql-firewall_whitelist_enabled`. `hostssl` additionally sets `use_ssl=1` on
the matching users.

---

## 4. What is *not* mapped

These are reported per occurrence — fatal in strict mode, warnings otherwise.

**Authentication.** `auth_query`, `auth_user`, `auth_dbname`. ProxySQL
authenticates from `pgsql_users` (or LDAP), not by querying the backend.

**`auth_type` values with no equivalent.** `trust` and `any` (ProxySQL always
verifies the user against `pgsql_users` and cannot accept an unauthenticated
connection), `hba` (`pgsql-authentication_method` is global, so per-rule methods
from `pg_hba.conf` cannot select the frontend method), and `cert`/`pam`.

**Pre-hashed passwords.** A `userlist.txt` entry holding an MD5 or SCRAM verifier
is imported verbatim but **will not authenticate**. ProxySQL derives both the MD5
challenge response and the SCRAM verifier from the *cleartext* password stored in
`pgsql_users.password`, so a pre-hashed value cannot be used. Replace those
entries with the cleartext password after importing.

> Tracked in #6134. This limitation is expected to lift with PR #5865 / issue #5863, which teaches
> `pgsql_users.password` to hold a SCRAM verifier or an `md5…` hash directly —
> exactly the formats `userlist.txt` already stores. When that lands, this
> section and the strict-mode error in `convert_users()` should be revisited so a
> `userlist.txt` imports as-is. Note the constraints that come with it: an
> md5-stored user needs an md5 backend, a verifier-stored user needs a
> `scram-sha-256` backend, and the verifier must be byte-identical to the
> backend's `rolpassword` (same salt and iterations).

**`dbname=` aliases.** PgBouncer's `dbname=` connects to a backend database under
a different name than the client asked for. ProxySQL routes to a hostgroup but
does not rewrite the database in the startup packet, so the alias cannot be
honoured.

**HBA `reject` rules.** The ProxySQL firewall whitelist is allow-only and has no
deny entry, so a `reject` that precedes a broader allow cannot be reproduced —
the allow would win. Enforce the denial upstream.

**HBA `local` / `hostnossl` records**, and the `cert`, `peer`, `pam`, `ident`,
`gss` and `sspi` methods.

**Clustering.** `peer_id` and the `[peers]` section — use ProxySQL Cluster.

**No equivalent.** `so_reuseport`, `disable_pqexec`, `application_name_add_host`,
`dns_zone_check_period`, `resolv_conf`, a non-default `server_reset_query`,
`sbuf_loopcnt`, `pkt_buf`.

---

## 5. PgBouncer `SHOW` commands

Available on the **PostgreSQL admin port**. Each command also accepts a
`SHOW EXTENDED <command>` form that appends ProxySQL-specific columns to the
right of the PgBouncer-compatible ones, so a tool reading by column position or
by the documented PgBouncer names keeps working.

| Command | Backed by |
|---|---|
| `SHOW POOLS` | `stats_pgsql_connection_pool` |
| `SHOW STATS` | `stats_pgsql_query_digest` |
| `SHOW SERVERS` | `stats_pgsql_connection_pool` (+ `runtime_pgsql_servers` when extended) |
| `SHOW CLIENTS` | `stats_pgsql_processlist` |
| `SHOW DATABASES` | `runtime_pgsql_servers` |
| `SHOW USERS` | `runtime_pgsql_users` |
| `SHOW CONFIG` | `global_variables` (`pgsql-` prefix stripped) |
| `SHOW VERSION` | identifies as ProxySQL in PgBouncer compatibility mode |
| `SHOW STATE` | always `active` |
| `SHOW LISTS` | object counts |

Rejected with an explanatory error: `SHOW DNS_HOSTS`, `SHOW DNS_ZONES`,
`SHOW FDS`, `SHOW PEERS`, `SHOW PEER_POOLS`, `SHOW MEM`, `SHOW ACTIVE_SOCKETS`,
`SHOW SOCKETS`.

Some columns are structurally absent from ProxySQL and are reported as `0` or the
empty string rather than omitted, so the column count stays stable: per-client
wait times, `local_addr`/`local_port` on servers, socket pointers (`ptr`, `link`)
and `prepared_statements`.

### Interaction with ProxySQL's own `SHOW`

The translation runs **before** ProxySQL's generic `SHOW` handling, because
ProxySQL already owns some of the same command words — `SHOW DATABASES` most
notably. Only the exact command set above is claimed: anything with a trailing
token (`SHOW POOLS foo`) falls through to normal admin handling, as does every
other `SHOW`.

---

## 6. Tests

| Test | Covers |
|---|---|
| `test/tap/tests/unit/pgbouncer_config_parser_unit-t` | INI / userlist / HBA parsing, quoting, includes, malformed input |
| `test/tap/tests/unit/pgbouncer_converter_unit-t` | mapping rules, strict vs relaxed, generated column names |
| `test/tap/tests/unit/pgbouncer_show_commands_unit-t` | `SHOW` translation and rejection |
| `test/tap/tests/pgsql-pgbouncer_compat-t` | **executes** every `SHOW` and every generated statement against a live admin port |

The integration test matters disproportionately here. The unit tests compare
generated strings, which cannot catch a wrong column name — and three such
defects (`pgsql_query_rules.schemaname`, `stats_pgsql_processlist.db`, and
reading `weight`/`max_connections` from `stats_pgsql_connection_pool`) passed the
full unit suite while being unable to execute. Any new mapping or `SHOW`
translation must be exercised by `pgsql-pgbouncer_compat-t`, not by a string
comparison alone.
14 changes: 14 additions & 0 deletions docker/images/proxysql/deb-compliant/ctl/proxysql.ctl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Description: High performance MySQL and PostgreSQL proxy
It features query routing, query caching, query rewriting (for queries generated by ORMs, for example) and is most of the time a drop-in replacement for mysqld and postgres from the point of view of the application. It can be configured and remote controlled through an SQL-compatible admin interface.
File: postinst
#!/bin/sh -e
# proxysql-cli is the same binary under a second name; main() dispatches on
# argv[0] (see src/main.cpp) and never starts the daemon in that mode. equivs
# `Files:` copies regular files only, so the link is made here.
ln -sf proxysql /usr/bin/proxysql-cli

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: postinst runs ln -sf proxysql /usr/bin/proxysql-cli on every install and every upgrade, and -f unconditionally unlinks whatever currently sits at /usr/bin/proxysql-cli. A pre-existing file or symlink placed there by an operator or another package is silently destroyed the first time ProxySQL is installed or upgraded. Guard the create so an existing path is not clobbered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker/images/proxysql/deb-compliant/ctl/proxysql.ctl, line 31:

<comment>postinst runs `ln -sf proxysql /usr/bin/proxysql-cli` on every install *and* every upgrade, and `-f` unconditionally unlinks whatever currently sits at `/usr/bin/proxysql-cli`. A pre-existing file or symlink placed there by an operator or another package is silently destroyed the first time ProxySQL is installed or upgraded. Guard the create so an existing path is not clobbered.</comment>

<file context>
@@ -25,6 +25,10 @@ Description: High performance MySQL and PostgreSQL proxy
+ # proxysql-cli is the same binary under a second name; main() dispatches on
+ # argv[0] (see src/main.cpp) and never starts the daemon in that mode. equivs
+ # `Files:` copies regular files only, so the link is made here.
+ ln -sf proxysql /usr/bin/proxysql-cli
  if [ ! -d /var/lib/proxysql ]; then mkdir /var/lib/proxysql ; fi
  if ! id -u proxysql > /dev/null 2>&1; then useradd -r -U -s /bin/false  -d /var/lib/proxysql -c "ProxySQL Server"  proxysql; fi
</file context>
Suggested change
ln -sf proxysql /usr/bin/proxysql-cli
if [ ! -e /usr/bin/proxysql-cli ]; then ln -s proxysql /usr/bin/proxysql-cli ; fi

if [ ! -d /var/lib/proxysql ]; then mkdir /var/lib/proxysql ; fi
if ! id -u proxysql > /dev/null 2>&1; then useradd -r -U -s /bin/false -d /var/lib/proxysql -c "ProxySQL Server" proxysql; fi
chown -R proxysql: /var/lib/proxysql
Expand All @@ -34,3 +38,13 @@ File: postinst
systemctl enable proxysql.service > /dev/null || true
systemctl --system daemon-reload > /dev/null || true
fi
File: postrm
#!/bin/sh -e
# Remove the proxysql-cli symlink created by postinst. dpkg does not track it
# (equivs `Files:` handles regular files only), so it has to be cleaned up
# explicitly or it would be left dangling after the binary is removed.
case "$1" in
remove|purge)
if [ -L /usr/bin/proxysql-cli ]; then rm -f /usr/bin/proxysql-cli ; fi
;;
esac
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ mkdir -p %{buildroot}
cp -a * %{buildroot}
mkdir -p %{buildroot}/var/run/%{name}
mkdir -p %{buildroot}/var/lib/%{name}
# proxysql-cli is the same binary under a second name; main() dispatches on
# argv[0] (see src/main.cpp) and never starts the daemon in that mode. Shipped
# as a real symlink so rpm owns it and removes it on uninstall; the existing
# %{_bindir}/* glob in %files already picks it up.
ln -sf proxysql %{buildroot}%{_bindir}/proxysql-cli

%clean
rm -rf %{buildroot}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ mkdir -p %{buildroot}
cp -a * %{buildroot}
mkdir -p %{buildroot}/var/run/%{name}
mkdir -p %{buildroot}/var/lib/%{name}
# proxysql-cli is the same binary under a second name; main() dispatches on
# argv[0] (see src/main.cpp) and never starts the daemon in that mode. Shipped
# as a real symlink so rpm owns it and removes it on uninstall; the existing
# %{_bindir}/* glob in %files already picks it up.
ln -sf proxysql %{buildroot}%{_bindir}/proxysql-cli

%clean
rm -rf %{buildroot}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ exec "${BIN_DIR}/proxysql.bin" "$@"
EOF
chmod 0755 "pkgroot/${DIR_NAME}/bin/proxysql"

# proxysql-cli: the same binary, selected by argv[0]. main() compares the
# basename against "proxysql-cli" exactly, and the wrapper above execs
# proxysql.bin -- which would make argv[0] "proxysql.bin". So the wrapper for
# the CLI execs a correctly-named symlink under libexec/ instead. `exec -a` is
# not available in POSIX sh, which is why this needs the extra indirection.
mkdir -p "pkgroot/${DIR_NAME}/libexec"
ln -sf ../bin/proxysql.bin "pkgroot/${DIR_NAME}/libexec/proxysql-cli"
cat > "pkgroot/${DIR_NAME}/bin/proxysql-cli" <<'EOF'
#!/bin/sh
set -eu

BIN_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
export LD_LIBRARY_PATH="${BIN_DIR}/../lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
exec "${BIN_DIR}/../libexec/proxysql-cli" "$@"
EOF
chmod 0755 "pkgroot/${DIR_NAME}/bin/proxysql-cli"

bundle_runtime_library() {
local soname="$1"
local resolved_path
Expand Down
35 changes: 35 additions & 0 deletions include/PgBouncer_AuthFileParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef __CLASS_PGBOUNCER_AUTH_FILE_PARSER_H
#define __CLASS_PGBOUNCER_AUTH_FILE_PARSER_H

#include "PgBouncer_Config.h"
#include <string>
#include <vector>

namespace PgBouncer {

class AuthFileParser {
public:
// Parse a PgBouncer userlist.txt file.
// Format: "username" "password" per line
// Password types detected:
// - Plain text: any string not matching MD5 or SCRAM patterns
// - MD5: starts with "md5" followed by 32 hex chars
// - SCRAM: starts with "SCRAM-SHA-256$"
// Double-quote escaping: "" inside quoted strings represents a literal "
bool parse(const std::string& filepath,
std::vector<AuthFileEntry>& entries,
std::vector<ParseMessage>& errors);

private:
// Parse a double-quoted string starting at pos, advancing pos past the closing quote.
// Returns the unescaped content. Returns false if malformed.
static bool parse_quoted_string(const std::string& line, size_t& pos,
std::string& result);

// Detect password type from the raw password string
static AuthType detect_password_type(const std::string& password);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

} // namespace PgBouncer

#endif // __CLASS_PGBOUNCER_AUTH_FILE_PARSER_H
Loading
Loading