From 9a6749edf46e01a80b92cb98c512f936aa488470 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Fri, 2 Oct 2026 11:08:47 -0700 Subject: [PATCH] Support PostgreSQL 19 --- .github/workflows/install-postgres.sh | 7 +++- .github/workflows/tests.yml | 2 +- README.rst | 2 +- asyncpg/protocol/codecs/pgproto.pyx | 3 +- asyncpg/protocol/pgtypes.pxi | 6 +++- docs/index.rst | 2 +- docs/usage.rst | 4 +++ tests/test_codecs.py | 48 ++++++++++++++++++++++++++- tools/generate_exceptions.py | 9 +++++ tools/generate_type_map.py | 21 +++++++++--- 10 files changed, 92 insertions(+), 12 deletions(-) diff --git a/.github/workflows/install-postgres.sh b/.github/workflows/install-postgres.sh index 733c7033..ad0cc4ac 100755 --- a/.github/workflows/install-postgres.sh +++ b/.github/workflows/install-postgres.sh @@ -22,7 +22,12 @@ if [[ $OSTYPE == linux* ]]; then apt-get install -y --no-install-recommends curl gnupg ca-certificates curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - mkdir -p /etc/apt/sources.list.d/ - echo "deb https://apt.postgresql.org/pub/repos/apt/ ${VERSION_CODENAME}-pgdg main" \ + PGDG_COMPONENTS="main" + if [[ ${PGVERSION} == 19 ]]; then + # PostgreSQL beta packages live in a separate component. + PGDG_COMPONENTS+=" 19" + fi + echo "deb https://apt.postgresql.org/pub/repos/apt/ ${VERSION_CODENAME}-pgdg ${PGDG_COMPONENTS}" \ >> /etc/apt/sources.list.d/pgdg.list apt-get update apt-get install -y --no-install-recommends \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a8a91bdf..aaff688c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -141,7 +141,7 @@ jobs: if: needs.classify-tests.outputs.is_release != 'true' strategy: matrix: - postgres-version: ["9.5", "9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18"] + postgres-version: ["9.5", "9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"] runs-on: ubuntu-24.04 diff --git a/README.rst b/README.rst index 1a37296d..73e795fc 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ framework. You can read more about asyncpg in an introductory `blog post `_. asyncpg requires Python 3.9 or later and is supported for PostgreSQL -versions 9.5 to 18. Other PostgreSQL versions or other databases +versions 9.5 to 19. Other PostgreSQL versions or other databases implementing the PostgreSQL protocol *may* work, but are not being actively tested. diff --git a/asyncpg/protocol/codecs/pgproto.pyx b/asyncpg/protocol/codecs/pgproto.pyx index 51d650d0..4bcdd743 100644 --- a/asyncpg/protocol/codecs/pgproto.pyx +++ b/asyncpg/protocol/codecs/pgproto.pyx @@ -235,7 +235,7 @@ cdef init_pseudo_codecs(): # 64-bit OID types oid8_types = [ - XID8OID, + XID8OID, OID8OID, ] for oid_type in oid8_types: @@ -253,6 +253,7 @@ cdef init_pseudo_codecs(): REGPROCOID, REGPROCEDUREOID, REGOPEROID, REGOPERATOROID, REGCLASSOID, REGTYPEOID, REGCONFIGOID, REGDICTIONARYOID, REGNAMESPACEOID, REGROLEOID, REFCURSOROID, REGCOLLATIONOID, + REGDATABASEOID, ] for reg_type in reg_types: diff --git a/asyncpg/protocol/pgtypes.pxi b/asyncpg/protocol/pgtypes.pxi index 86f8e663..f5f3ed1f 100644 --- a/asyncpg/protocol/pgtypes.pxi +++ b/asyncpg/protocol/pgtypes.pxi @@ -10,7 +10,7 @@ DEF INVALIDOID = 0 DEF MAXBUILTINOID = 9999 -DEF MAXSUPPORTEDOID = 5080 +DEF MAXSUPPORTEDOID = 6490 DEF BOOLOID = 16 DEF BYTEAOID = 17 @@ -112,6 +112,8 @@ DEF ANYCOMPATIBLEOID = 5077 DEF ANYCOMPATIBLEARRAYOID = 5078 DEF ANYCOMPATIBLENONARRAYOID = 5079 DEF ANYCOMPATIBLERANGEOID = 5080 +DEF OID8OID = 6437 +DEF REGDATABASEOID = 6490 ARRAY_TYPES = {_TEXTOID, _OIDOID} @@ -164,6 +166,7 @@ BUILTIN_TYPE_OID_MAP = { MONEYOID: 'money', NAMEOID: 'name', NUMERICOID: 'numeric', + OID8OID: 'oid8', OIDOID: 'oid', OPAQUEOID: 'opaque', PATHOID: 'path', @@ -183,6 +186,7 @@ BUILTIN_TYPE_OID_MAP = { REGCLASSOID: 'regclass', REGCOLLATIONOID: 'regcollation', REGCONFIGOID: 'regconfig', + REGDATABASEOID: 'regdatabase', REGDICTIONARYOID: 'regdictionary', REGNAMESPACEOID: 'regnamespace', REGOPERATOROID: 'regoperator', diff --git a/docs/index.rst b/docs/index.rst index 5ca0bef0..f2ac5695 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ of PostgreSQL server binary protocol for use with Python's ``asyncio`` framework. **asyncpg** requires Python 3.9 or later and is supported for PostgreSQL -versions 9.5 to 18. Other PostgreSQL versions or other databases implementing +versions 9.5 to 19. Other PostgreSQL versions or other databases implementing the PostgreSQL protocol *may* work, but are not being actively tested. Contents diff --git a/docs/usage.rst b/docs/usage.rst index e490a2ef..e9475653 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -138,6 +138,10 @@ The table below shows the correspondence between PostgreSQL and Python types. | ``integer``, | | | ``bigint`` | | +----------------------+-----------------------------------------------------+ +| ``oid8`` | :class:`int ` | ++----------------------+-----------------------------------------------------+ +| ``regdatabase`` | :class:`str ` | ++----------------------+-----------------------------------------------------+ | ``numeric`` | :class:`Decimal ` | +----------------------+-----------------------------------------------------+ | ``json``, ``jsonb`` | :class:`str ` | diff --git a/tests/test_codecs.py b/tests/test_codecs.py index f466349a..456d83fa 100644 --- a/tests/test_codecs.py +++ b/tests/test_codecs.py @@ -461,7 +461,21 @@ def _system_timezone(): 0, 10, 4294967295 - ]) + ]), + ('oid8', 'oid8', ( + 0, + 1, + 2 ** 32, + 2 ** 63, + 2 ** 64 - 1, + ), (19, 0)), + ('regdatabase', 'regdatabase', ( + 'postgres', + 'template0', + 'template1', + '-', + '4294967295', + ), (19, 0)), ] @@ -552,6 +566,38 @@ async def test_all_builtin_types_handled(self): codec, 'core type {} ({}) is unhandled'.format(typename, oid)) + async def test_oid8(self): + if self.server_version < (19, 0): + self.skipTest('oid8 requires PostgreSQL 19 or later') + + values = [0, 2 ** 32, 2 ** 63, 2 ** 64 - 1, None] + result = await self.con.fetchval('SELECT $1::oid8[]', values) + self.assertEqual(result, values) + + for value in (-1, 2 ** 64, 2 ** 256): + with self.subTest(value=value): + with self.assertRaisesRegex( + asyncpg.DataError, 'value out of uint64 range'): + await self.con.fetchval('SELECT $1::oid8', value) + + # A text fallback would accept this; the integer codec must reject it. + with self.assertRaises(asyncpg.DataError): + await self.con.fetchval('SELECT $1::oid8', '42') + + async def test_regdatabase(self): + if self.server_version < (19, 0): + self.skipTest('regdatabase requires PostgreSQL 19 or later') + + oid = await self.con.fetchval( + "SELECT oid FROM pg_database WHERE datname = 'postgres'") + result = await self.con.fetchval('SELECT $1::regdatabase', str(oid)) + self.assertEqual(result, 'postgres') + + result = await self.con.fetchval( + "SELECT current_database()::regdatabase") + self.assertEqual(result, await self.con.fetchval( + 'SELECT current_database()')) + async def test_void(self): res = await self.con.fetchval('select pg_sleep(0)') self.assertIsNone(res) diff --git a/tools/generate_exceptions.py b/tools/generate_exceptions.py index 0b626558..a36b65e4 100755 --- a/tools/generate_exceptions.py +++ b/tools/generate_exceptions.py @@ -76,6 +76,15 @@ def main(): with open(args.errcodesfile, 'r') as errcodes_f: errcodes = errcodes_f.read() + # Removed in PostgreSQL 17, but still raised by older supported servers. + if not re.search(r'^72000\s', errcodes, re.MULTILINE): + errcodes = errcodes.replace( + 'Section: Class F0', + 'Section: Class 72 - Snapshot Failure\n\n' + '72000 E ERRCODE_SNAPSHOT_TOO_OLD snapshot_too_old\n\n' + 'Section: Class F0', + ) + section_re = re.compile(r'^Section: .*') tpl = """\ diff --git a/tools/generate_type_map.py b/tools/generate_type_map.py index b4e90664..d19f261f 100755 --- a/tools/generate_type_map.py +++ b/tools/generate_type_map.py @@ -23,6 +23,15 @@ # postgresql/src/include/access/transam.h: FirstBootstrapObjectId _MAXBUILTINOID = 10000 - 1 +# Types removed from newer PostgreSQL versions that we still support. +_LEGACY_TYPES = { + 210: 'smgr', + 702: 'abstime', + 703: 'reltime', + 704: 'tinterval', + 2282: 'opaque', +} + # A list of alternative names for builtin types. _TYPE_ALIASES = { 'smallint': 'int2', @@ -78,9 +87,11 @@ async def runner(args): typemap = {} array_types = [] - for pg_type in pg_types: - typeoid = pg_type['oid'] - typename = pg_type['typname'] + pg_types = _LEGACY_TYPES | { + pg_type['oid']: pg_type['typname'] for pg_type in pg_types + } + + for typeoid, typename in sorted(pg_types.items()): defname = '{}OID'.format(typename.upper()) defs.append('DEF {name} = {oid}'.format(name=defname, oid=typeoid)) @@ -91,11 +102,11 @@ async def runner(args): typemap[defname] = typename - buf += 'DEF MAXSUPPORTEDOID = {}\n\n'.format(pg_types[-1]['oid']) + buf += 'DEF MAXSUPPORTEDOID = {}\n\n'.format(max(pg_types)) buf += '\n'.join(defs) - buf += '\n\ncdef ARRAY_TYPES = ({},)'.format(', '.join(array_types)) + buf += '\n\nARRAY_TYPES = {{{}}}'.format(', '.join(array_types)) f_typemap = ('{}: {!r}'.format(dn, n) for dn, n in sorted(typemap.items())) buf += '\n\nBUILTIN_TYPE_OID_MAP = {{\n {}\n}}'.format(