Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ecb20a4
Add psycopg package.
clokep Nov 9, 2023
c7e3120
Method to set statement timeout.
clokep Nov 15, 2023
91ef287
Separate PostgresEngine into Psycopg2Engine and PsycopgEngine.
clokep Nov 9, 2023
0a06be1
Run tests in CI against psycopg.
clokep Nov 9, 2023
260a5b7
Update user directory to handle psycopg3.
clokep Oct 9, 2024
ce8ad96
Support execute_values on psycopg3.
clokep Oct 9, 2024
93b1740
Fix-up simple_* tests.
clokep Oct 10, 2024
d49827d
Fix-up calls to end_to_end_keys.
clokep Oct 10, 2024
287f0a6
Use superclass version of `executescript()` (#1)
realtyem Oct 17, 2024
9f77ac4
Switch out formatting placeholder for what psycopg2 is expecting (#2)
realtyem Oct 17, 2024
3bbd562
Merge remote-tracking branch 'upstream/develop' into psycopg3
clokep Oct 23, 2024
f5b6429
Linting (and a fix) (#3)
realtyem Oct 23, 2024
7ff4584
Merge remote-tracking branch 'refs/remotes/origin/psycopg3' into psyc…
clokep Oct 23, 2024
5353f8d
Try running complement builds?
clokep Oct 24, 2024
1bec3d7
Merge branch 'develop' into psycopg3
realtyem Jul 21, 2025
f6c2364
Adjust type: ignore line to where mypy will apply it
realtyem Jul 21, 2025
279791d
Add both PsycopgEngine and Psycopg2Engine to database.engines.__all__…
realtyem Jul 21, 2025
1ceb332
Adjust _mark_state_groups_as_pending_deletion_txn() to use execute_ba…
realtyem Jul 21, 2025
95eb7f8
Adjust set_profile_field() insertion sql for a narrower type on param…
realtyem Jul 22, 2025
4ad733f
Try an update to the config schema?
realtyem Jul 22, 2025
f65a885
Adjust unit tests to reflect updated minimal versions of python/postgres
realtyem Jul 22, 2025
425971f
Minor changes to profile field
clokep Sep 30, 2025
61a3aaa
Merge remote-tracking branch 'upstream/develop' into psycopg3
clokep Sep 30, 2025
f31d8d2
Newsfragment
clokep Sep 30, 2025
ad229a9
Merge remote-tracking branch 'upstream/develop' into psycopg3
clokep Sep 30, 2025
2f4352b
poetry lock again
clokep Sep 30, 2025
467a8c4
Merge branch 'develop' into psycopg3
clokep Oct 6, 2025
e59ea08
Merge branch 'develop' into psycopg3-adjust-settings
realtyem Nov 23, 2025
d9226e0
Update poetry.lock file
realtyem Nov 23, 2025
0693794
linting fixups
realtyem Nov 23, 2025
dcc4b5a
Merge pull request #4 from realtyem/psycopg3-adjust-settings
clokep Nov 25, 2025
c195ee6
Merge branch 'develop' into jason/psycopg-merge-develop
jason-famedly Jan 30, 2026
99ff2ac
Adjust for execute_values() usage with fetch=False to only be appropr…
jason-famedly Jan 30, 2026
698a56d
Attempt forcing a lower case comparison to "psycopg" in complement.sh
jason-famedly Jan 31, 2026
088ead3
Add mini docstrings to the abstract postgres engine class, as well as…
jason-famedly Jan 31, 2026
123aeac
I forgot that the new driver is not wired up in the complement script…
jason-famedly Feb 2, 2026
680ffee
Merge pull request #6 from jason-famedly/jason/psycopg-merge-develop
clokep Feb 3, 2026
4cd7924
Use the 'c' extra instead of the 'pure python' version for C-compiled…
jason-famedly Feb 3, 2026
a320d8a
Update 'c' extra to 3.2.8 as a minimum. This avoids olddebian's packa…
jason-famedly Feb 19, 2026
d94d4c0
Allow complement to be ran with WORKERS=1 and still allow the psycopg…
jason-famedly Feb 19, 2026
73415a2
Merge pull request #7 from jason-famedly/jason/adjust-for-feedback
clokep Feb 19, 2026
0e083b1
Merge branch 'develop' into jason/update-psycopg3
jason-famedly Mar 30, 2026
63c7f03
update poetry lock
jason-famedly Mar 30, 2026
0af6799
Merge branch 'develop' into jason/update-psycopg3
jason-famedly Apr 6, 2026
5c4f8a9
Merge pull request #8 from jason-famedly/jason/update-psycopg3
clokep Apr 6, 2026
efcb28c
Merge branch 'develop' into jason/update-poetry-lock
jason-famedly Apr 28, 2026
59cd733
Merge pull request #9 from jason-famedly/jason/update-poetry-lock
clokep Apr 28, 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
31 changes: 23 additions & 8 deletions .ci/scripts/calculate_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,38 @@ def set_output(key: str, value: str):
for version in ("3.9", "3.10", "3.11", "3.12", "3.13")
)

# Run with both psycopg2 and psycopg.
trial_postgres_tests = [
{
"python-version": "3.8",
"database": "postgres",
"postgres-version": "11",
"extras": "all",
}
},
{
"python-version": "3.8",
"database": "psycopg",
"postgres-version": "11",
"extras": "all",
},
]

if not IS_PR:
trial_postgres_tests.append(
{
"python-version": "3.13",
"database": "postgres",
"postgres-version": "17",
"extras": "all",
}
trial_postgres_tests.extend(
[
{
"python-version": "3.13",
"database": "postgres",
"postgres-version": "17",
"extras": "all",
},
{
"python-version": "3.13",
"database": "psycopg",
"postgres-version": "17",
"extras": "all",
},
]
)

trial_no_extra_tests = [
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,9 @@ jobs:
run: until pg_isready -h localhost; do sleep 1; done
- run: poetry run trial --jobs=6 tests
env:
SYNAPSE_POSTGRES: ${{ matrix.job.database == 'postgres' || '' }}
# If matrix.job.database is 'psycopg' set SYNAPSE_POSTGRES to that string;
# otherwise if it is 'postgres' set it to true. Otherwise, empty.
SYNAPSE_POSTGRES: ${{ matrix.job.database == 'psycopg' && 'psycopg' || matrix.job.database == 'postgres' || '' }}
SYNAPSE_POSTGRES_HOST: /var/run/postgresql
SYNAPSE_POSTGRES_USER: postgres
SYNAPSE_POSTGRES_PASSWORD: postgres
Expand Down Expand Up @@ -654,6 +656,9 @@ jobs:
- arrangement: workers
database: Postgres

- arrangement: workers
database: Psycopg

steps:
- name: Run actions/checkout@v4 for synapse
uses: actions/checkout@v4
Expand All @@ -678,7 +683,7 @@ jobs:
COMPLEMENT_DIR=`pwd`/complement synapse/scripts-dev/complement.sh -p 1 -json 2>&1 | synapse/.ci/scripts/gotestfmt
shell: bash
env:
POSTGRES: ${{ (matrix.database == 'Postgres') && 1 || '' }}
POSTGRES: ${{ (matrix.database == 'Postgres' || matrix.database == 'Psycopg') && matrix.database || '' }}
WORKERS: ${{ (matrix.arrangement == 'workers') && 1 || '' }}
name: Run Complement Tests

Expand Down
12 changes: 12 additions & 0 deletions docker/complement/conf/start_for_complement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export SYNAPSE_REPORT_STATS=no
case "$SYNAPSE_COMPLEMENT_DATABASE" in
postgres)
# Set postgres authentication details which will be placed in the homeserver config file
export POSTGRES_DRIVER=psycopg2
export POSTGRES_PASSWORD=somesecret
export POSTGRES_USER=postgres
export POSTGRES_HOST=localhost

# configure supervisord to start postgres
export START_POSTGRES=true
;;

psycopg)
# Set postgres authentication details which will be placed in the homeserver config file
export POSTGRES_DRIVER=psycopg
export POSTGRES_PASSWORD=somesecret
export POSTGRES_USER=postgres
export POSTGRES_HOST=localhost
Expand Down
4 changes: 2 additions & 2 deletions docker/conf/homeserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ listeners:

{% if POSTGRES_PASSWORD %}
database:
name: "psycopg2"
name: "{{ POSTGRES_DRIVER or "psycopg2" }}"
args:
user: "{{ POSTGRES_USER or "synapse" }}"
password: "{{ POSTGRES_PASSWORD }}"
database: "{{ POSTGRES_DB or "synapse" }}"
dbname: "{{ POSTGRES_DB or "synapse" }}"
{% if not SYNAPSE_USE_UNIX_SOCKET %}
{# Synapse will use a default unix socket for Postgres when host/port is not specified (behavior from `psycopg2`). #}
host: "{{ POSTGRES_HOST or "db" }}"
Expand Down
70 changes: 67 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ matrix-synapse-ldap3 = { version = ">=0.1", optional = true }
psycopg2 = { version = ">=2.8", markers = "platform_python_implementation != 'PyPy'", optional = true }
psycopg2cffi = { version = ">=2.8", markers = "platform_python_implementation == 'PyPy'", optional = true }
psycopg2cffi-compat = { version = "==1.1", markers = "platform_python_implementation == 'PyPy'", optional = true }
psycopg = { version = "^3.1", optional = true }
pysaml2 = { version = ">=4.5.0", optional = true }
authlib = { version = ">=0.15.1", optional = true }
# systemd-python is necessary for logging to the systemd journal via
Expand All @@ -257,6 +258,7 @@ pyicu = { version = ">=2.10.2", optional = true }
# twice: once here, and once in the `all` extra.
matrix-synapse-ldap3 = ["matrix-synapse-ldap3"]
postgres = ["psycopg2", "psycopg2cffi", "psycopg2cffi-compat"]
psycopg = ["psycopg"]
saml2 = ["pysaml2"]
oidc = ["authlib"]
# systemd-python is necessary for logging to the systemd journal via
Expand Down Expand Up @@ -293,7 +295,7 @@ all = [
# matrix-synapse-ldap3
"matrix-synapse-ldap3",
# postgres
"psycopg2", "psycopg2cffi", "psycopg2cffi-compat",
"psycopg2", "psycopg2cffi", "psycopg2cffi-compat", "psycopg",
# saml2
"pysaml2",
# oidc and jwt
Expand Down
10 changes: 8 additions & 2 deletions scripts-dev/complement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,11 @@ if [[ -n "$WORKERS" ]]; then
export PASS_SYNAPSE_WORKER_TYPES="$WORKER_TYPES"

# Workers can only use Postgres as a database.
export PASS_SYNAPSE_COMPLEMENT_DATABASE=postgres
if [[ "$POSTGRES" = "psycopg" ]]; then
export PASS_SYNAPSE_COMPLEMENT_DATABASE=psycopg
else
export PASS_SYNAPSE_COMPLEMENT_DATABASE=postgres
fi

# And provide some more configuration to complement.

Expand All @@ -256,7 +260,9 @@ if [[ -n "$WORKERS" ]]; then
export COMPLEMENT_SPAWN_HS_TIMEOUT_SECS=120
else
export PASS_SYNAPSE_COMPLEMENT_USE_WORKERS=
if [[ -n "$POSTGRES" ]]; then
if [[ "$POSTGRES" = "psycopg" ]]; then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

judging from the CI yaml this needs to be case-insensitive (to match Psycopg from the CI matrix.database); or am I wrong?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I imagine you are correct. I think I was using it from the command line and tested with it's lower case function. I will adjust to use the bash "${POSTGRES,,}}" pattern instead, which should lower case all the letters

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This actually opened a can of worms. You were correct to bring this up, as it turned out that Complement in CI was not using this driver for two reasons:

  1. I had not enabled that this driver would be recognized when in WORKERS=1 mode 🤦‍♂️ which was how the example testing was set up
  2. The Complement docker image will actually not run with psycopg in it's "pure python" installation method without significant changes, that I am not willing to attempt at this time. After 4 hours of exploring how to make this work I came to the conclusion that the easiest method to move past this is to not use the "pure python" installation but use either the "binary" or "c" installations instead. Neither of these have the same symptoms(these other installation types have the necessary libraries bundled with them)
some more details about point 2: This was manifesting as
synapse_main | **********************************************************************************
synapse_main |  Error during initialisation:
synapse_main |      Traceback (most recent call last):
synapse_main |        File "/usr/local/lib/python3.13/site-packages/synapse/app/homeserver.py", line 486, in main
synapse_main |          setup(hs)
synapse_main |          ~~~~~^^^^
synapse_main |        File "/usr/local/lib/python3.13/site-packages/synapse/app/homeserver.py", line 422, in setup
synapse_main |          hs.setup()
synapse_main |          ~~~~~~~~^^
synapse_main |        File "/usr/local/lib/python3.13/site-packages/synapse/server.py", line 636, in setup
synapse_main |          self.datastores = Databases(self.DATASTORE_CLASS, self)
synapse_main |                            ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
synapse_main |        File "/usr/local/lib/python3.13/site-packages/synapse/storage/databases/__init__.py", line 84, in __init__
synapse_main |          engine = create_engine(database_config.config)
synapse_main |        File "/usr/local/lib/python3.13/site-packages/synapse/storage/engines/__init__.py", line 74, in create_engine
synapse_main |          return PsycopgEngine(database_config)
synapse_main |        File "/usr/local/lib/python3.13/site-packages/synapse/storage/engines/__init__.py", line 48, in __new__
synapse_main |          raise RuntimeError(
synapse_main |              f"Cannot create {cls.__name__} -- psycopg module is not installed"
synapse_main |          )
synapse_main |      RuntimeError: Cannot create PsycopgEngine -- psycopg module is not installed
synapse_main |  
synapse_main |  There may be more information in the logs.
synapse_main | **********************************************************************************

Which suggested that the module of psycopg was not even installed. This module does appear during the pip installation logs in CI, so was a slight misdirection.

I then removed the exception catch for the importing of PsycopgEngine hoping it would give me more information. It did.

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/usr/local/lib/python3.13/site-packages/synapse/app/homeserver.py", line 42, in <module>
    from synapse.app import _base
  File "/usr/local/lib/python3.13/site-packages/synapse/app/_base.py", line 75, in <module>
    from synapse.events.auto_accept_invites import InviteAutoAccepter
  File "/usr/local/lib/python3.13/site-packages/synapse/events/auto_accept_invites.py", line 28, in <module>
    from synapse.module_api import EventBase, ModuleApi, run_as_background_process
  File "/usr/local/lib/python3.13/site-packages/synapse/module_api/__init__.py", line 58, in <module>
    from synapse.handlers.auth import (
    ...<7 lines>...
    )
  File "/usr/local/lib/python3.13/site-packages/synapse/handlers/auth.py", line 57, in <module>
    from synapse.api.ratelimiting import Ratelimiter
  File "/usr/local/lib/python3.13/site-packages/synapse/api/ratelimiting.py", line 27, in <module>
    from synapse.storage.databases.main import DataStore
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/__init__.py", line 40, in <module>
    from synapse.storage.databases import Databases
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/databases/__init__.py", line 26, in <module>
    from synapse.storage._base import SQLBaseStore
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/_base.py", line 26, in <module>
    from synapse.storage.database import (
    ...<3 lines>...
    )
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/database.py", line 61, in <module>
    from synapse.storage.background_updates import BackgroundUpdater
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/background_updates.py", line 39, in <module>
    from synapse.storage.engines import PostgresEngine
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/engines/__init__.py", line 43, in <module>
    from .psycopg import PsycopgEngine
  File "/usr/local/lib/python3.13/site-packages/synapse/storage/engines/psycopg.py", line 18, in <module>
    import psycopg
  File "/usr/local/lib/python3.13/site-packages/psycopg/__init__.py", line 9, in <module>
    from . import pq  # noqa: F401 import early to stabilize side effects
    ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/psycopg/pq/__init__.py", line 116, in <module>
    import_from_libpq()
    ~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/psycopg/pq/__init__.py", line 108, in import_from_libpq
            raise ImportError(
    ...<4 lines>...
            )
ImportError: no pq wrapper available.
Attempts made:
- couldn't import psycopg 'c' implementation: No module named 'psycopg_c'
- couldn't import psycopg 'binary' implementation: No module named 'psycopg_binary'
- couldn't import psycopg 'python' implementation: libpq library not found

Inspection of the functions being called here(and some minor sleuthing in the psycopg issues page) led me to the conclusion that the ctypes.util.find_library("pq") was not finding what it wanted, namely the system level library of libpq5. This is installed in the base docker image, and does appear to be in the filesystem at the correct place in the final complement image. But was not found by this function anyway. Using a rough apt install -y libpq5 in the final complement image did allow it to work. But why was not found even though it was present? The issues tracker on psycopg did suggest that it could have also been caused by passing around data between distro-less docker images, this does not appear to be the case here at any point.

Since the next stages of introducing psycopg support to Synapse will be changing the installation method to either binary or c anyway, spending time to figure out why this is not working seems like a waste of time. This is the easy solution, it just means the time table gets moved up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After much testing and some debate, the c extra was chosen. The binary extra does not have a source distribution(only wheels) and building debian packages was failing(this appeared to be because the bundles libs were not being found, even though they were verifiably in the correct location. I suspected this was a RPATH issue).

The minimum version of psycopg chosen was originally v3.1.x. With the c extra this was causing the 'old deps' trial to fail. Bookworm packages a copy of libpq5 that was updated to support Postgres 18, but also broke psycopg prior to v3.2.8. Since we control the version of psycopg but not libpq5, the minimum version of psycopg was bumped to this version

export PASS_SYNAPSE_COMPLEMENT_DATABASE=psycopg
elif [[ -n "$POSTGRES" ]]; then
export PASS_SYNAPSE_COMPLEMENT_DATABASE=postgres
else
export PASS_SYNAPSE_COMPLEMENT_DATABASE=sqlite
Expand Down
2 changes: 1 addition & 1 deletion synapse/_scripts/synapse_port_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ def main() -> None:
if "name" not in postgres_config:
sys.stderr.write("Malformed database config: no 'name'\n")
sys.exit(2)
if postgres_config["name"] != "psycopg2":
if postgres_config["name"] not in ("psycopg", "psycopg2"):
sys.stderr.write("Database must use the 'psycopg2' connector.\n")
sys.exit(3)

Expand Down
2 changes: 1 addition & 1 deletion synapse/config/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class DatabaseConnectionConfig:
def __init__(self, name: str, db_config: dict):
db_engine = db_config.get("name", "sqlite3")

if db_engine not in ("sqlite3", "psycopg2"):
if db_engine not in ("sqlite3", "psycopg2", "psycopg"):
raise ConfigError("Unsupported database type %r" % (db_engine,))

if db_engine == "sqlite3":
Expand Down
13 changes: 7 additions & 6 deletions synapse/storage/background_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,8 @@ def create_index_psql(conn: "LoggingDatabaseConnection") -> None:
# postgres insists on autocommit for the index
conn.engine.attempt_to_set_autocommit(conn.conn, True)

assert isinstance(self.db_pool.engine, PostgresEngine)

try:
c = conn.cursor()

Expand All @@ -795,8 +797,7 @@ def create_index_psql(conn: "LoggingDatabaseConnection") -> None:

# override the global statement timeout to avoid accidentally squashing
# a long-running index creation process
timeout_sql = "SET SESSION statement_timeout = 0"
c.execute(timeout_sql)
self.db_pool.engine.set_statement_timeout(c, 0)

sql = (
"CREATE %(unique)s INDEX CONCURRENTLY %(name)s"
Expand All @@ -818,11 +819,11 @@ def create_index_psql(conn: "LoggingDatabaseConnection") -> None:
logger.debug("[SQL] %s", sql)
c.execute(sql)
finally:
# mypy ignore - `statement_timeout` is defined on PostgresEngine
# reset the global timeout to the default
default_timeout = self.db_pool.engine.statement_timeout # type: ignore[attr-defined]
undo_timeout_sql = f"SET statement_timeout = {default_timeout}"
conn.cursor().execute(undo_timeout_sql)
if self.db_pool.engine.statement_timeout is not None:
self.db_pool.engine.set_statement_timeout(
conn.cursor(), self.db_pool.engine.statement_timeout
)

conn.engine.attempt_to_set_autocommit(conn.conn, False)

Expand Down
Loading