Add RustPostgresEngine for the native Rust Postgres backend

A database engine that drives the Rust Connection/Cursor shim instead of
psycopg2. It subclasses PostgresEngine to reuse the pure SQL-generation and
configuration behaviour, and overrides only the parts that touch a live
connection or are wired to psycopg2 internals:

  - `module` points at the Rust backend's DBAPI2 exception hierarchy, which the
    transaction driver catches on;
  - `convert_param_style` rewrites `?` to `$1, $2, ...` (the shim binds libpq
    positional placeholders, not psycopg2's `%s`);
  - `in_transaction` / `is_connection_closed` / `attempt_to_set_autocommit` call
    the shim's own methods;
  - `is_deadlock` matches the Rust `DatabaseError` and its `pgcode`;
  - `executescript` uses the shim's multi-statement primitive;
  - `on_new_connection` is a no-op — session setup belongs in the Rust pool.

Per-transaction isolation-level overrides raise NotImplementedError for now, and
`check_database` / `server_version` still read psycopg2 attributes; the engine
is not yet selectable via `create_engine`, so neither is reached. Wiring it into
`make_pool` (which also feeds session config to the pool) is the next step.

Tested directly: param-style rewriting, deadlock/pgcode matching, the module
pointer, and — against a live shim connection — in_transaction, is_closed and
autocommit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
This commit is contained in:
Erik Johnston
2026-07-06 09:47:10 +00:00
co-authored by Claude Opus 4.8
parent b78e894663
commit da873902ab
2 changed files with 252 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
"""A database engine for the native Rust ``tokio-postgres`` backend.
This drives the Rust ``Connection`` / ``Cursor`` shim
(:mod:`synapse.synapse_rust.database.postgres`) rather than psycopg2. It reuses
:class:`PostgresEngine` for everything that is pure SQL generation or
configuration (``supports_using_any_list``, ``lock_table``, ``row_id_name``, …)
and overrides only the parts that touch a live connection or that are wired to
psycopg2 internals:
- the DBAPI2 exception ``module`` — the Rust backend has its own hierarchy;
- ``convert_param_style`` — the shim binds ``$1, $2, …`` placeholders, not
psycopg2's ``%s``;
- ``in_transaction`` / ``is_connection_closed`` / ``attempt_to_set_autocommit``
— served by the shim's own methods;
- ``is_deadlock`` — matches the Rust ``DatabaseError`` and its ``pgcode``;
- ``executescript`` — uses the shim's dedicated multi-statement primitive.
Per-connection session setup (isolation level, ``synchronous_commit``,
``statement_timeout``) lives in the Rust connection pool rather than in
``on_new_connection``, so that hook is a no-op here.
Not yet adapted (the engine is not yet selectable via ``create_engine``, so
these are not reached): ``check_database`` / ``server_version`` still read
psycopg2 connection attributes, and per-transaction isolation-level overrides
are unimplemented. Both are follow-ups for the full ``make_pool`` wiring.
"""
import logging
from typing import TYPE_CHECKING, Any, Mapping
from synapse.storage.engines._base import AUTO_INCREMENT_PRIMARY_KEYPLACEHOLDER
from synapse.storage.engines.postgres import PostgresEngine
from synapse.synapse_rust.database import postgres
if TYPE_CHECKING:
from synapse.storage.database import LoggingDatabaseConnection
logger = logging.getLogger(__name__)
# Deadlock / serialization-failure SQLSTATEs that Synapse retries.
_RETRYABLE_PGCODES = ("40001", "40P01")
class RustPostgresEngine(PostgresEngine):
"""A :class:`PostgresEngine` that talks to the Rust backend's shim."""
def __init__(self, database_config: Mapping[str, Any]):
super().__init__(database_config)
# Route the DBAPI2 exception hierarchy (OperationalError, DatabaseError,
# IntegrityError, …) to the Rust backend's classes; the transaction
# driver catches `engine.module.<Error>`.
self.module = postgres
def convert_param_style(self, sql: str) -> str:
# The shim binds positional `$1, $2, ...` placeholders (like libpq),
# not psycopg2's `%s`. Rewrite `?` left-to-right, matching the Rust-side
# `convert_placeholders`; callers must parameterise rather than embed a
# literal `?`.
out = []
n = 0
for ch in sql:
if ch == "?":
n += 1
out.append(f"${n}")
else:
out.append(ch)
return "".join(out)
def on_new_connection(self, db_conn: "LoggingDatabaseConnection") -> None:
# No-op: per-connection session setup happens in the Rust connection
# pool's connection manager, not here.
pass
def is_deadlock(self, error: Exception) -> bool:
if isinstance(error, postgres.DatabaseError):
return getattr(error, "pgcode", None) in _RETRYABLE_PGCODES
return False
def is_connection_closed(self, conn: Any) -> bool:
return bool(conn.is_closed())
def in_transaction(self, conn: Any) -> bool:
return bool(conn.in_transaction())
def attempt_to_set_autocommit(self, conn: Any, autocommit: bool) -> None:
conn.set_autocommit(autocommit)
def attempt_to_set_isolation_level(
self, conn: Any, isolation_level: int | None
) -> None:
# Per-transaction isolation overrides are not implemented for the shim
# yet; the connection's default level is set when the pool opens it.
raise NotImplementedError(
"per-transaction isolation levels are not supported by the Rust "
"Postgres backend yet"
)
@staticmethod
def executescript(cursor: Any, script: str) -> None:
# Use the shim's dedicated multi-statement primitive rather than
# psycopg2's "just execute it" behaviour. The script runs in the
# connection's current transaction (opened lazily) and is left open for
# the caller to commit.
script = script.replace(
AUTO_INCREMENT_PRIMARY_KEYPLACEHOLDER,
"BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY",
)
cursor.executescript(script)
+132
View File
@@ -0,0 +1,132 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
"""Tests for :class:`synapse.storage.engines.postgres_rust.RustPostgresEngine`."""
from typing import Any
from synapse.storage.engines.postgres_rust import RustPostgresEngine
from synapse.synapse_rust.database import postgres
from tests import unittest
from tests.unittest import skip_unless
from tests.utils import (
POSTGRES_BASE_DB,
POSTGRES_HOST,
POSTGRES_PASSWORD,
POSTGRES_PORT,
POSTGRES_USER,
USE_POSTGRES_FOR_TESTS,
)
def _build_dsn() -> str:
"""Build a libpq keyword/value connection string from the test config."""
parts = [f"dbname={POSTGRES_BASE_DB}"]
if POSTGRES_USER is not None:
parts.append(f"user={POSTGRES_USER}")
if POSTGRES_HOST is not None:
parts.append(f"host={POSTGRES_HOST}")
if POSTGRES_PORT is not None:
parts.append(f"port={POSTGRES_PORT}")
if POSTGRES_PASSWORD is not None:
parts.append(f"password={POSTGRES_PASSWORD}")
return " ".join(parts)
class RustPostgresEngineTestCase(unittest.TestCase):
"""The engine's connection-independent behaviour (no database needed)."""
def setUp(self) -> None:
self.engine = RustPostgresEngine({})
def test_module_is_the_rust_backend(self) -> None:
# The transaction driver catches `engine.module.<Error>`, so the module
# must be the Rust backend's DBAPI2 hierarchy, not psycopg2's.
self.assertIs(self.engine.module, postgres)
def test_convert_param_style_rewrites_to_dollar_placeholders(self) -> None:
self.assertEqual(
self.engine.convert_param_style("SELECT * FROM t WHERE a = ? AND b = ?"),
"SELECT * FROM t WHERE a = $1 AND b = $2",
)
# No placeholders: unchanged. Past nine: decimal, not a single digit.
self.assertEqual(self.engine.convert_param_style("SELECT 1"), "SELECT 1")
self.assertTrue(
self.engine.convert_param_style("VALUES " + "(?)" * 11).endswith("($11)")
)
def test_is_deadlock_matches_rust_database_error_pgcodes(self) -> None:
for pgcode in ("40001", "40P01"):
err = postgres.DatabaseError("boom")
err.pgcode = pgcode
self.assertTrue(self.engine.is_deadlock(err))
# A DatabaseError with some other pgcode is not a deadlock.
other = postgres.DatabaseError("nope")
other.pgcode = "23505"
self.assertFalse(self.engine.is_deadlock(other))
# Neither is an unrelated exception.
self.assertFalse(self.engine.is_deadlock(ValueError("unrelated")))
def test_isolation_level_override_not_yet_supported(self) -> None:
with self.assertRaises(NotImplementedError):
self.engine.attempt_to_set_isolation_level(object(), None)
@skip_unless(
bool(USE_POSTGRES_FOR_TESTS), "requires a Postgres server (set SYNAPSE_POSTGRES)"
)
class RustPostgresEngineConnectionTestCase(unittest.TestCase):
"""The engine's connection-touching checks, against a live shim connection."""
def setUp(self) -> None:
self.engine = RustPostgresEngine({})
self.pool = postgres.ConnectionPool(_build_dsn())
self.conn = self.pool.connect()
def tearDown(self) -> None:
del self.conn
self.pool.close()
def _exec(self, sql: str) -> Any:
cursor = self.conn.cursor()
cursor.execute(sql)
return cursor
def test_in_transaction_tracks_the_transaction(self) -> None:
self.assertFalse(self.engine.in_transaction(self.conn))
# The first statement lazily opens a transaction.
self._exec("SELECT 1")
self.assertTrue(self.engine.in_transaction(self.conn))
self.conn.commit()
self.assertFalse(self.engine.in_transaction(self.conn))
def test_is_connection_closed(self) -> None:
self.assertFalse(self.engine.is_connection_closed(self.conn))
self.conn.close()
self.assertTrue(self.engine.is_connection_closed(self.conn))
def test_attempt_to_set_autocommit(self) -> None:
# In autocommit mode the shim issues no implicit BEGIN, so a statement
# does not open a transaction.
self.engine.attempt_to_set_autocommit(self.conn, True)
self._exec("SELECT 1")
self.assertFalse(self.engine.in_transaction(self.conn))
# Turning it back off restores the transactional default.
self.engine.attempt_to_set_autocommit(self.conn, False)
self._exec("SELECT 1")
self.assertTrue(self.engine.in_transaction(self.conn))
self.conn.rollback()