/*------------------------------------------------------------------------- * * pg_shdepend.c * routines to support manipulation of the pg_shdepend relation * * Portions Copyright (c) 1996-2004, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * IDENTIFICATION * $PostgreSQL$ * *------------------------------------------------------------------------- */ #include "postgres.h" #include "miscadmin.h" #include "access/genam.h" #include "access/heapam.h" #include "access/htup.h" #include "access/skey.h" #include "catalog/catname.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_shdepend.h" #include "commands/tablespace.h" #include "lib/stringinfo.h" #include "storage/proc.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/fmgroids.h" #include "utils/syscache.h" typedef enum { LOCAL_OBJECT, SHARED_OBJECT, REMOTE_OBJECT } objectType; static Oid classIdGetDbId(Oid classId); static void shdepLockAndCheckObject(Oid objectId, Oid classId); static void shdepAddDependency(Relation shdepRel, Oid classId, Oid objectId, Oid refclassId, Oid refobjId); static void shdepDropDependency(Relation shdepRel, Oid classId, Oid objectId, Oid refclassId, Oid refobjId); static void storeObjectDescription(StringInfo descs, objectType type, ObjectAddress *object, int count); /* * Record a dependency between 2 objects via their respective ObjectAddresses. * The first argument is the dependent object, the second the one it * references. * * This locks the referenced object and makes sure it still exists. * Then it creates an entry in pg_shdepend. The lock is kept until * the end of the transaction. */ void recordSharedDependencyOn(const ObjectAddress *depender, const ObjectAddress *referenced) { Relation shdepRel; /* * Objects in pg_shdepend can't have SubIds. * * XXX Is this restriction actually useful, besides saving space * in pg_shdepend? */ Assert(depender->objectSubId == 0); Assert(referenced->objectSubId == 0); /* * During bootstrap, do nothing since pg_shdepend may not exist yet. * initdb will fill in appropriate pg_shdepend entries after bootstrap. */ if (IsBootstrapProcessingMode()) return; shdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); shdepAddDependency(shdepRel, depender->classId, depender->objectId, referenced->classId, referenced->objectId); heap_close(shdepRel, RowExclusiveLock); } /* * recordDependencyOnCurrentUser * * A convenient form of recordSharedDependencyOn(). */ void recordDependencyOnCurrentUser(const ObjectAddress *object) { ObjectAddress referenced; referenced.classId = get_system_catalog_relid(ShadowRelationName); referenced.objectId = GetUserId(); referenced.objectSubId = 0; recordSharedDependencyOn(object, &referenced); } /* * shdependChangeOwner * * Update the shared dependencies to account for the new owner. */ void shdependChangeOwner(Oid classId, Oid objectId, int newOwnerSysId) { Relation sdepRel; ScanKeyData key[3]; SysScanDesc scan; HeapTuple tup; TupleDesc sdepDesc; /* * Make sure the new user doesn't go away while we record the dependency * on her. */ shdepLockAndCheckObject(newOwnerSysId, RelOid_pg_shadow); sdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); sdepDesc = RelationGetDescr(sdepRel); ScanKeyInit(&key[0], Anum_pg_shdepend_dbid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classIdGetDbId(classId))); ScanKeyInit(&key[1], Anum_pg_shdepend_classid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classId)); ScanKeyInit(&key[2], Anum_pg_shdepend_objid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(objectId)); scan = systable_beginscan(sdepRel, SharedDependDependerIndex, true, SnapshotNow, 3, key); while (HeapTupleIsValid(tup = systable_getnext(scan))) { Form_pg_shdepend sdepForm = (Form_pg_shdepend) GETSTRUCT(tup); HeapTuple newtup; char repl_nulls[Natts_pg_shdepend]; char replace[Natts_pg_shdepend]; Datum *values; /* Look for tuples referring to pg_shadow */ if (sdepForm->refclassid != RelOid_pg_shadow) continue; /* * Skip a tuple we just inserted. * * XXX Another possibility would be to break out of the loop * as soon as we modify one tuple. Not sure what is best. */ if (DatumGetInt32(sdepForm->refobjid) == newOwnerSysId) continue; values = (Datum *) palloc0(sizeof(Datum) * Natts_pg_shdepend); MemSet(repl_nulls, ' ', Natts_pg_shdepend); MemSet(replace, ' ', Natts_pg_shdepend); replace[Anum_pg_shdepend_refobjid - 1] = 'r'; values[Anum_pg_shdepend_refobjid - 1] = Int32GetDatum(newOwnerSysId); newtup = heap_modifytuple(tup, sdepDesc, values, repl_nulls, replace); simple_heap_update(sdepRel, &tup->t_self, newtup); /* Keep indexes current */ CatalogUpdateIndexes(sdepRel, newtup); pfree(values); heap_freetuple(newtup); } systable_endscan(scan); heap_close(sdepRel, RowExclusiveLock); } /* * shdependChangeTablespace * * Update the shared dependencies to account for the new tablespace. */ void shdependChangeTablespace(Oid classId, Oid objectId, Oid newTblspc) { Relation sdepRel; ScanKeyData key[3]; SysScanDesc scan; HeapTuple tup; int count = 0; TupleDesc sdepDesc; /* * Make sure the new tablespace doesn't go away while we record the * dependency on it. */ shdepLockAndCheckObject(newTblspc, RelOid_pg_tablespace); sdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); sdepDesc = RelationGetDescr(sdepRel); ScanKeyInit(&key[0], Anum_pg_shdepend_dbid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classIdGetDbId(classId))); ScanKeyInit(&key[1], Anum_pg_shdepend_classid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classId)); ScanKeyInit(&key[2], Anum_pg_shdepend_objid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(objectId)); scan = systable_beginscan(sdepRel, SharedDependDependerIndex, true, SnapshotNow, 3, key); while (HeapTupleIsValid(tup = systable_getnext(scan))) { Form_pg_shdepend sdepForm = (Form_pg_shdepend) GETSTRUCT(tup); HeapTuple newtup; char repl_nulls[Natts_pg_shdepend]; char replace[Natts_pg_shdepend]; Datum *values; /* Look for tuples referring to pg_tablespace */ if (sdepForm->refclassid != RelOid_pg_tablespace) continue; /* * Skip a tuple we just inserted; otherwise we could loop here * forever. If we do skip it, count it anyway, so we don't insert * a new entry when the user changes a relation's tablespace to * the same as before. * * XXX Another possibility would be to break out of the loop * as soon as we modify one tuple. Not sure what is best. */ if (DatumGetObjectId(sdepForm->refobjid) == newTblspc) { count++; continue; } values = (Datum *) palloc0(sizeof(Datum) * Natts_pg_shdepend); MemSet(repl_nulls, ' ', Natts_pg_shdepend); MemSet(replace, ' ', Natts_pg_shdepend); replace[Anum_pg_shdepend_refobjid - 1] = 'r'; values[Anum_pg_shdepend_refobjid - 1] = ObjectIdGetDatum(newTblspc); newtup = heap_modifytuple(tup, sdepDesc, values, repl_nulls, replace); simple_heap_update(sdepRel, &tup->t_self, newtup); /* Keep indexes current */ CatalogUpdateIndexes(sdepRel, newtup); count++; pfree(values); heap_freetuple(newtup); } systable_endscan(scan); /* * If we didn't update anything, then the object didn't have a * reference registered because it was using the default tablespace. * Register one now. */ if (count == 0) { ObjectAddress self, referenced; self.classId = classId; self.objectId = objectId; self.objectSubId = 0; referenced.classId = RelOid_pg_tablespace; referenced.objectId = newTblspc; referenced.objectSubId = 0; recordSharedDependencyOn(&self, &referenced); } heap_close(sdepRel, RowExclusiveLock); } /* * shdependUpdateAclInfo * Update the pg_shdepend info related to an object's ACL. * * We calculate the difference between the new and old lists of users and * groups, and the insert (if it's a grant) or delete (if it's a revoke) from * pg_shdepend as appropiate. * * Note that we can't insert blindly at grant, because we would end up with * duplicate registered dependencies. We could check for existence of the * tuple before inserting, but that is more expensive than what we are doing * now. On the other hand, we can't just delete the tuples blindly at revoke, * because the user may still have other privileges. * * FIXME -- most likely, this addition broke the handling of CHANGE OWNER. */ void shdependUpdateAclInfo(Oid classId, Oid objectId, AclId ownerId, bool isGrant, List *oldusers, List *newusers, List *oldgroups, List *newgroups) { List *users; List *groups; Relation shdepRel; ListCell *cell; /* * Calculate the differences between the old and new lists. * We have to count the owner as if she was a member of both * user lists! (We could substract him from both lists ...) */ oldusers = lappend_oid(oldusers, ownerId); newusers = lappend_oid(newusers, ownerId); if (isGrant) { users = list_difference_oid(newusers, oldusers); groups = list_difference_oid(newgroups, oldgroups); } else { users = list_difference_oid(oldusers, newusers); groups = list_difference_oid(oldgroups, newgroups); } /* If there are no differences, return early */ if (list_length(users) + list_length(groups) == 0) return; shdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); /* Handle users */ foreach (cell, users) { Oid user = lfirst_oid(cell); if (isGrant) shdepAddDependency(shdepRel, classId, objectId, RelOid_pg_shadow, user); else shdepDropDependency(shdepRel, classId, objectId, RelOid_pg_shadow, user); } /* Handle groups */ foreach(cell, groups) { Oid group = lfirst_oid(cell); if (isGrant) shdepAddDependency(shdepRel, classId, objectId, RelOid_pg_group, group); else shdepDropDependency(shdepRel, classId, objectId, RelOid_pg_group, group); } heap_close(shdepRel, RowExclusiveLock); } /* * A struct to keep track of dependencies found in other databases. */ typedef struct { Oid dbOid; int count; } remoteDep; /* * checkSharedDependencies * * Check whether there are shared dependency entries for a given global * object. Returns a string containing a newline-separated list of object * descriptions that depend on the shared object, or NULL if none is found. */ char * checkSharedDependencies(Oid classId, Oid objectId) { Relation sdepRel; ScanKeyData key[2]; SysScanDesc scan; HeapTuple tup; List *remDeps = NIL; ListCell *cell; ObjectAddress object; StringInfo descs; descs = makeStringInfo(); sdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); ScanKeyInit(&key[0], Anum_pg_shdepend_refclassid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classId)); ScanKeyInit(&key[1], Anum_pg_shdepend_refobjid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(objectId)); scan = systable_beginscan(sdepRel, SharedDependReferenceIndex, true, SnapshotNow, 2, key); while (HeapTupleIsValid(tup = systable_getnext(scan))) { Form_pg_shdepend sdepForm = (Form_pg_shdepend) GETSTRUCT(tup); object.classId = sdepForm->classid; object.objectId = sdepForm->objid; object.objectSubId = 0; /* * If it's a dependency local to this database or is a shared object, * describe it. * * If it's a remote dependency, keep track of it so we can * report the number of them later. */ if (sdepForm->dbid == MyProc->databaseId) storeObjectDescription(descs, LOCAL_OBJECT, &object, 0); else if (classIdGetDbId(sdepForm->classid) == InvalidOid) storeObjectDescription(descs, SHARED_OBJECT, &object, 0); else { /* It's not local nor shared, so it must be remote. */ remoteDep *dep; bool stored = false; /* * XXX this info is kept on a simple List. Maybe it's not good for * performance, but using a hash table hardly seems needlessly * complex. */ foreach(cell, remDeps) { dep = lfirst(cell); if (dep->dbOid == sdepForm->dbid) { dep->count++; stored = true; break; } } if (!stored) { dep = (remoteDep *) palloc(sizeof(remoteDep)); dep->dbOid = sdepForm->dbid; dep->count = 1; remDeps = lappend(remDeps, dep); } } } systable_endscan(scan); heap_close(sdepRel, RowExclusiveLock); foreach(cell, remDeps) { remoteDep *dep = lfirst(cell); object.classId = RelOid_pg_database; object.objectId = dep->dbOid; object.objectSubId = 0; storeObjectDescription(descs, REMOTE_OBJECT, &object, dep->count); } list_free_deep(remDeps); if (descs->len == 0) return NULL; return descs->data; } /* * copyTemplateDependencies * * Routine to create the initial shared dependencies of a new database. * We simply copy the dependencies from the template database. * * FIXME -- do something about locking the owner and tablespace of the * new database. Is it needed? Most likely the caller has locked them * already. */ void copyTemplateDependencies(Oid templateDbId, Oid newDbId) { Relation sdepRel; TupleDesc sdepDesc; ScanKeyData key[1]; SysScanDesc scan; HeapTuple tup; CatalogIndexState indstate; Datum *values; char repl_nulls[Natts_pg_shdepend]; char replace[Natts_pg_shdepend]; /* I think this lock is OK? */ sdepRel = heap_openr(SharedDependRelationName, RowShareLock); sdepDesc = RelationGetDescr(sdepRel); ScanKeyInit(&key[0], Anum_pg_shdepend_dbid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(templateDbId)); scan = systable_beginscan(sdepRel, SharedDependDependerIndex, true, SnapshotNow, 1, key); indstate = CatalogOpenIndexes(sdepRel); values = (Datum *) palloc0(sizeof(Datum) * Natts_pg_shdepend); MemSet(repl_nulls, ' ', Natts_pg_shdepend); MemSet(replace, ' ', Natts_pg_shdepend); replace[Anum_pg_shdepend_dbid - 1] = 'r'; values[Anum_pg_shdepend_dbid - 1] = ObjectIdGetDatum(newDbId); /* * Copy the entries of the original database, changing the database Id * to that of the new database. */ while (HeapTupleIsValid(tup = systable_getnext(scan))) { HeapTuple newtup; newtup = heap_modifytuple(tup, sdepDesc, values, repl_nulls, replace); simple_heap_insert(sdepRel, newtup); /* Keep indexes current */ CatalogIndexInsert(indstate, newtup); heap_freetuple(newtup); } pfree(values); systable_endscan(scan); CatalogCloseIndexes(indstate); heap_close(sdepRel, RowShareLock); } /* * dropDatabaseDependencies * * Delete pg_shdepend entries corresponding to a database that's being * dropped. */ void dropDatabaseDependencies(Oid databaseId) { Relation sdepRel; ScanKeyData key[3]; SysScanDesc scan; HeapTuple tup; sdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); /* * First, delete all the entries that have the database Oid in the * dbid field. */ ScanKeyInit(&key[0], Anum_pg_shdepend_dbid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(databaseId)); scan = systable_beginscan(sdepRel, SharedDependDependerIndex, true, SnapshotNow, 1, key); while (HeapTupleIsValid(tup = systable_getnext(scan))) { simple_heap_delete(sdepRel, &tup->t_self); } systable_endscan(scan); /* Now delete all entries corresponding to the database itself */ deleteSharedDependencyRecordsFor(RelOid_pg_database, databaseId); heap_close(sdepRel, RowExclusiveLock); } /* * deleteSharedDependencyRecordsFor * * Delete all pg_shdepend entries corresponding to a database-local object * that's being dropped or modified. */ void deleteSharedDependencyRecordsFor(Oid classId, Oid objectId) { Relation sdepRel; sdepRel = heap_openr(SharedDependRelationName, RowExclusiveLock); shdepDropDependency(sdepRel, classId, objectId, InvalidOid, InvalidOid); heap_close(sdepRel, RowExclusiveLock); } /* * shdepAddDependency * Internal workhorse for inserting into pg_shdepend */ static void shdepAddDependency(Relation shdepRel, Oid classId, Oid objectId, Oid refclassId, Oid refobjId) { HeapTuple tup; char nulls[Natts_pg_shdepend]; Datum values[Natts_pg_shdepend]; int i; /* * Make sure the object doesn't go away while we record the dependency * on it. DROP routines should lock the object exclusively before they * check shared dependencies. */ shdepLockAndCheckObject(refobjId, refclassId); MemSet(nulls, ' ', sizeof(nulls)); /* * Form the new tuple and record the dependency. */ i = 0; values[i++] = ObjectIdGetDatum(classIdGetDbId(classId)); /* dbid */ values[i++] = ObjectIdGetDatum(classId); /* classid */ values[i++] = ObjectIdGetDatum(objectId); /* objid */ values[i++] = ObjectIdGetDatum(refclassId); /* refclassid */ values[i++] = ObjectIdGetDatum(refobjId); /* refobjid */ tup = heap_formtuple(shdepRel->rd_att, values, nulls); simple_heap_insert(shdepRel, tup); /* keep indexes current */ CatalogUpdateIndexes(shdepRel, tup); /* clean up */ heap_freetuple(tup); } /* * shdepDropDependency * Internal workhorse for deleting entries from pg_shdepend. */ static void shdepDropDependency(Relation shdepRel, Oid classId, Oid objectId, Oid refclassId, Oid refobjId) { ScanKeyData key[3]; SysScanDesc scan; HeapTuple tup; ScanKeyInit(&key[0], Anum_pg_shdepend_dbid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classIdGetDbId(classId))); ScanKeyInit(&key[1], Anum_pg_shdepend_classid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(classId)); ScanKeyInit(&key[2], Anum_pg_shdepend_objid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(objectId)); scan = systable_beginscan(shdepRel, SharedDependDependerIndex, true, SnapshotNow, 3, key); while (HeapTupleIsValid(tup = systable_getnext(scan))) { Form_pg_shdepend shdepForm = (Form_pg_shdepend) GETSTRUCT(tup); /* * Skip entries not matching the referenced object's class Id * and object Id, if given. */ if (OidIsValid(refclassId) && shdepForm->refclassid != refclassId) continue; if (OidIsValid(refobjId) && shdepForm->refobjid != refobjId) continue; simple_heap_delete(shdepRel, &tup->t_self); } systable_endscan(scan); } /* * classIdGetDbId * * Get the database Id that should be used in pg_shdepend. For shared * objects, it's 0 (InvalidOid); for all other objects, it's the * current database Id. * * XXX optimize this. Keep a list of already checked entries somewhere? */ static Oid classIdGetDbId(Oid classId) { Relation class; Oid dbId; /* heap_open will fail if the classId doesn't exist */ class = heap_open(classId, AccessShareLock); if (class->rd_rel->relisshared) dbId = InvalidOid; else dbId = MyProc->databaseId; heap_close(class, AccessShareLock); return dbId; } /* * shdepLockAndCheckObject * * Lock the object that we are about to record a dependency on. * After it's locked, verify that it hasn't been dropped while we * weren't looking. If the object has been dropped, this function * does not return! */ static void shdepLockAndCheckObject(Oid objectId, Oid classId) { char *className = NULL; bool notfound = false; int cacheId = 0; LockSharedObject(objectId, classId, AccessShareLock); switch (classId) { case RelOid_pg_shadow: cacheId = SHADOWSYSID; className = "user"; break; case RelOid_pg_tablespace: { char *tablespace = get_tablespace_name(objectId); if (tablespace != NULL) { pfree(tablespace); return; } className = "tablespace"; notfound = true; } case RelOid_pg_group: cacheId = GROSYSID; className = "group"; break; default: elog(ERROR, "unrecognized classId %u", classId); } if (notfound || !SearchSysCacheExists(cacheId, ObjectIdGetDatum(objectId), 0, 0, 0)) { ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("%s %u was concurrently dropped", className, objectId))); } } /* storeObjectDescription * store the description of a depender object * * While searching for dependencies of a shared object, we stash the * descriptions of depender objects we find in a single string, that we later * pass to ereport() in the DETAIL field when somebody attempts to drop a * referenced shared object. * * When type is LOCAL_OBJECT or SHARED_OBJECT, we expect "object" to be the * depender object. When it's REMOTE_OBJECT, we expect "object" to be the * database name, and count to be nonzero. * * XXX what should the entry separator be? * * XXX do something if we have too many objects. We could overflow ereport()'s * stack. */ static void storeObjectDescription(StringInfo descs, objectType type, ObjectAddress *object, int count) { char *temp = getObjectDescription(object); /* separate entries with a newline */ if (descs->len != 0) appendStringInfoChar(descs, '\n'); switch (type) { case LOCAL_OBJECT: appendStringInfo(descs, "in this database: %s", temp); break; case SHARED_OBJECT: appendStringInfo(descs, "%s", temp); break; case REMOTE_OBJECT: appendStringInfo(descs, "in %s: %d objects", temp, count); break; default: elog(ERROR, "invalid object type %d", type); } pfree(temp); }