Skip to content

Add PostgreSQL 19 (beta1) support#404

Open
sfc-gh-okalaci wants to merge 19 commits into
mainfrom
okalaci/pg19-support
Open

Add PostgreSQL 19 (beta1) support#404
sfc-gh-okalaci wants to merge 19 commits into
mainfrom
okalaci/pg19-support

Conversation

@sfc-gh-okalaci

Copy link
Copy Markdown
Collaborator

This makes pg_lake build and pass tests on PostgreSQL 19beta1, and keeps
16/17/18 working. The majority of the changes are trivial: missing includes,
CI matrix bumps, and small API adjustments behind PG_VERSION_NUM guards. A few
areas needed real thought, noted below.

  • COPY (JSON): PG19 added a native COPY ... TO (FORMAT json). We don't want to
    silently diverge from core, so we give core precedence: for the cases it
    handles itself (uncompressed COPY TO a local file or STDOUT) we step aside,
    and pg_lake keeps the rest (COPY FROM json, compression, remote/lake
    targets). This is the same idea we already use for CSV. The routing sits
    behind a hidden, test only GUC pg_lake_copy.json_copy_mode (auto/postgres/
    pglake); auto is the production default and the upstream regression suite
    forces postgres so core's expected output reproduces exactly. We also map
    COPY FORCE_ARRAY to DuckDB's ARRAY writer. On the test side we round-trip
    every case (write, then read it back through pg_lake), parametrize the local
    cases over both writers, and pin the real divergences (generated columns,
    pg_map serialization) so they can't regress unnoticed.

  • Wire protocol negotiation: PG19 libpq probes whether the server speaks
    NegotiateProtocolVersion (it requests protocol 3.9999 and sends a fake
    pq.* option). pgduck_server used to drop any connection that was not
    exactly 3.0, so every PG19 client failed to attach. It now replies with a
    'v' message advertising 3.0 and echoes back the unknown pq.* options, same
    as core. Older clients are not affected.

  • gnu11: PG19 headers use the C11 static_assert keyword (StaticAssertDecl), so
    -std=gnu99 stopped compiling. We bump the extension Makefiles to -std=gnu11
    unconditionally; it is a superset and still builds on 16/17/18.

  • TupleDescFinalize: PG19 added a cache field to TupleDesc and asserts it is
    populated before a descriptor is blessed. Every place we build a TupleDesc
    by hand now calls TupleDescFinalize(), with a no-op shim in pg_compat.h for
    <=18 so the call can stay unconditional.

  • Shared memory init: PG19 replaced the shmem_request_hook/shmem_startup_hook
    pair with RegisterShmemCallbacks plus ShmemRequestStruct/ShmemRequestHash.
    We use the new path on PG19 and keep the old hooks for <=18, gated by
    version.

  • COPY TO partitioned tables: PG19 lets you COPY a partitioned table directly.
    pg_lake rejected this before. We allow it on PG19+ so a partitioned parent
    (whose leaves can be pg_lake/iceberg tables) streams out in one statement,
    and we set the SELECT inh flag from relkind so the parent descends into its
    partitions.

  • pgaudit: pgaudit has no PG19 SQL/PGQ (graph_table) support yet, and with it
    preloaded the backend crashes on the property graph statements in core's own
    privileges regression test, cascading into ~100 failures. We drop pgaudit
    from the preload list for installcheck-postgres on PG19 only (it stays on for
    <=18). It only writes to the server log, so regression output does not
    change.

  • pg_cron: upstream pg_cron has no PG19 release, so for CI we build it from
    CyberDem0n's postgresql-19 branch (citusdata/pg_cron PR pg_lake_copy: reject multidimensional arrays in COPY TO before CSV serialization #430) until that
    lands.

  • pull_var_clause recursion: on PG19 aggregate/placeholder nodes can appear
    inside baserestrictinfo clauses where earlier majors did not, and
    pull_var_clause elog()s on them without a recurse flag, so we pass
    PVC_RECURSE_AGGREGATES at those sites. postgres_fdw never needed this because
    it does not push down the complex query shapes we do, so it does not reach
    those clauses the same way.

  • GROUP BY ALL: PG19's column-inference GROUP BY ALL also exists in DuckDB, but
    the two infer the grouping columns differently and the deparser keeps the
    literal "GROUP BY ALL", so pushing it down could let DuckDB pick a different
    grouping. We mark such queries as not shippable and run them locally with the
    grouping PostgreSQL already resolved. The older "GROUP BY ALL col" modifier
    is not affected.

  • IGNORE NULLS window functions: PG19 added IGNORE NULLS / RESPECT NULLS, and
    core deparses it as "lag(v) IGNORE NULLS OVER ...", which DuckDB rejects. We
    mark window funcs with ignore_nulls != NO_NULLTREATMENT as not shippable.
    RESPECT NULLS is the default and collapses to NO_NULLTREATMENT, so it still
    pushes down.

  • Isolation tests: PG19 reports a serialization conflict from a concurrent
    delete with different wording (and our copy-on-write makes an UPDATE look
    like a delete to a concurrent reader). Instead of keeping _1.out alternate
    expected files, we trap the failure by SQLSTATE (40001) and re-raise one
    canonical message, so a single expected file matches every version.

Comment thread docker/Dockerfile

# Compile and install pg_cron
RUN git clone https://github.com/citusdata/pg_cron.git && \
# Use CyberDem0n's postgresql-19 branch (PR citusdata/pg_cron#430) until the

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@sfc-gh-mslot do you have plans to merge that branch :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Heh, I'd thought the same... 😁

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks like this is in main now...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

i.e., we can revert this hunk.

Comment thread .github/actions/run-test/action.yml Outdated
Comment thread .github/actions/run-test/action.yml
Comment thread docker/Dockerfile Outdated
ARG PGAUDIT16_VERSION=REL_16_STABLE
ARG PGAUDIT17_VERSION=REL_17_STABLE
ARG PGAUDIT18_VERSION=REL_18_STABLE
ARG PGAUDIT19_VERSION=main

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

FWIW, there is a pgaudit 19 PR that should probably be merged soon.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I tried this: built pgaudit from the maintainer's PG19 PR branch (dwsteele/pgaudit#317, dev-pg19) and re-enabled it on PG19. It compiles fine, but CI showed the backend still segfaults while auditing PG19's SQL/PGQ graph_table statements in PostgreSQL's own privileges regression test, cascading into 109/245 upstream test failures. So the branch isn't there yet.

I reverted to keeping pgaudit out of the PG19 preload (now with the literal "19" check you asked for) and left a comment pointing at the PR so we can flip it back once upstream actually supports PG19.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This has been fixed now, and there is aREL_19_STABLE branch we can use.

Comment thread docker/Dockerfile Outdated
* the function, so define a no-op shim there and let callers always finalize.
*/
#if PG_VERSION_NUM < 190000
static inline void

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not just an empty #define?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we want TupleDescFinalize to be in the C code? So instead of having ifdefs in the code, we have it like this? I don't have any strong opinions, happy to follow if you think strongly about it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would still appear in the code, just wouldn't be an empty inline function. (I assume it's probably totally possible that the compiler just noops anyway.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

My proposal was just doing something like this instead of static inline void, just feels a little more canonical for this sort of thing:

#if PG_VERSION_NUM < 190000
#define TupleDescFinalize(tupdesc)
#endif

with callers unchanged.

Comment thread pg_lake_table/tests/pytests/test_pg19_new_features.py Outdated
Comment thread pg_lake_table/tests/pytests/test_pg19_new_features.py Outdated
Comment thread pg_lake_table/tests/pytests/test_pg19_new_features.py
Comment thread pg_lake_table/tests/pytests/test_pg19_new_features.py
Comment thread pgduck_server/tests/pytests/test_protocol_negotiation.py Outdated
sfc-gh-okalaci and others added 18 commits June 22, 2026 11:52
This makes pg_lake build and pass tests on PostgreSQL 19beta1, and keeps
16/17/18 working. The majority of the changes are trivial: missing includes,
CI matrix bumps, and small API adjustments behind PG_VERSION_NUM guards. A few
areas needed real thought, noted below.

- COPY (JSON): PG19 added a native COPY ... TO (FORMAT json). We don't want to
  silently diverge from core, so we give core precedence: for the cases it
  handles itself (uncompressed COPY TO a local file or STDOUT) we step aside,
  and pg_lake keeps the rest (COPY FROM json, compression, remote/lake
  targets). This is the same idea we already use for CSV. The routing sits
  behind a hidden, test only GUC pg_lake_copy.json_copy_mode (auto/postgres/
  pglake); auto is the production default and the upstream regression suite
  forces postgres so core's expected output reproduces exactly. We also map
  COPY FORCE_ARRAY to DuckDB's ARRAY writer. On the test side we round-trip
  every case (write, then read it back through pg_lake), parametrize the local
  cases over both writers, and pin the real divergences (generated columns,
  pg_map serialization) so they can't regress unnoticed.

- Wire protocol negotiation: PG19 libpq probes whether the server speaks
  NegotiateProtocolVersion (it requests protocol 3.9999 and sends a fake
  _pq_.* option). pgduck_server used to drop any connection that was not
  exactly 3.0, so every PG19 client failed to attach. It now replies with a
  'v' message advertising 3.0 and echoes back the unknown _pq_.* options, same
  as core. Older clients are not affected.

- gnu11: PG19 headers use the C11 static_assert keyword (StaticAssertDecl), so
  -std=gnu99 stopped compiling. We bump the extension Makefiles to -std=gnu11
  unconditionally; it is a superset and still builds on 16/17/18.

- TupleDescFinalize: PG19 added a cache field to TupleDesc and asserts it is
  populated before a descriptor is blessed. Every place we build a TupleDesc
  by hand now calls TupleDescFinalize(), with a no-op shim in pg_compat.h for
  <=18 so the call can stay unconditional.

- Shared memory init: PG19 replaced the shmem_request_hook/shmem_startup_hook
  pair with RegisterShmemCallbacks plus ShmemRequestStruct/ShmemRequestHash.
  We use the new path on PG19 and keep the old hooks for <=18, gated by
  version.

- COPY TO partitioned tables: PG19 lets you COPY a partitioned table directly.
  pg_lake rejected this before. We allow it on PG19+ so a partitioned parent
  (whose leaves can be pg_lake/iceberg tables) streams out in one statement,
  and we set the SELECT inh flag from relkind so the parent descends into its
  partitions.

- pgaudit: pgaudit has no PG19 SQL/PGQ (graph_table) support yet, and with it
  preloaded the backend crashes on the property graph statements in core's own
  privileges regression test, cascading into ~100 failures. We drop pgaudit
  from the preload list for installcheck-postgres on PG19 only (it stays on for
  <=18). It only writes to the server log, so regression output does not
  change.

- pg_cron: upstream pg_cron has no PG19 release, so for CI we build it from
  CyberDem0n's postgresql-19 branch (citusdata/pg_cron PR #430) until that
  lands.

- pull_var_clause recursion: on PG19 aggregate/placeholder nodes can appear
  inside baserestrictinfo clauses where earlier majors did not, and
  pull_var_clause elog()s on them without a recurse flag, so we pass
  PVC_RECURSE_AGGREGATES at those sites. postgres_fdw never needed this because
  it does not push down the complex query shapes we do, so it does not reach
  those clauses the same way.

- GROUP BY ALL: PG19's column-inference GROUP BY ALL also exists in DuckDB, but
  the two infer the grouping columns differently and the deparser keeps the
  literal "GROUP BY ALL", so pushing it down could let DuckDB pick a different
  grouping. We mark such queries as not shippable and run them locally with the
  grouping PostgreSQL already resolved. The older "GROUP BY ALL col" modifier
  is not affected.

- IGNORE NULLS window functions: PG19 added IGNORE NULLS / RESPECT NULLS, and
  core deparses it as "lag(v) IGNORE NULLS OVER ...", which DuckDB rejects. We
  mark window funcs with ignore_nulls != NO_NULLTREATMENT as not shippable.
  RESPECT NULLS is the default and collapses to NO_NULLTREATMENT, so it still
  pushes down.

- Isolation tests: PG19 reports a serialization conflict from a concurrent
  delete with different wording (and our copy-on-write makes an UPDATE look
  like a delete to a concurrent reader). Instead of keeping _1.out alternate
  expected files, we trap the failure by SQLSTATE (40001) and re-raise one
  canonical message, so a single expected file matches every version.

Co-authored-by: Cursor <cursoragent@cursor.com>
The pgaudit-on-PG19 workaround is temporary, so check for the literal
major "19" instead of ">= 19".

Co-authored-by: Cursor <cursoragent@cursor.com>
pgaudit has no PG19 release yet, so build it from the maintainer's PG19
support branch (dwsteele/pgaudit#317, dev-pg19) instead of pinning main.
That lets pgaudit stay preloaded on PG19 like every other version, so we
can drop the PG19-only preload/enable workaround in the test action. We
can switch to REL_19_STABLE once it is tagged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Combine the four per-version pgaudit build steps into a single RUN and
remove the extracted source dirs afterwards to cut image layers and size.

Co-authored-by: Cursor <cursoragent@cursor.com>
Revert an accidental reordering of the tranche id / name assignments in
BaseWorkerSharedMemoryInit so it matches main; the order is immaterial but
the swap was unintentional.

Co-authored-by: Cursor <cursoragent@cursor.com>
PG19 added PG_SIG_IGN (SIG_IGN cast to pqsigfunc) for pqsignal(). Define
the name as plain SIG_IGN on older releases in pg_compat.h so the SIGINT
handler setup can use PG_SIG_IGN unconditionally instead of a per-call

Co-authored-by: Cursor <cursoragent@cursor.com>
#if.
Spread the pg_plan_query() arguments onto their own lines so the PG19-only
trailing NULL argument reads cleanly instead of being appended inline.

Co-authored-by: Cursor <cursoragent@cursor.com>
Inline the 190000 server-version literal in the PG19 skip helpers instead
of defining a one-off PG19 module constant, matching how the rest of the
suite spells version checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
These COPY-to-partitioned-table tests will outlive PG19, and the version
guard already skips them where unsupported, so rename the functions and
helper to describe the behavior instead of the release.

Co-authored-by: Cursor <cursoragent@cursor.com>
Compare the full, deterministically-ordered table contents before and
after REPACK instead of only the row count, so a silent rewrite that
preserved cardinality but corrupted rows would be caught.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wire protocol 3.2 arrived in PG18 and the GREASE/_pq_ probing that forces
pgduck_server to answer NegotiateProtocolVersion is new in PG19; the 'v'
message itself predates both. Reword so the versions are attributed
correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>
The dev-pg19 pgaudit branch builds, but CI showed it still segfaults the
backend while auditing PG19's SQL/PGQ graph_table statements in
PostgreSQL's own privileges regression test (109/245 upstream tests
cascade-failed). Restore the PG19-only pgaudit drop (using a literal "19"
check) and build plain pgaudit main in the image until upstream actually
supports PG19.

Co-authored-by: Cursor <cursoragent@cursor.com>
main #406 split rest_catalog.c into sibling files. The PG19 implicit-
include hardening needs access/htup_details.h (GETSTRUCT) in the DDL
sibling and pg_extension_base/pg_compat.h (numeric_int4_opt_error shim)
in the HTTP sibling, which the old monolithic file pulled in transitively.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the inline "#if ... ,NULL" spliced argument with two complete
pg_plan_query() calls, one per major, per review feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
Give the helper a constant-false definition for pre-19 so the JSON
routing in IsPgLakeCopy no longer needs its own version guard; the only
PG19 #if now lives at the declaration/definition.

Co-authored-by: Cursor <cursoragent@cursor.com>
The include just covers PG19's stricter implicit-include rules for
WAIT_EVENT_CLIENT_READ; utils/wait_event.h has existed since PG14, so
drop the version guard.

Co-authored-by: Cursor <cursoragent@cursor.com>
The header only needs the HTAB typedef for its signatures, not the
dynahash internals. Include utils/hsearch.h unconditionally and let
callers pull in utils/dynahash.h where they actually use it.

Co-authored-by: Cursor <cursoragent@cursor.com>
palloc0 already zeroes the mapping, so the no-matching-column case needs
no body. Fold the explanation into the guard and remove the empty else.

Co-authored-by: Cursor <cursoragent@cursor.com>
The maintainer fixed the SQL/PGQ graph_table audit crash on the dev-pg19
branch (dwsteele/pgaudit#317, commit ffbae89), so we no longer need to
keep pgaudit out of the PG19 preload. Build pgaudit for PG19 from that
branch and preload it on PG19 like every other version.

Verified locally on PG19beta1: a GRAPH_TABLE query over a property graph
is now audited instead of aborting the backend.

Co-authored-by: Cursor <cursoragent@cursor.com>
@raphaelauv raphaelauv mentioned this pull request Jul 1, 2026
10 tasks
Comment thread docker/Dockerfile
Comment on lines +24 to +25
ARG PGAUDIT19_REPO=dwsteele/pgaudit
ARG PGAUDIT19_VERSION=dev-pg19

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
ARG PGAUDIT19_REPO=dwsteele/pgaudit
ARG PGAUDIT19_VERSION=dev-pg19
ARG PGAUDIT19_VERSION=REL_19_STABLE

Comment thread docker/Dockerfile
wget https://github.com/pgaudit/pgaudit/archive/refs/heads/$PGAUDIT17_VERSION.tar.gz && \
wget https://github.com/pgaudit/pgaudit/archive/refs/heads/$PGAUDIT18_VERSION.tar.gz && \
wget https://github.com/pgaudit/pgaudit/archive/refs/heads/$PGAUDIT19_VERSION.tar.gz && \
wget https://github.com/$PGAUDIT19_REPO/archive/refs/heads/$PGAUDIT19_VERSION.tar.gz && \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
wget https://github.com/$PGAUDIT19_REPO/archive/refs/heads/$PGAUDIT19_VERSION.tar.gz && \
wget https://github.com/pgaudit/pgaudit/archive/refs/heads/$PGAUDIT19_VERSION.tar.gz && \

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants