PG19 Hacktober: 31 Days of New Features: DDL and Privileges: GRANTED BY, CHECK … NOT ENFORCED, SET EXPRESSION and CHECKPOINT Options

PostgreSQL 19 adds several practical DDL and administration features. This post covers four of them, each run side by side on PostgreSQL 18 and PostgreSQL 19 RC1:

  • GRANT / REVOKE … GRANTED BY records a privilege change as made by another role, not only the current user.
  • ALTER TABLE … ALTER CONSTRAINT … [NOT] ENFORCED now works for CHECK constraints, not only foreign keys.
  • ALTER TABLE … ALTER COLUMN … SET EXPRESSION now succeeds on virtual generated columns that have CHECK constraints.
  • CHECKPOINT now accepts options: MODE (FAST or SPREAD) and FLUSH_UNLOGGED.

Test environment. Both servers were built from source on Ubuntu 24.04: the REL_18_STABLE branch (PostgreSQL 18.6) and the REL_19_RC1 tag (19rc1).

PostgreSQL 18 vs PostgreSQL 19 at a glance

FeaturePostgreSQL 18PostgreSQL 19
GRANTED BY on object privilegesSyntax accepted, but only the current user is allowed: grantor must be the current userGrant is recorded as made by the named role, if you inherit that role’s privileges
ALTER CONSTRAINT … [NOT] ENFORCEDForeign keys only; a CHECK fails with cannot alter enforceabilityWorks for FOREIGN KEY and CHECK
SET EXPRESSION on a virtual column in a table with a CHECKError: not supported in tables with check constraintsAllowed; existing rows are checked, no table rewrite
CHECKPOINT optionsNone; CHECKPOINT (MODE SPREAD) is a syntax errorMODE (FAST | SPREAD) and FLUSH_UNLOGGED

1. GRANT / REVOKE … GRANTED BY

1.1 What was missing in PostgreSQL 18

Every privilege stored on an object has a grantor, the role the grant is attributed to. A later REVOKE removes only the grants made by the grantor it acts as, so the recorded grantor matters in delegated administration.

PostgreSQL 18 parses GRANTED BY on object privileges, but only for SQL compatibility: the named grantor must be the current user. You could not choose whom a grant is attributed to.

1.2 What changes in PostgreSQL 19

Every GRANT and REVOKE form now ends with [ GRANTED BY role_specification ]. The grant is recorded as made by the named role. The role running the command may do this only if it inherits the privileges of the named role. Role-membership GRANT role TO role GRANTED BY … already worked on both versions. The new part is the effective role for object privileges.

1.3 Setup

CREATE ROLE app_owner;
CREATE ROLE app_admin;
CREATE ROLE team_lead;
CREATE ROLE reader1;
CREATE ROLE reader2;
CREATE ROLE reader3;
CREATE ROLE outsider;
CREATE TABLE orders (id int);
ALTER TABLE orders OWNER TO app_owner;
GRANT app_owner TO app_admin;   -- app_admin inherits app_owner
GRANT team_lead TO app_admin;   -- app_admin inherits team_lead
SET ROLE app_owner;
GRANT SELECT ON orders TO team_lead WITH GRANT OPTION;
RESET ROLE;

The starting state is identical on both versions. In the access privileges column, the role after the slash is the grantor.

postgres=# \dp orders
 Schema |  Name  | Type  |      Access privileges       
--------+--------+-------+------------------------------
 public | orders | table | app_owner=arwdDxtm/app_owner+
        |        |       | team_lead=r*/app_owner       

1.4 Examples

Example 1: attribute a grant to the table owner (run as a superuser, who is not the owner):

GRANT SELECT ON orders TO reader1 GRANTED BY app_owner;

PostgreSQL 18:

ERROR:  grantor must be current user

PostgreSQL 19 accepts it and records reader1=r/app_owner.

Example 2: attribute a grant to a role you inherit

SET ROLE app_admin;
GRANT SELECT ON orders TO reader2 GRANTED BY team_lead;
RESET ROLE;

PostgreSQL 18 again returns grantor must be current user. On PostgreSQL 19 the entry is attributed to team_lead, not to app_admin:

postgres=# \dp orders
Schema |  Name  | Type  |      Access privileges       
--------+--------+-------+------------------------------
 public | orders | table | app_owner=arwdDxtm/app_owner+
        |        |       | team_lead=r*/app_owner      +
        |        |       | reader1=r/app_owner         +
        |        |       | reader2=r/team_lead      

Example 3: a role that does not inherit the grantor is refused

SET ROLE outsider;
GRANT SELECT ON orders TO reader3 GRANTED BY team_lead;
PostgreSQL 19 gives a specific error (PostgreSQL 18 gives the generic one):
ERROR:  must inherit privileges of role "team_lead"

Example 4: REVOKE … GRANTED BY removes only that grantor’s entry

REVOKE SELECT ON orders FROM reader2 GRANTED BY team_lead;

On PostgreSQL 19, reader2 disappears, and the other entries stay. On PostgreSQL 18 the statement fails with the message “grantor must be current user”.

Example 5: attribute a grant to a role you do not inherit (invoker is a member of other roles, but not of the named one)

SET ROLE app_admin;
GRANT SELECT ON orders TO reader3 GRANTED BY outsider;

app_admin inherits app_owner and team_lead, but it is not a member of outsider. PostgreSQL 19 refuses the grant and names the role it could not inherit:

ERROR:  must inherit privileges of role "outsider"

PostgreSQL 18 refuses it too, but with the generic message:

ERROR:  grantor must be current user

Nothing is granted on either version: \dp orders is unchanged and reader3 does not appear. This differs from Example 3 only in who runs the command: there the invoker was outsider itself, here it is a role that inherits other grantors but not the one named.

1.5 How this was handled in PostgreSQL 18

The only way to get a grant attributed to team_lead was to become that role first:

-- connected as app_admin
SET ROLE team_lead;                       -- whole session now acts as team_lead
GRANT SELECT ON orders TO reader3;
RESET ROLE;

The result is reader3=r/team_lead on both versions. GRANTED BY States the intent inside the statement itself.

1.6 Compatibility check: GRANTED BY CURRENT_USER

Because PostgreSQL 19 now honors the grantor you name, one old habit changes. Run as a superuser who is not the owner of orders:

GRANT SELECT ON orders TO reader3 GRANTED BY CURRENT_USER;
  • PostgreSQL 18: the grant succeeds and is recorded as made by the owner (reader3=r/app_owner).
  • PostgreSQL 19: nothing is granted.

WARNING:  no privileges were granted for “orders”

A plain GRANT SELECT ON orders TO reader3; as the same superuser still records the owner as grantor on both versions. 

Review scripts that add GRANTED BY CURRENT_USER to grants issued by a superuser before you upgrade.

1.7 Practical guidance

  • GRANTED BY changes only who the grant is attributed to. It does not give anyone new privileges.
  • The command issuer must inherit the privileges of the named role, otherwise, PostgreSQL 19 requires that the issuer inherit privileges of the role.
  • For role memberships, the recorded grantor must still hold ADMIN OPTION on the target role, unless it is the bootstrap superuser. This rule is unchanged.
  • Use the same grantor on REVOKE that you used on GRANT, since REVOKE acts on one grantor’s entries.
Command / InvokerPostgreSQL 18PostgreSQL 19
GRANT … GRANTED BY other_roleERROR: grantor must be current userRecorded as made by other_role
REVOKE … GRANTED BY other_roleERROR: grantor must be current userRemoves only that grantor’s entry
Invoker does not inherit the named roleERROR: grantor must be current userERROR: must inherit privileges of role
Superuser runs GRANT … GRANTED BY CURRENT_USERSucceeds, recorded as ownerWARNING: no privileges were granted
SET ROLE workaroundWorksWorks

2. ALTER CONSTRAINT … [NOT] ENFORCED for CHECK constraints

2.1 What was missing in PostgreSQL 18

PostgreSQL 18 lets you create a CHECK constraint as NOT ENFORCED, which is handy for bulk loads and migrations. The gap could only be noticed afterwards: only foreign keys could change enforceability with ALTER CONSTRAINT, so a CHECK could never be switched to ENFORCED again.

2.2 What changes in PostgreSQL 19

ALTER TABLE … ALTER CONSTRAINT … [NOT] ENFORCED now works for FOREIGN KEY and CHECK constraints.

  • NOT ENFORCED stops checking the constraint for new and updated rows.
  • ENFORCED first verifies that existing rows satisfy the constraint, then checks new and updated rows.
  • Deferrability can still be altered only for foreign keys.

2.3 Setup: a NOT ENFORCED CHECK with one bad row

CREATE TABLE emp (id int, salary numeric,
    CONSTRAINT sal_pos CHECK (salary > 0) NOT ENFORCED);
INSERT INTO emp VALUES (1, 100), (2, -5);   -- accepted: constraint not enforced

SELECT conname, conenforced, convalidated
FROM pg_constraint WHERE conrelid = 'emp'::regclass;
 conname | conenforced | convalidated 
---------+-------------+--------------
 sal_pos | f           | f

This part is identical on both versions.

2.4 Examples

Example 1: enforce while a bad row exists

ALTER TABLE emp ALTER CONSTRAINT sal_pos ENFORCED;

PostgreSQL 18 refuses any CHECK:

ERROR:  cannot alter enforceability of constraint “sal_pos” of relation “emp”

PostgreSQL 19 checks the existing rows and reports the violation. The constraint stays NOT ENFORCED:

ERROR:  check constraint “sal_pos” of relation “emp” is violated by some row

Example 2: clean the data, then enforce

UPDATE emp SET salary = 50 WHERE id = 2;
ALTER TABLE emp ALTER CONSTRAINT sal_pos ENFORCED;
INSERT INTO emp VALUES (3, -1);

On PostgreSQL 18 the ALTER fails again, even though the data is clean, and the INSERT of -1 succeeds silently. On PostgreSQL 19 the constraint becomes enforced and validated, and the bad insert is rejected:

conname | conenforced | convalidated 
---------+-------------+--------------
 sal_pos | t           | t

ERROR:  new row for relation “emp” violates check constraint “sal_pos”

DETAIL:  Failing row contains (3, -1).

What the output looks like once the constraint is enforced (PostgreSQL 19)

The command itself prints only its tag, and psql shows the change in \d. Before enforcing, the constraint is marked in the table definition:

postgres=# \d emp
                Table "public.emp"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 id     | integer |           |          | 
 salary | numeric |           |          | 
Check constraints:
    "sal_pos" CHECK (salary > 0::numeric) NOT ENFORCED

After the data is clean and the constraint is enforced:

postgres=# ALTER TABLE emp ALTER CONSTRAINT sal_pos ENFORCED;
ALTER TABLE
postgres=# \d emp
                Table "public.emp"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 id     | integer |           |          | 
 salary | numeric |           |          | 
Check constraints:
    "sal_pos" CHECK (salary > 0::numeric)

The NOT ENFORCED suffix is gone, pg_constraint shows conenforced = t and convalidated = t (the table above), and a violating insert is rejected from then on. On PostgreSQL 18 the same ALTER fails, and \d emp still ends with NOT ENFORCED.

Example 3: switch enforcement off again (PostgreSQL 19)

ALTER TABLE emp ALTER CONSTRAINT sal_pos NOT ENFORCED;
INSERT INTO emp VALUES (3, -1);   -- succeeds again

Turning enforcement off also clears convalidated (f | f), so a later ENFORCED always re-checks the table.

Example 4: what did not change

ALTER TABLE emp VALIDATE CONSTRAINT sal_pos;
ALTER TABLE emp ALTER CONSTRAINT sal_pos DEFERRABLE;

ERROR: cannot validate NOT ENFORCED constraint
ERROR: constraint “sal_pos” of relation “emp” is not a foreign key constraint.

Both errors are the same on PostgreSQL 18 and 19. One small improvement: for a constraint type that cannot change enforceability, such as NOT NULL, PostgreSQL 19 adds a hint to the same error:


ERROR:  cannot alter enforceability of constraint “nn_a_not_null” of relation “nn”
HINT:  Only foreign key and check constraints can change enforceability.

2.5 How this was handled in PostgreSQL 18

Drop the constraint, add it back as NOT VALID (enforced for new rows, no scan of existing rows), then validate:

BEGIN;
ALTER TABLE emp2 DROP CONSTRAINT sal_pos;
ALTER TABLE emp2 ADD CONSTRAINT sal_pos CHECK (salary > 0) NOT VALID;
COMMIT;
ALTER TABLE emp2 VALIDATE CONSTRAINT sal_pos;

The end state is the same (conenforced = t, convalidated = t). Lock modes observed in pg_locks:

StepLock mode
DROP CONSTRAINT and ADD CONSTRAINT … NOT VALIDAccessExclusiveLock
VALIDATE CONSTRAINTShareUpdateExclusiveLock
PostgreSQL 19: ALTER CONSTRAINT … ENFORCEDAccessExclusiveLock

2.6 Practical guidance

  • ENFORCED verifies existing rows while holding an ACCESS EXCLUSIVE lock. On large, busy tables, ADD CONSTRAINT … NOT VALID followed by VALIDATE CONSTRAINT is still the low-impact route, and it still works on PostgreSQL 19.
  • While a constraint is NOT ENFORCED, bad rows can accumulate, and ENFORCED will fail until they are fixed.
  • Only FOREIGN KEY and CHECK constraints can change enforceability.

3. ALTER COLUMN … SET EXPRESSION on virtual columns with CHECK constraints

3.1 What was missing in PostgreSQL 18

Virtual generated columns, introduced in PostgreSQL 18, store no data. The value is computed when the column is read, so changing the expression should be cheap. Yet PostgreSQL 18 refused SET EXPRESSION on a virtual generated column whenever the table had a CHECK constraint, even if the new expression clearly satisfied it.

3.2 What changes in PostgreSQL 19

SET EXPRESSION now succeeds on virtual generated columns used in CHECK constraints. Per the 19 documentation, replacing the expression of a virtual column needs no table rewrite, but if the column is used in a constraint, the table is scanned to check that existing rows still meet it.

3.3 Setup

CREATE TABLE gtest20 (
    a int PRIMARY KEY,
    b int GENERATED ALWAYS AS (a * 2) VIRTUAL CHECK (b < 50)
);
INSERT INTO gtest20 (a) VALUES (10);   -- ok, b = 20
INSERT INTO gtest20 (a) VALUES (30);   -- rejected on both versions, b would be 60
ERROR:  new row for relation "gtest20" violates check constraint "gtest20_b_check"
DETAIL:  Failing row contains (30, virtual).

3.4 Examples

Example 1: a new expression that violates the constraint

ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);

PostgreSQL 18 refuses before looking at any data:

ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables with check constraints

DETAIL:  Column “b” of relation “gtest20” is a virtual generated column.

PostgreSQL 19 checks the rows and rejects the change because of the data:

ERROR:  check constraint “gtest20_b_check” of relation “gtest20” is violated by some row

Example 2: a new expression that satisfies the constraint

SELECT pg_relation_filenode('gtest20') AS fb \gset

ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);

SELECT pg_relation_filenode('gtest20') = :fb AS same_file_no_rewrite;
SELECT a, b FROM gtest20;

PostgreSQL 18 returns the same “not supported” error as in Example 1, and b stays a * 2. PostgreSQL 19 succeeds, the file is unchanged (no rewrite), and the row reads with the new expression:

 same_file_no_rewrite 
----------------------
 t

 a  | b  
----+----
 10 | 30

The CHECK keeps protecting the column under the new expression. With a * 3, inserting a = 20 would give b = 60:

ERROR:  new row for relation “gtest20” violates check constraint “gtest20_b_check”

DETAIL:  Failing row contains (20, virtual).

Example 3: without a CHECK constraint, nothing changes

CREATE TABLE gtest_nochk (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
INSERT INTO gtest_nochk VALUES (10);
ALTER TABLE gtest_nochk ALTER COLUMN b SET EXPRESSION AS (a * 3);   -- works on both

Example 4: a restriction that remains

CREATE TABLE gtest_pub (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
CREATE PUBLICATION pub_t FOR TABLE gtest_pub;
ALTER TABLE gtest_pub ALTER COLUMN b SET EXPRESSION AS (a * 3);

Both versions reject it when the table is part of a publication:

ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication

3.5 How this was handled in PostgreSQL 18

Drop the constraint, change the expression, add the constraint back, all in one transaction:

BEGIN;
ALTER TABLE g2 DROP CONSTRAINT g2_b_check;
ALTER TABLE g2 ALTER COLUMN b SET EXPRESSION AS (a * 3);
ALTER TABLE g2 ADD CONSTRAINT g2_b_check CHECK (b < 50);
COMMIT;

This works, but you must know and repeat the constraint definition.

3.6 Practical guidance

  • On both versions the transaction held an AccessExclusiveLock on the table (observed in pg_locks), so scheduled it like any other table-level DDL.
  • No table rewrite happens: the relation file stays the same.
  • Tables that are part of a publication are still not supported.
  • Per the 19 documentation, ANALYZE is not needed after changing a virtual column’s expression unless extended statistics are defined on it. PostgreSQL 19 also allows extended statistics on virtual generated columns.
ScenarioPostgreSQL 18PostgreSQL 19
SET EXPRESSION on a virtual column, table has a CHECKERROR: not supported … with check constraintsSupported; existing rows checked
New expression violates the CHECK for an existing rowERROR: not supported … with check constraintsERROR: check constraint … is violated by some row
Table file after a successful changeNot applicable (refused)Unchanged, no rewrite
Table without CHECK constraintsSupportedSupported
Table in a publicationERROR: not supported … part of a publicationERROR: not supported … part of a publication
Drop CHECK, change, re-add CHECKWorksWorks

4. CHECKPOINT options: MODE and FLUSH_UNLOGGED

4.1 What was missing in PostgreSQL 18

In PostgreSQL 18, CHECKPOINT takes no options. It always forces a fast checkpoint, which can mean a burst of I/O on a busy system. A plain checkpoint also leaves dirty buffers of unlogged tables alone. Any option is a syntax error:

CHECKPOINT [ ( option [, ...] ) ]

-- where option can be one of:
--     FLUSH_UNLOGGED [ boolean ]
--     MODE { FAST | SPREAD }
  • MODE FAST is the default and behaves like the old command: finish as quickly as possible, at the cost of higher I/O.
  • MODE SPREAD spreads the checkpoint over a longer interval controlled by the usual checkpoint settings, like a scheduled checkpoint.
  • FLUSH_UNLOGGED also flushes dirty buffers of unlogged relations. It is off by default.

Bad input gets a clear error:

CHECKPOINT (MODE FAST);
CHECKPOINT (MODE SPREAD);
CHECKPOINT (FLUSH_UNLOGGED);
CHECKPOINT (MODE SPREAD, FLUSH_UNLOGGED true);
CHECKPOINT (MODE SLOW);

ERROR:  unrecognized value for CHECKPOINT option “mode”: “slow”

Two more invalid forms: the option list must be in parentheses, and option names are checked.

CHECKPOINT FAST;
CHECKPOINT (MODE SLOW);

PostgreSQL 19:

ERROR:  syntax error at or near "FAST"
LINE 1: CHECKPOINT FAST;
                                        ^
ERROR:  unrecognized CHECKPOINT option "node"
LINE 1: CHECKPOINT (MODE SLOW);
                                          ^

The first fails because FAST is not a bare keyword; write CHECKPOINT (MODE FAST);. The second has a valid option name but an invalid value: MODE accepts only FAST or SPREAD, so SLOW is rejected. PostgreSQL 18 gives a syntax error for both, at FAST and at the opening parenthesis.

With log_checkpoints = on, the options show up in the server log. The same plain CHECKPOINT; is logged as immediate force wait on PostgreSQL 18 and as fast force wait on PostgreSQL 19, which matters if you parse these lines:

-- PostgreSQL 19
checkpoint starting: fast force wait
checkpoint starting: force wait                       -- MODE SPREAD
checkpoint starting: fast force wait flush-unlogged   -- FLUSH_UNLOGGED

PostgreSQL 18 logs the same plain CHECKPOINT; with different wording. The start line says immediate force wait, and the completion line carries no flags:

-- PostgreSQL 18
LOG:  checkpoint starting: immediate force wait
LOG:  checkpoint complete: wrote 55 buffers (0.3%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.003 s, sync=0.005 s, total=0.010 s; sync files=49, longest=0.001 s, average=0.001 s; distance=248 kB, estimate=248 kB; lsn=0/172D588, redo lsn=0/172D530

PostgreSQL 19 repeats the flags on the completion line, right after checkpoint complete:

-- PostgreSQL 19
LOG:  checkpoint starting: fast force wait
LOG:  checkpoint complete: fast force wait: wrote 52 buffers (0.3%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.003 s, sync=0.005 s, total=0.010 s; sync files=49, longest=0.002 s, average=0.001 s; distance=234 kB, estimate=234 kB; lsn=0/01726510, redo lsn=0/017264B8

The shutdown checkpoint is renamed the same way: shutdown immediate on PostgreSQL 18 and shutdown fast on PostgreSQL 19. Update any log-parsing rules that match the old wording.

4.3 Example 1: fast versus spread

Settings used: checkpoint_timeout = 30s, shared_buffers = 512MB, max_wal_size = 8GB, default checkpoint_completion_target (0.9). We created a 1,500,000-row table, ran a checkpoint, then updated every row to dirty the buffers and timed the command.

\timing on
CHECKPOINT (MODE FAST);     -- Time: 219.408 ms
CHECKPOINT (MODE SPREAD);   -- Time: 26197.207 ms (00:26.197)
CommandPostgreSQL 18PostgreSQL 19 RC1
CHECKPOINT (MODE FAST)Syntax error0.22 s
CHECKPOINT (MODE SPREAD)Syntax error26.2 s

The spread checkpoint took roughly checkpoint_timeout times checkpoint_completion_target (30 s x 0.9 = 27 s). CHECKPOINT waits for completion, so the session is blocked for that whole time.

4.4 Example 2: flush the dirty buffers of an unlogged table

Settings used: shared_buffers = 128MB, bgwriter_lru_maxpages = 0, so dirty pages stay in memory. We used pg_buffercache to count the dirty buffers of a 14 MB unlogged table.

CREATE EXTENSION pg_buffercache;
CREATE UNLOGGED TABLE u_tbl AS
    SELECT g, md5(g::text) AS pad FROM generate_series(1, 200000) g;

SELECT count(*) FILTER (WHERE is dirty) AS dirty, count(*) AS cached
FROM pg_buffercache
WHERE reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database())
  AND relfilenode = pg_relation_filenode('u_tbl');
MomentPostgreSQL 18 (dirty/cached)PostgreSQL 19 RC1 (dirty/cached)
After creating the table1667 / 17281667 / 1728
After CHECKPOINT;1667 / 17281667 / 1728
After CHECKPOINT (FLUSH_UNLOGGED);Syntax error, still 1667 / 17280 / 1728

A plain checkpoint left all 1667 dirty unlogged buffers untouched on both versions. Only PostgreSQL 19 with FLUSH_UNLOGGED wrote them out.

4.5 How this was handled in PostgreSQL 18

There was no SQL equivalent. CHECKPOINT was always fast, so a gentle checkpoint meant waiting for the scheduled one, and a plain manual checkpoint did not write unlogged buffers.

4.6 Practical guidance

  • CHECKPOINT still requires superuser or membership in the pg_checkpoint role, and the documentation says it is not meant for normal operation.
  • The server may merge concurrent checkpoint requests and combine their options. If one session asks for fast and another for spread, the server may perform one fast checkpoint.
  • During recovery, CHECKPOINT still forces a restartpoint instead of a new checkpoint.
  • With MODE SPREAD, your session waits as long as the checkpoint takes. Run it from a session that can wait.
Command / StatePostgreSQL 18PostgreSQL 19
CHECKPOINT;Fast checkpointSame (equivalent to MODE FAST)
CHECKPOINT (MODE SPREAD);Syntax errorSpread over the checkpoint interval
CHECKPOINT (MODE FAST);Syntax errorSupported
CHECKPOINT (FLUSH_UNLOGGED);Syntax errorAlso writes dirty unlogged buffers
Unlogged buffers on plain CHECKPOINT;Left dirtyLeft dirty, unless FLUSH_UNLOGGED
Invalid optionSyntax errorSpecific error message

Conclusion

  • GRANTED BY: state the grantor inside GRANT or REVOKE, without SET ROLE. You must inherit the named role. Check scripts that use GRANTED BY CURRENT_USER and invoked by a superuser.
  • CHECK … [NOT] ENFORCED: load data under a relaxed CHECK, then enforce it with one command. It verifies existing rows under an ACCESS EXCLUSIVE lock; so it is still recommended to use NOT VALID plus VALIDATE for large, busy tables.
  • SET EXPRESSION on virtual columns: no more dropping and re-creating the CHECK. No rewrite, existing rows are verified, publications are still unsupported.
  • CHECKPOINT options: MODE SPREAD for a gentle manual checkpoint, FLUSH_UNLOGGED to write unlogged buffers on demand.

Credits

These features exist thanks to the PostgreSQL community. Thanks to Nathan Bossart and Tom Lane (GRANTED BY), Jian He (CHECK enforcement and SET EXPRESSION), Christoph Berg (CHECKPOINT options), and to the following committers and reviewers whose names appear in the commit messages of the official PostgreSQL repository at the REL_19_RC1 tag for creating and validating these features.

FeatureAuthorCommitterReviewersCommit
GRANT/REVOKE … GRANTED BYNathan Bossart (co-author: Tom Lane)Nathan Bossartnone listeddd1398f137, 2026-03-19
ALTER CONSTRAINT … [NOT] ENFORCED for CHECKJian HeAndrew DunstanRobert Treat, Kirill Reshke, Amul Sul, Zsolt Parragi, Andrew Dunstan342051d73b, 2026-03-11
SET EXPRESSION on virtual columns with CHECKJian HePeter EisentrautMatheus Alcantaraf80bedd52b, 2026-02-24
CHECKPOINT option listChristoph BergNathan BossartFujii Masaoa4f126516e, 2025-07-11
CHECKPOINT MODEChristoph BergNathan BossartAndres Freund, Fujii Masao, Laurenz Albe, Dilip Kumar2f698d7f4b, 2025-07-11
CHECKPOINT FLUSH_UNLOGGEDChristoph BergNathan BossartLaurenz Albe, Fujii Masao, Dilip Kumar8d33fbacba, 2025-07-11

References

• GRANT
• CHECKPOINT
• ALTER TABLE
• PostgreSQL 19 release notes


Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top