From 13c433c56c0fc29700e0d47d494217c3f1e28f66 Mon Sep 17 00:00:00 2001 From: Gabriele Bartolini Date: Thu, 27 Aug 2026 12:38:21 +1000 Subject: [PATCH] Track last-modification timestamp for roles, databases, tablespaces Add an updated timestamptz column to pg_authid, pg_database and pg_tablespace, recording when the most recent CREATE or ALTER command against that object was executed: rolupdated, datupdated and spcupdated respectively. Tools that manage these objects declaratively (Kubernetes operators, Ansible, Terraform) currently have no cheap way to ask whether one changed since they last looked, and must either reapply DDL unconditionally on every reconcile pass or reimplement attribute-by-attribute diffing client side. The timestamp is set by: - CreateRole(), AlterRole(), RenameRole() and AlterRoleSet() for ALTER ROLE ... SET/RESET - createdb(), AlterDatabase(), AlterDatabaseSet(), AlterDatabaseOwner(), AlterDatabaseRefreshColl(), movedb() and RenameDatabase() - CreateTableSpace(), RenameTableSpace(), AlterTableSpaceOptions() and, via the generic AlterObjectOwner_internal() path, ALTER TABLESPACE ... OWNER TO The value advances whenever such a command completes successfully, even if it did not actually change anything: for role passwords this is not merely a simplification but a necessity, as pg_be_scram_build_secret() generates a fresh random salt per call, so re-issuing an identical password cannot be distinguished from a change. None of the three columns are preserved by pg_dumpall or pg_upgrade, which replay the CREATE and ALTER commands against the target cluster, so restored objects carry the time of the restore. rolupdated is exposed in pg_roles and as updated in pg_shadow and pg_user; datupdated and spcupdated are read directly from their catalogs. The shared machinery lives in catalog.c: a declarative ModificationTrackedCatalogs[] table naming which catalogs opt in, GetObjectUpdatedAttnum() for callers building their own replacement tuple, and RecordObjectModification() for the ALTER ... SET commands that write pg_db_role_setting rather than the object's own catalog row. Assisted-by: Claude Signed-off-by: Gabriele Bartolini --- doc/src/sgml/catalogs.sgml | 52 +++++++++ doc/src/sgml/system-views.sgml | 36 ++++++ src/backend/catalog/catalog.c | 126 +++++++++++++++++++++ src/backend/catalog/system_views.sql | 9 +- src/backend/commands/alter.c | 14 +++ src/backend/commands/dbcommands.c | 48 +++++++- src/backend/commands/tablespace.c | 29 +++++ src/backend/commands/user.c | 25 +++- src/include/catalog/catalog.h | 3 + src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_authid.dat | 51 ++++++--- src/include/catalog/pg_authid.h | 2 + src/include/catalog/pg_database.dat | 3 +- src/include/catalog/pg_database.h | 3 + src/include/catalog/pg_tablespace.dat | 6 +- src/include/catalog/pg_tablespace.h | 6 + src/test/regress/expected/database.out | 79 +++++++++++++ src/test/regress/expected/role_updated.out | 81 +++++++++++++ src/test/regress/expected/rules.out | 9 +- src/test/regress/expected/tablespace.out | 50 ++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/database.sql | 41 +++++++ src/test/regress/sql/role_updated.sql | 53 +++++++++ src/test/regress/sql/tablespace.sql | 22 ++++ 24 files changed, 722 insertions(+), 30 deletions(-) create mode 100644 src/test/regress/expected/role_updated.out create mode 100644 src/test/regress/sql/role_updated.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 96a6677b284..c84b3a5d995 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1624,6 +1624,23 @@ null if no expiration + + + + rolupdated timestamptz + + + Time at which the most recent or + command for this role was executed + (including a rename or a role-level configuration parameter + change). The timestamp advances whenever such a command completes + successfully, even if it did not change any value. Null for roles + created during initdb that have never + been altered. Not preserved by pg_dumpall + or pg_upgrade, which recreate roles in + the target cluster. + + @@ -3253,6 +3270,23 @@ SCRAM-SHA-256$<iteration count>:&l Access privileges; see for details + + + + datupdated timestamptz + + + Time at which the most recent or + command for this database was + executed. The timestamp advances whenever such a command completes + successfully, even if it did not change any value. Null for + template1, which is created during bootstrap and + has never been altered. Not preserved by + pg_dumpall or + pg_upgrade, which recreate databases in the + target cluster. + + @@ -8997,6 +9031,24 @@ SCRAM-SHA-256$<iteration count>:&l Tablespace-level options, as keyword=value strings + + + + spcupdated timestamptz + + + Time at which the most recent or + command for this tablespace was + executed. The timestamp advances whenever such a command completes + successfully, even if it did not change any value. Null for the + tablespaces created during bootstrap + (pg_default and pg_global) if + they have never been altered. Not preserved by + pg_dumpall or + pg_upgrade, which recreate tablespaces in + the target cluster. + + diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 5ea19d68622..35aad8521ed 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -3333,6 +3333,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx ID of role + + + + rolupdated timestamptz + + + Time at which the most recent or + command for this role was executed, + even if it did not change any value. Null for roles created during + initdb that have never been altered + + @@ -4165,6 +4177,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Session defaults for run-time configuration variables + + + + updated timestamptz + + + Time at which the most recent or + command for this role was executed, + even if it did not change any value. Null for roles created during + initdb that have never been altered + + @@ -5529,6 +5553,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Session defaults for run-time configuration variables + + + + updated timestamptz + + + Time at which the most recent or + command for this role was executed, + even if it did not change any value; null for roles created during + initdb that have never been altered + + diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c index cf9b88b3e25..4b44f70c35c 100644 --- a/src/backend/catalog/catalog.c +++ b/src/backend/catalog/catalog.c @@ -25,6 +25,7 @@ #include "access/table.h" #include "access/transam.h" #include "catalog/catalog.h" +#include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" @@ -47,6 +48,7 @@ #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/timestamp.h" /* * Parameters to determine when to emit a log message in @@ -657,6 +659,130 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence) return rlocator.locator.relNumber; } +/* + * Catalogs that track the time of the last DDL command affecting each of + * their objects. + * + * Only global (shared) object types appear here. Objects living in a + * database are deliberately excluded: DDL on them can already be observed + * with an event trigger, whereas event triggers do not fire for shared + * objects (see EventTriggerSupportsObjectType()), and the per-database + * catalogs are also the ones where the storage cost of an extra column + * would be significant. + */ +static const struct +{ + Oid class_oid; /* catalog holding the objects */ + SysCacheIdentifier oid_cache_id; /* syscache on the oid column */ + AttrNumber attnum_updated; /* the timestamp column */ +} ModificationTrackedCatalogs[] = +{ + { + AuthIdRelationId, AUTHOID, Anum_pg_authid_rolupdated + }, + { + DatabaseRelationId, DATABASEOID, Anum_pg_database_datupdated + }, + { + TableSpaceRelationId, TABLESPACEOID, Anum_pg_tablespace_spcupdated + }, +}; + +/* + * GetObjectUpdatedAttnum + * Attribute number of a catalog's "last updated" column, or + * InvalidAttrNumber if the catalog does not track modification times. + * + * Generic DDL code that builds a replacement tuple can use this to stamp the + * column alongside whatever else it is changing. + */ +AttrNumber +GetObjectUpdatedAttnum(Oid classId) +{ + for (int i = 0; i < lengthof(ModificationTrackedCatalogs); i++) + { + if (ModificationTrackedCatalogs[i].class_oid == classId) + return ModificationTrackedCatalogs[i].attnum_updated; + } + + return InvalidAttrNumber; +} + +/* + * RecordObjectModification + * Stamp the current time on a global object's "last updated" column. + * + * This is only for DDL that changes an object's state without otherwise + * rewriting its catalog row, such as ALTER ROLE ... SET and + * ALTER DATABASE ... SET, whose settings live in pg_db_role_setting. + * + * A command that does update the row itself must instead set the column in + * the tuple it is already building. Calling this afterwards would both read + * a stale tuple from the syscache and update the same row twice in one + * command, which fails in simple_heap_update() with "tuple already updated + * by self". + * + * Does nothing if the catalog does not track modification times. The caller + * is expected to have already verified that the object exists and locked it, + * so a cache miss indicates a bug. + * + * No post-alter hook is invoked here; the calling command reports its own + * alteration, and this update is only internal bookkeeping. + */ +void +RecordObjectModification(Oid classId, Oid objectId) +{ + Relation rel; + HeapTuple tuple, + newtuple; + SysCacheIdentifier cacheId = SYSCACHEID_INVALID; + AttrNumber attnum = InvalidAttrNumber; + Datum *values; + bool *nulls; + bool *replaces; + int natts; + + attnum = GetObjectUpdatedAttnum(classId); + if (attnum == InvalidAttrNumber) + return; /* catalog does not track modifications */ + + for (int i = 0; i < lengthof(ModificationTrackedCatalogs); i++) + { + if (ModificationTrackedCatalogs[i].class_oid == classId) + { + cacheId = ModificationTrackedCatalogs[i].oid_cache_id; + break; + } + } + + rel = table_open(classId, RowExclusiveLock); + + tuple = SearchSysCache1(cacheId, ObjectIdGetDatum(objectId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for object %u in catalog \"%s\"", + objectId, RelationGetRelationName(rel)); + + natts = RelationGetDescr(rel)->natts; + values = palloc0(natts * sizeof(Datum)); + nulls = palloc0(natts * sizeof(bool)); + replaces = palloc0(natts * sizeof(bool)); + + values[attnum - 1] = TimestampTzGetDatum(GetCurrentTimestamp()); + replaces[attnum - 1] = true; + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), + values, nulls, replaces); + CatalogTupleUpdate(rel, &tuple->t_self, newtuple); + + ReleaseSysCache(tuple); + heap_freetuple(newtuple); + pfree(values); + pfree(nulls); + pfree(replaces); + + table_close(rel, NoLock); +} + /* * SQL callable interface for GetNewOidWithIndex(). Outside of initdb's * direct insertions into catalog tables, and recovering from corruption, this diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 8612d99a890..dccb0fa3614 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -33,7 +33,8 @@ CREATE VIEW pg_roles AS rolvaliduntil, rolbypassrls, setconfig as rolconfig, - pg_authid.oid + pg_authid.oid, + rolupdated FROM pg_authid LEFT JOIN pg_db_role_setting s ON (pg_authid.oid = setrole AND setdatabase = 0); @@ -47,7 +48,8 @@ CREATE VIEW pg_shadow AS rolbypassrls AS usebypassrls, rolpassword AS passwd, rolvaliduntil AS valuntil, - setconfig AS useconfig + setconfig AS useconfig, + rolupdated AS updated FROM pg_authid LEFT JOIN pg_db_role_setting s ON (pg_authid.oid = setrole AND setdatabase = 0) WHERE rolcanlogin; @@ -72,7 +74,8 @@ CREATE VIEW pg_user AS usebypassrls, '********'::text as passwd, valuntil, - useconfig + useconfig, + updated FROM pg_shadow; CREATE VIEW pg_policies AS diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index 26e05f061e2..afd60b5ef1f 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -17,6 +17,7 @@ #include "access/htup_details.h" #include "access/relation.h" #include "access/table.h" +#include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/namespace.h" @@ -65,6 +66,7 @@ #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" +#include "utils/timestamp.h" static Oid AlterObjectNamespace_internal(Relation rel, Oid objid, Oid nspOid); @@ -944,6 +946,7 @@ AlterObjectOwner_internal(Oid classId, Oid objectId, Oid new_ownerId) AttrNumber Anum_namespace = get_object_attnum_namespace(catalogId); AttrNumber Anum_acl = get_object_attnum_acl(catalogId); AttrNumber Anum_name = get_object_attnum_name(catalogId); + AttrNumber Anum_updated = GetObjectUpdatedAttnum(catalogId); Relation rel; HeapTuple oldtup; Datum datum; @@ -1030,6 +1033,17 @@ AlterObjectOwner_internal(Oid classId, Oid objectId, Oid new_ownerId) values[Anum_owner - 1] = ObjectIdGetDatum(new_ownerId); replaces[Anum_owner - 1] = true; + /* + * If this catalog records when its objects were last changed, stamp + * that here too, while we are already building the new tuple. + */ + if (Anum_updated != InvalidAttrNumber) + { + values[Anum_updated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + replaces[Anum_updated - 1] = true; + } + /* * Determine the modified ACL for the new owner. This is only * necessary when the ACL is non-null. diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index 7e3fc59eafd..39bd8abe1d2 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -71,6 +71,7 @@ #include "utils/relmapper.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/timestamp.h" #include "utils/wait_event.h" /* @@ -1533,6 +1534,9 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) */ new_record_nulls[Anum_pg_database_datacl - 1] = true; + new_record[Anum_pg_database_datupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + tuple = heap_form_tuple(RelationGetDescr(pg_database_rel), new_record, new_record_nulls); @@ -2032,7 +2036,28 @@ RenameDatabase(const char *oldname, const char *newname) elog(ERROR, "cache lookup failed for database %u", db_id); otid = newtup->t_self; namestrcpy(&(((Form_pg_database) GETSTRUCT(newtup))->datname), newname); - CatalogTupleUpdate(rel, &otid, newtup); + + /* + * datupdated sits past the fixed-length fields, so it cannot be set + * through GETSTRUCT like datname above; build a replacement tuple. It + * has to happen here rather than afterwards, since updating the row a + * second time in the same command would fail. + */ + { + Datum values[Natts_pg_database] = {0}; + bool nulls[Natts_pg_database] = {0}; + bool replaces[Natts_pg_database] = {0}; + HeapTuple stamped; + + values[Anum_pg_database_datupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + replaces[Anum_pg_database_datupdated - 1] = true; + + stamped = heap_modify_tuple(newtup, RelationGetDescr(rel), + values, nulls, replaces); + CatalogTupleUpdate(rel, &otid, stamped); + heap_freetuple(stamped); + } UnlockTuple(rel, &otid, InplaceUpdateTupleLock); InvokeObjectPostAlterHook(DatabaseRelationId, db_id, 0); @@ -2287,6 +2312,10 @@ movedb(const char *dbname, const char *tblspcname) new_record[Anum_pg_database_dattablespace - 1] = ObjectIdGetDatum(dst_tblspcoid); new_record_repl[Anum_pg_database_dattablespace - 1] = true; + new_record[Anum_pg_database_datupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + new_record_repl[Anum_pg_database_datupdated - 1] = true; + newtuple = heap_modify_tuple(oldtuple, RelationGetDescr(pgdbrel), new_record, new_record_nulls, new_record_repl); @@ -2570,6 +2599,10 @@ AlterDatabase(ParseState *pstate, AlterDatabaseStmt *stmt, bool isTopLevel) new_record_repl[Anum_pg_database_datconnlimit - 1] = true; } + new_record[Anum_pg_database_datupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + new_record_repl[Anum_pg_database_datupdated - 1] = true; + newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), new_record, new_record_nulls, new_record_repl); CatalogTupleUpdate(rel, &tuple->t_self, newtuple); @@ -2660,6 +2693,9 @@ AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt) values[Anum_pg_database_datcollversion - 1] = CStringGetTextDatum(newversion); replaces[Anum_pg_database_datcollversion - 1] = true; + values[Anum_pg_database_datupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + replaces[Anum_pg_database_datupdated - 1] = true; newtuple = heap_modify_tuple(tuple, RelationGetDescr(rel), values, nulls, replaces); @@ -2703,6 +2739,12 @@ AlterDatabaseSet(AlterDatabaseSetStmt *stmt) AlterSetting(datid, InvalidOid, stmt->setstmt); + /* + * ALTER DATABASE ... SET stores the setting in pg_db_role_setting rather + * than pg_database, so stamp the modification time separately. + */ + RecordObjectModification(DatabaseRelationId, datid); + UnlockSharedObject(DatabaseRelationId, datid, 0, AccessShareLock); return datid; @@ -2786,6 +2828,10 @@ AlterDatabaseOwner(const char *dbname, Oid newOwnerId) repl_repl[Anum_pg_database_datdba - 1] = true; repl_val[Anum_pg_database_datdba - 1] = ObjectIdGetDatum(newOwnerId); + repl_repl[Anum_pg_database_datupdated - 1] = true; + repl_val[Anum_pg_database_datupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + /* * Determine the modified ACL for the new owner. This is only * necessary when the ACL is non-null. diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c index e3c4a7fac87..b4d1c6e7a94 100644 --- a/src/backend/commands/tablespace.c +++ b/src/backend/commands/tablespace.c @@ -79,6 +79,7 @@ #include "utils/guc_hooks.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/timestamp.h" #include "utils/varlena.h" /* GUC variables */ @@ -350,6 +351,9 @@ CreateTableSpace(CreateTableSpaceStmt *stmt) else nulls[Anum_pg_tablespace_spcoptions - 1] = true; + values[Anum_pg_tablespace_spcupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + tuple = heap_form_tuple(rel->rd_att, values, nulls); CatalogTupleInsert(rel, tuple); @@ -1011,6 +1015,28 @@ RenameTableSpace(const char *oldname, const char *newname) /* OK, update the entry */ namestrcpy(&(newform->spcname), newname); + /* + * spcupdated sits past the fixed-length fields, so it cannot be set + * through the Form like spcname above; build a replacement tuple. It has + * to happen here rather than afterwards, since updating the row a second + * time in the same command would fail. + */ + { + Datum repl_val[Natts_pg_tablespace] = {0}; + bool repl_null[Natts_pg_tablespace] = {0}; + bool repl_repl[Natts_pg_tablespace] = {0}; + HeapTuple stamped; + + repl_val[Anum_pg_tablespace_spcupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + repl_repl[Anum_pg_tablespace_spcupdated - 1] = true; + + stamped = heap_modify_tuple(newtuple, RelationGetDescr(rel), + repl_val, repl_null, repl_repl); + heap_freetuple(newtuple); + newtuple = stamped; + } + CatalogTupleUpdate(rel, &newtuple->t_self, newtuple); InvokeObjectPostAlterHook(TableSpaceRelationId, tspId, 0); @@ -1079,6 +1105,9 @@ AlterTableSpaceOptions(AlterTableSpaceOptionsStmt *stmt) else repl_null[Anum_pg_tablespace_spcoptions - 1] = true; repl_repl[Anum_pg_tablespace_spcoptions - 1] = true; + repl_val[Anum_pg_tablespace_spcupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + repl_repl[Anum_pg_tablespace_spcupdated - 1] = true; newtuple = heap_modify_tuple(tup, RelationGetDescr(rel), repl_val, repl_null, repl_repl); diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 04b270c08a8..a81f05f9af6 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -39,6 +39,7 @@ #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/syscache.h" +#include "utils/timestamp.h" #include "utils/varlena.h" /* @@ -463,6 +464,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls); + new_record[Anum_pg_authid_rolupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + /* * pg_largeobject_metadata contains pg_authid.oid's, so we use the * binary-upgrade override. @@ -960,6 +964,14 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) new_record_repl[Anum_pg_authid_rolbypassrls - 1] = true; } + /* + * Record that an ALTER ROLE command was executed for this role. + * The timestamp advances even when the command did not actually change any value. + */ + new_record[Anum_pg_authid_rolupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + new_record_repl[Anum_pg_authid_rolupdated - 1] = true; + new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record, new_record_nulls, new_record_repl); CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple); @@ -999,7 +1011,6 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) return roleid; } - /* * ALTER ROLE ... SET */ @@ -1086,6 +1097,13 @@ AlterRoleSet(AlterRoleSetStmt *stmt) AlterSetting(databaseid, roleid, stmt->setstmt); + /* + * ALTER ROLE ... SET alters role state outside AlterRole(), so update + * rolupdated here. Ignore if stmt->role is NULL (global/database SET). + */ + if (OidIsValid(roleid)) + RecordObjectModification(AuthIdRelationId, roleid); + return roleid; } @@ -1467,6 +1485,11 @@ RenameRole(const char *oldname, const char *newname) (errmsg("MD5 password cleared because of role rename"))); } + repl_repl[Anum_pg_authid_rolupdated - 1] = true; + repl_val[Anum_pg_authid_rolupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + repl_null[Anum_pg_authid_rolupdated - 1] = false; + newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl); CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple); diff --git a/src/include/catalog/catalog.h b/src/include/catalog/catalog.h index feab3982cf5..1d75514ad87 100644 --- a/src/include/catalog/catalog.h +++ b/src/include/catalog/catalog.h @@ -41,6 +41,9 @@ extern bool IsSharedRelation(Oid relationId); extern bool IsPinnedObject(Oid classId, Oid objectId); +extern AttrNumber GetObjectUpdatedAttnum(Oid classId); +extern void RecordObjectModification(Oid classId, Oid objectId); + extern Oid GetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn); extern RelFileNumber GetNewRelFileNumber(Oid reltablespace, diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 31e49d5a8a7..38c978e53a8 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202608183 +#define CATALOG_VERSION_NO 202608213 #endif diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index 2960707c729..f52e1dbddbc 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -25,86 +25,103 @@ rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '6171', oid_symbol => 'ROLE_PG_DATABASE_OWNER', rolname => 'pg_database_owner', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '6181', oid_symbol => 'ROLE_PG_READ_ALL_DATA', rolname => 'pg_read_all_data', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '6182', oid_symbol => 'ROLE_PG_WRITE_ALL_DATA', rolname => 'pg_write_all_data', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '3373', oid_symbol => 'ROLE_PG_MONITOR', rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '3374', oid_symbol => 'ROLE_PG_READ_ALL_SETTINGS', rolname => 'pg_read_all_settings', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '3375', oid_symbol => 'ROLE_PG_READ_ALL_STATS', rolname => 'pg_read_all_stats', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '3377', oid_symbol => 'ROLE_PG_STAT_SCAN_TABLES', rolname => 'pg_stat_scan_tables', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '4569', oid_symbol => 'ROLE_PG_READ_SERVER_FILES', rolname => 'pg_read_server_files', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '4570', oid_symbol => 'ROLE_PG_WRITE_SERVER_FILES', rolname => 'pg_write_server_files', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '4571', oid_symbol => 'ROLE_PG_EXECUTE_SERVER_PROGRAM', rolname => 'pg_execute_server_program', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '4200', oid_symbol => 'ROLE_PG_SIGNAL_BACKEND', rolname => 'pg_signal_backend', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINT', rolname => 'pg_checkpoint', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '6337', oid_symbol => 'ROLE_PG_MAINTAIN', rolname => 'pg_maintain', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '4550', oid_symbol => 'ROLE_PG_USE_RESERVED_CONNECTIONS', rolname => 'pg_use_reserved_connections', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '6304', oid_symbol => 'ROLE_PG_CREATE_SUBSCRIPTION', rolname => 'pg_create_subscription', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, { oid => '6392', oid_symbol => 'ROLE_PG_SIGNAL_AUTOVACUUM_WORKER', rolname => 'pg_signal_autovacuum_worker', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rolupdated => '_null_' }, ] diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h index baf5b099797..6f3de221ff1 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -47,6 +47,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 #ifdef CATALOG_VARLEN /* variable-length fields start here */ text rolpassword; /* password, if any */ timestamptz rolvaliduntil; /* password expiration time, if any */ + timestamptz rolupdated; /* time of last CREATE ROLE / ALTER ROLE on + * this role, if any */ #endif } FormData_pg_authid; diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat index 4ce20dbe81f..bbf60e43b5e 100644 --- a/src/include/catalog/pg_database.dat +++ b/src/include/catalog/pg_database.dat @@ -19,6 +19,7 @@ datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0', datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE', datctype => 'LC_CTYPE', datlocale => 'DATLOCALE', - daticurules => 'ICU_RULES', datacl => '_null_' }, + daticurules => 'ICU_RULES', datacl => '_null_', + datupdated => '_null_' }, ] diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h index 8a495e96eed..325e8be6c07 100644 --- a/src/include/catalog/pg_database.h +++ b/src/include/catalog/pg_database.h @@ -87,6 +87,9 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID /* access permissions */ aclitem datacl[1]; + + /* time of last CREATE DATABASE / ALTER DATABASE on this database, if any */ + timestamptz datupdated; #endif } FormData_pg_database; diff --git a/src/include/catalog/pg_tablespace.dat b/src/include/catalog/pg_tablespace.dat index c4cde415219..187897d59dc 100644 --- a/src/include/catalog/pg_tablespace.dat +++ b/src/include/catalog/pg_tablespace.dat @@ -13,8 +13,10 @@ [ { oid => '1663', oid_symbol => 'DEFAULTTABLESPACE_OID', - spcname => 'pg_default', spcacl => '_null_', spcoptions => '_null_' }, + spcname => 'pg_default', spcacl => '_null_', spcoptions => '_null_', + spcupdated => '_null_' }, { oid => '1664', oid_symbol => 'GLOBALTABLESPACE_OID', - spcname => 'pg_global', spcacl => '_null_', spcoptions => '_null_' }, + spcname => 'pg_global', spcacl => '_null_', spcoptions => '_null_', + spcupdated => '_null_' }, ] diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h index 3bd4a74f003..0ebec5f2743 100644 --- a/src/include/catalog/pg_tablespace.h +++ b/src/include/catalog/pg_tablespace.h @@ -39,6 +39,12 @@ CATALOG(pg_tablespace,1213,TableSpaceRelationId) BKI_SHARED_RELATION #ifdef CATALOG_VARLEN /* variable-length fields start here */ aclitem spcacl[1]; /* access permissions */ text spcoptions[1]; /* per-tablespace options */ + + /* + * time of last CREATE TABLESPACE / ALTER TABLESPACE on this tablespace, + * if any + */ + timestamptz spcupdated; #endif } FormData_pg_tablespace; diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out index 6b879b0f62a..b8e90c2cc5c 100644 --- a/src/test/regress/expected/database.out +++ b/src/test/regress/expected/database.out @@ -1,9 +1,79 @@ CREATE DATABASE regression_tbd ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0; +-- template1 is created during bootstrap, so it has no modification time, +-- whereas CREATE DATABASE records one +SELECT datupdated IS NULL AS template1_is_null + FROM pg_database WHERE datname = 'template1'; + template1_is_null +------------------- + t +(1 row) + +SELECT datupdated IS NOT NULL AS create_sets_updated + FROM pg_database WHERE datname = 'regression_tbd'; + create_sets_updated +--------------------- + t +(1 row) + +SELECT datupdated AS ts_created + FROM pg_database WHERE datname = 'regression_tbd' \gset +-- each of the commands below advances datupdated; the check before +-- each one shows that the comparison discriminates +SELECT datname FROM pg_database + WHERE datname = 'regression_tbd' AND datupdated > :'ts_created'; + datname +--------- +(0 rows) + ALTER DATABASE regression_tbd RENAME TO regression_utf8; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_created'; + datname +----------------- + regression_utf8 +(1 row) + +SELECT datupdated AS ts_renamed + FROM pg_database WHERE datname = 'regression_utf8' \gset ALTER DATABASE regression_utf8 SET TABLESPACE regress_tblspace; ALTER DATABASE regression_utf8 SET TABLESPACE pg_default; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_renamed'; + datname +----------------- + regression_utf8 +(1 row) + +SELECT datupdated AS ts_moved + FROM pg_database WHERE datname = 'regression_utf8' \gset ALTER DATABASE regression_utf8 CONNECTION_LIMIT 123; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_moved'; + datname +----------------- + regression_utf8 +(1 row) + +SELECT datupdated AS ts_altered + FROM pg_database WHERE datname = 'regression_utf8' \gset +-- ALTER DATABASE ... SET stores the setting outside pg_database, but still +-- advances datupdated +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_altered'; + datname +--------- +(0 rows) + +ALTER DATABASE regression_utf8 SET work_mem = '10MB'; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_altered'; + datname +----------------- + regression_utf8 +(1 row) + +ALTER DATABASE regression_utf8 RESET work_mem; -- Test PgDatabaseToastTable. Doing this with GRANT would be slow. BEGIN; UPDATE pg_database @@ -14,7 +84,16 @@ ALTER DATABASE regression_utf8 RENAME TO regression_rename_rolled_back; ROLLBACK; CREATE ROLE regress_datdba_before; CREATE ROLE regress_datdba_after; +SELECT datupdated AS ts_before_owner + FROM pg_database WHERE datname = 'regression_utf8' \gset ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_before_owner'; + datname +----------------- + regression_utf8 +(1 row) + REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after; DROP DATABASE regression_utf8; DROP ROLE regress_datdba_before; diff --git a/src/test/regress/expected/role_updated.out b/src/test/regress/expected/role_updated.out new file mode 100644 index 00000000000..d540eb28ab7 --- /dev/null +++ b/src/test/regress/expected/role_updated.out @@ -0,0 +1,81 @@ +-- +-- Tests for pg_authid.rolupdated +-- +-- rolupdated records when the most recent CREATE ROLE or ALTER ROLE +-- command for a role was executed, so that tools managing roles +-- declaratively can cheaply detect that a role may have changed. +-- +-- Each command below is checked twice against the timestamp recorded +-- before it ran: first while nothing has happened yet, where no row +-- should qualify, and again afterwards, where the role should appear. +-- +-- roles created during initdb have no recorded modification time +SELECT rolupdated IS NULL AS bootstrap_role_is_null + FROM pg_authid WHERE rolname = 'pg_read_all_data'; + bootstrap_role_is_null +------------------------ + t +(1 row) + +-- CREATE ROLE records a timestamp +CREATE ROLE regress_updated_role; +SELECT rolupdated IS NOT NULL AS create_sets_updated + FROM pg_authid WHERE rolname = 'regress_updated_role'; + create_sets_updated +--------------------- + t +(1 row) + +SELECT rolupdated AS ts_created + FROM pg_authid WHERE rolname = 'regress_updated_role' \gset +-- changing an attribute advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_created'; + rolname +--------- +(0 rows) + +ALTER ROLE regress_updated_role WITH CONNECTION LIMIT 5; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_created'; + rolname +---------------------- + regress_updated_role +(1 row) + +SELECT rolupdated AS ts_altered + FROM pg_authid WHERE rolname = 'regress_updated_role' \gset +-- ALTER ROLE ... SET stores the setting outside pg_authid, but still +-- advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_altered'; + rolname +--------- +(0 rows) + +ALTER ROLE regress_updated_role SET work_mem = '10MB'; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_altered'; + rolname +---------------------- + regress_updated_role +(1 row) + +SELECT rolupdated AS ts_set + FROM pg_authid WHERE rolname = 'regress_updated_role' \gset +-- a rename advances it too, and the value is visible through pg_roles +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_set'; + rolname +--------- +(0 rows) + +ALTER ROLE regress_updated_role RENAME TO regress_updated_role2; +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_updated_role2' AND rolupdated > :'ts_set'; + rolname +----------------------- + regress_updated_role2 +(1 row) + +DROP ROLE regress_updated_role2; diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 1a29d46213e..d3b9fd3d09f 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1525,7 +1525,8 @@ pg_roles| SELECT pg_authid.rolname, pg_authid.rolvaliduntil, pg_authid.rolbypassrls, s.setconfig AS rolconfig, - pg_authid.oid + pg_authid.oid, + pg_authid.rolupdated FROM (pg_authid LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid)))); pg_rules| SELECT n.nspname AS schemaname, @@ -1763,7 +1764,8 @@ pg_shadow| SELECT pg_authid.rolname AS usename, pg_authid.rolbypassrls AS usebypassrls, pg_authid.rolpassword AS passwd, pg_authid.rolvaliduntil AS valuntil, - s.setconfig AS useconfig + s.setconfig AS useconfig, + pg_authid.rolupdated AS updated FROM (pg_authid LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid)))) WHERE pg_authid.rolcanlogin; @@ -2876,7 +2878,8 @@ pg_user| SELECT usename, usebypassrls, '********'::text AS passwd, valuntil, - useconfig + useconfig, + updated FROM pg_shadow; pg_user_mappings| SELECT u.oid AS umid, s.oid AS srvid, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..0a9f42722de 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,22 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- pg_default is created during bootstrap, so it has no modification time, +-- whereas CREATE TABLESPACE records one +SELECT spcupdated IS NULL AS pg_default_is_null + FROM pg_tablespace WHERE spcname = 'pg_default'; + pg_default_is_null +-------------------- + t +(1 row) + +SELECT spcupdated IS NOT NULL AS create_sets_updated + FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; + create_sets_updated +--------------------- + t +(1 row) + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, @@ -32,7 +48,22 @@ SELECT regexp_replace(pg_tablespace_location(oid), '(pg_tblspc)/(\d+)', '\1/NNN' (1 row) -- try setting and resetting some properties for the new tablespace +SELECT spcupdated AS ts_before_set + FROM pg_tablespace WHERE spcname = 'regress_tblspace' \gset +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace' AND spcupdated > :'ts_before_set'; + spcname +--------- +(0 rows) + ALTER TABLESPACE regress_tblspace SET (random_page_cost = 1.0, seq_page_cost = 1.1); +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace' AND spcupdated > :'ts_before_set'; + spcname +------------------ + regress_tblspace +(1 row) + ALTER TABLESPACE regress_tblspace SET (some_nonexistent_parameter = true); -- fail ERROR: unrecognized parameter "some_nonexistent_parameter" ALTER TABLESPACE regress_tblspace RESET (random_page_cost = 2.0); -- fail @@ -937,7 +968,16 @@ ROLLBACK; CREATE ROLE regress_tablespace_user1 login; CREATE ROLE regress_tablespace_user2 login; GRANT USAGE ON SCHEMA testschema TO regress_tablespace_user2; +SELECT spcupdated AS ts_before_owner + FROM pg_tablespace WHERE spcname = 'regress_tblspace' \gset ALTER TABLESPACE regress_tblspace OWNER TO regress_tablespace_user1; +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace' AND spcupdated > :'ts_before_owner'; + spcname +------------------ + regress_tblspace +(1 row) + CREATE TABLE testschema.tablespace_acl (c int); -- new owner lacks permission to create this index from scratch CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; @@ -951,7 +991,17 @@ ERROR: permission denied for tablespace regress_tblspace REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail ERROR: permission denied for tablespace regress_tblspace RESET ROLE; +SELECT spcupdated AS ts_before_rename + FROM pg_tablespace WHERE spcname = 'regress_tblspace' \gset ALTER TABLESPACE regress_tblspace RENAME TO regress_tblspace_renamed; +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace_renamed' + AND spcupdated > :'ts_before_rename'; + spcname +-------------------------- + regress_tblspace_renamed +(1 row) + ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; ALTER INDEX ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8356ca98ef2..4b4b5146dd3 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi # ---------- # Another group of parallel tests # ---------- -test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of +test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes role_updated create_am hash_func errors infinite_recurse create_property_graph for_portion_of # ---------- # sanity_check does a vacuum, affecting the sort order of SELECT * diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql index 4ef36127291..5f4ed224765 100644 --- a/src/test/regress/sql/database.sql +++ b/src/test/regress/sql/database.sql @@ -1,9 +1,46 @@ CREATE DATABASE regression_tbd ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0; + +-- template1 is created during bootstrap, so it has no modification time, +-- whereas CREATE DATABASE records one +SELECT datupdated IS NULL AS template1_is_null + FROM pg_database WHERE datname = 'template1'; +SELECT datupdated IS NOT NULL AS create_sets_updated + FROM pg_database WHERE datname = 'regression_tbd'; +SELECT datupdated AS ts_created + FROM pg_database WHERE datname = 'regression_tbd' \gset + +-- each of the commands below advances datupdated; the check before +-- each one shows that the comparison discriminates +SELECT datname FROM pg_database + WHERE datname = 'regression_tbd' AND datupdated > :'ts_created'; ALTER DATABASE regression_tbd RENAME TO regression_utf8; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_created'; +SELECT datupdated AS ts_renamed + FROM pg_database WHERE datname = 'regression_utf8' \gset + ALTER DATABASE regression_utf8 SET TABLESPACE regress_tblspace; ALTER DATABASE regression_utf8 SET TABLESPACE pg_default; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_renamed'; +SELECT datupdated AS ts_moved + FROM pg_database WHERE datname = 'regression_utf8' \gset + ALTER DATABASE regression_utf8 CONNECTION_LIMIT 123; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_moved'; +SELECT datupdated AS ts_altered + FROM pg_database WHERE datname = 'regression_utf8' \gset + +-- ALTER DATABASE ... SET stores the setting outside pg_database, but still +-- advances datupdated +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_altered'; +ALTER DATABASE regression_utf8 SET work_mem = '10MB'; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_altered'; +ALTER DATABASE regression_utf8 RESET work_mem; -- Test PgDatabaseToastTable. Doing this with GRANT would be slow. BEGIN; @@ -16,7 +53,11 @@ ROLLBACK; CREATE ROLE regress_datdba_before; CREATE ROLE regress_datdba_after; +SELECT datupdated AS ts_before_owner + FROM pg_database WHERE datname = 'regression_utf8' \gset ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before; +SELECT datname FROM pg_database + WHERE datname = 'regression_utf8' AND datupdated > :'ts_before_owner'; REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after; DROP DATABASE regression_utf8; diff --git a/src/test/regress/sql/role_updated.sql b/src/test/regress/sql/role_updated.sql new file mode 100644 index 00000000000..f915b3ca461 --- /dev/null +++ b/src/test/regress/sql/role_updated.sql @@ -0,0 +1,53 @@ +-- +-- Tests for pg_authid.rolupdated +-- +-- rolupdated records when the most recent CREATE ROLE or ALTER ROLE +-- command for a role was executed, so that tools managing roles +-- declaratively can cheaply detect that a role may have changed. +-- +-- Each command below is checked twice against the timestamp recorded +-- before it ran: first while nothing has happened yet, where no row +-- should qualify, and again afterwards, where the role should appear. +-- + +-- roles created during initdb have no recorded modification time +SELECT rolupdated IS NULL AS bootstrap_role_is_null + FROM pg_authid WHERE rolname = 'pg_read_all_data'; + +-- CREATE ROLE records a timestamp +CREATE ROLE regress_updated_role; +SELECT rolupdated IS NOT NULL AS create_sets_updated + FROM pg_authid WHERE rolname = 'regress_updated_role'; + +SELECT rolupdated AS ts_created + FROM pg_authid WHERE rolname = 'regress_updated_role' \gset + +-- changing an attribute advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_created'; +ALTER ROLE regress_updated_role WITH CONNECTION LIMIT 5; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_created'; + +SELECT rolupdated AS ts_altered + FROM pg_authid WHERE rolname = 'regress_updated_role' \gset + +-- ALTER ROLE ... SET stores the setting outside pg_authid, but still +-- advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_altered'; +ALTER ROLE regress_updated_role SET work_mem = '10MB'; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_altered'; + +SELECT rolupdated AS ts_set + FROM pg_authid WHERE rolname = 'regress_updated_role' \gset + +-- a rename advances it too, and the value is visible through pg_roles +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_updated_role' AND rolupdated > :'ts_set'; +ALTER ROLE regress_updated_role RENAME TO regress_updated_role2; +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_updated_role2' AND rolupdated > :'ts_set'; + +DROP ROLE regress_updated_role2; diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..1fae010619e 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,13 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- pg_default is created during bootstrap, so it has no modification time, +-- whereas CREATE TABLESPACE records one +SELECT spcupdated IS NULL AS pg_default_is_null + FROM pg_tablespace WHERE spcname = 'pg_default'; +SELECT spcupdated IS NOT NULL AS create_sets_updated + FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; @@ -26,7 +33,13 @@ SELECT regexp_replace(pg_tablespace_location(oid), '(pg_tblspc)/(\d+)', '\1/NNN' FROM pg_tablespace WHERE spcname = 'regress_tblspace'; -- try setting and resetting some properties for the new tablespace +SELECT spcupdated AS ts_before_set + FROM pg_tablespace WHERE spcname = 'regress_tblspace' \gset +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace' AND spcupdated > :'ts_before_set'; ALTER TABLESPACE regress_tblspace SET (random_page_cost = 1.0, seq_page_cost = 1.1); +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace' AND spcupdated > :'ts_before_set'; ALTER TABLESPACE regress_tblspace SET (some_nonexistent_parameter = true); -- fail ALTER TABLESPACE regress_tblspace RESET (random_page_cost = 2.0); -- fail ALTER TABLESPACE regress_tblspace RESET (random_page_cost, effective_io_concurrency); -- ok @@ -406,7 +419,11 @@ CREATE ROLE regress_tablespace_user1 login; CREATE ROLE regress_tablespace_user2 login; GRANT USAGE ON SCHEMA testschema TO regress_tablespace_user2; +SELECT spcupdated AS ts_before_owner + FROM pg_tablespace WHERE spcname = 'regress_tblspace' \gset ALTER TABLESPACE regress_tblspace OWNER TO regress_tablespace_user1; +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace' AND spcupdated > :'ts_before_owner'; CREATE TABLE testschema.tablespace_acl (c int); -- new owner lacks permission to create this index from scratch @@ -420,7 +437,12 @@ REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail RESET ROLE; +SELECT spcupdated AS ts_before_rename + FROM pg_tablespace WHERE spcname = 'regress_tblspace' \gset ALTER TABLESPACE regress_tblspace RENAME TO regress_tblspace_renamed; +SELECT spcname FROM pg_tablespace + WHERE spcname = 'regress_tblspace_renamed' + AND spcupdated > :'ts_before_rename'; ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; ALTER INDEX ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; -- 2.55.0