Skip to content

Commit 87dbbc1

Browse files
Use iceberg_catalog server OID as canonical REST catalog identity
Now that every REST catalog -- built-in and user-created -- is backed by a real iceberg_catalog foreign server, the server's OID is a stable, unambiguous identifier. This commit pivots the in-memory machinery off case-insensitive name comparison and onto OID equality. * RestCatalogOptions gains a leading `Oid serverOid` field, populated in BuildRestCatalogOptionsFromServer from server->serverid and propagated by CopyRestCatalogOptions. The struct comment is updated to call out that `catalog` is now purely a user-facing label kept around for error messages, and that `serverOid` is the canonical identity. * The per-catalog OAuth token cache is rekeyed from a NAMEDATALEN char buffer to sizeof(Oid). BuildTokenCacheKey is removed; the lookup site now passes &opts->serverOid directly, with a defensive Assert(OidIsValid(opts->serverOid)) so a future caller that forgets to resolve options cannot silently funnel every catalog into the same cache slot. InvalidateRestTokenCache keeps its existing full-flush behavior on any pg_foreign_server change; targeted invalidation is not worth the per-entry bookkeeping at the rate ALTER SERVER actually happens. * The cross-transaction "same REST catalog throughout" guard in BindRelationToXactRestCatalog and the belt-and-suspenders branch of RecordRestCatalogRequestInTx now compare serverOid directly instead of pg_strcasecmp'ing catalog names. The two user-facing names are still surfaced in errdetail so the message remains in the terms the user typed. This also closes the corner case where the same physical server is referenced via different casings in successive statements: they now collapse to the same OID and are treated as one catalog. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: sfc-gh-npuka <naisila.puka@snowflake.com>
1 parent 0e65cd7 commit 87dbbc1

3 files changed

Lines changed: 48 additions & 45 deletions

File tree

pg_lake_iceberg/include/pg_lake/rest_catalog/rest_catalog.h

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,25 @@ extern int RestCatalogAuthType;
3636
extern bool RestCatalogEnableVendedCredentials;
3737

3838
/*
39-
* Resolved REST catalog connection options. For the built-in 'rest'
40-
* catalog the fields come entirely from GUC settings. For user-created
41-
* catalogs (CREATE SERVER ... FOREIGN DATA WRAPPER iceberg_catalog) the
42-
* server options override the GUC defaults.
39+
* Resolved REST catalog connection options. All REST catalogs --
40+
* built-in ('rest') and user-created (CREATE SERVER ... FOREIGN DATA
41+
* WRAPPER iceberg_catalog) -- are backed by a real pg_foreign_server
42+
* row; ApplyGUCDefaults populates the defaults, ApplyServerOptionOverrides
43+
* layers on any per-server options.
44+
*
45+
* The canonical identity of a catalog is `serverOid` (the OID of the
46+
* iceberg_catalog server row). Use it for in-memory equality, token
47+
* cache keys, and syscache-driven invalidation. `catalog` stores the
48+
* user-visible short name (e.g. 'rest', 'my_polaris') purely for error
49+
* messages.
4350
*/
4451
typedef struct RestCatalogOptions
4552
{
46-
char *catalog; /* catalog name, used for token cache keying;
47-
* can be 'rest' or a user-created server name
48-
* of TYPE 'rest' */
53+
Oid serverOid; /* iceberg_catalog server OID; canonical
54+
* identity, never InvalidOid for resolved
55+
* opts */
56+
char *catalog; /* short user-facing name; used in error
57+
* messages, never for equality */
4958
char *host;
5059
char *oauthHostPath;
5160
char *clientId;

pg_lake_iceberg/src/rest_catalog/rest_catalog.c

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,12 @@ int RestCatalogAuthType = REST_CATALOG_AUTH_TYPE_OAUTH2;
6868
bool RestCatalogEnableVendedCredentials = true;
6969

7070
/*
71-
* Per-rest-catalog token cache. Keyed by catalog.
72-
* Should always be accessed via GetRestCatalogAccessToken()
71+
* Per-rest-catalog token cache, keyed by the iceberg_catalog server OID.
72+
* Should always be accessed via GetRestCatalogAccessToken().
7373
*/
74-
#define TOKEN_CACHE_KEY_LEN NAMEDATALEN
75-
7674
typedef struct RestCatalogTokenCacheEntry
7775
{
78-
char key[TOKEN_CACHE_KEY_LEN];
76+
Oid serverOid; /* hash key */
7977
char *accessToken;
8078
TimestampTz accessTokenExpiry;
8179
} RestCatalogTokenCacheEntry;
@@ -527,8 +525,7 @@ ValidateIcebergCatalogServerDDL(ProcessUtilityParams * processUtilityParams,
527525
* Renaming an iceberg_catalog server is blocked because dependent
528526
* iceberg tables store the server name as a string option
529527
* (catalog='<name>') in pg_foreign_table.ftoptions. A rename would
530-
* silently break those references. This covers both user-created
531-
* servers and the three built-in ones.
528+
* silently break those references.
532529
*/
533530
ForeignServer *server = GetForeignServerByName(strVal(stmt->object),
534531
true);
@@ -694,6 +691,7 @@ BuildRestCatalogOptionsFromServer(const char *serverName,
694691

695692
RestCatalogOptions *opts = palloc0(sizeof(RestCatalogOptions));
696693

694+
opts->serverOid = server->serverid;
697695
opts->catalog = pstrdup(userVisibleCatalog);
698696
ApplyGUCDefaults(opts);
699697
ApplyServerOptionOverrides(opts, server);
@@ -748,6 +746,7 @@ CopyRestCatalogOptions(MemoryContext dst, const RestCatalogOptions * src)
748746
MemoryContext oldctx = MemoryContextSwitchTo(dst);
749747
RestCatalogOptions *copy = palloc0(sizeof(RestCatalogOptions));
750748

749+
copy->serverOid = src->serverOid;
751750
copy->catalog = pstrdup(src->catalog);
752751
copy->host = pstrdup(src->host);
753752
copy->oauthHostPath = src->oauthHostPath ? pstrdup(src->oauthHostPath) : NULL;
@@ -1230,29 +1229,17 @@ ReportHTTPError(HttpResult httpResult, int level)
12301229
}
12311230

12321231

1233-
/*
1234-
* Build a cache key for the per-catalog token cache.
1235-
*/
1236-
static void
1237-
BuildTokenCacheKey(char *key, const RestCatalogOptions * opts)
1238-
{
1239-
Assert(opts->catalog != NULL);
1240-
MemSet(key, 0, TOKEN_CACHE_KEY_LEN);
1241-
strlcpy(key, opts->catalog, TOKEN_CACHE_KEY_LEN);
1242-
}
1243-
1244-
12451232
/*
12461233
* Syscache invalidation callback for pg_foreign_server changes.
12471234
* Any ALTER/DROP SERVER blows away the entire token cache so stale
12481235
* credentials are never reused. The cache is rebuilt lazily on the
12491236
* next token lookup.
12501237
*
12511238
* We ignore hashvalue and reset the whole cache rather than selectively
1252-
* invalidating a single server's entry (as postgres_fdw does). With a
1253-
* handful of servers and infrequent ALTER SERVER, the cost of a few
1254-
* extra OAuth round-trips is negligible compared to the complexity of
1255-
* keying the cache by OID for selective invalidation.
1239+
* invalidating a single server's entry. With a handful of servers and
1240+
* infrequent ALTER SERVER, the cost of a few extra OAuth round-trips is
1241+
* negligible compared to the complexity of tracking per-entry hash
1242+
* values for targeted invalidation.
12561243
*/
12571244
static void
12581245
InvalidateRestTokenCache(Datum arg, int cacheid, uint32 hashvalue)
@@ -1297,7 +1284,7 @@ InitTokenCacheIfNeeded(void)
12971284
HASHCTL ctl;
12981285

12991286
memset(&ctl, 0, sizeof(ctl));
1300-
ctl.keysize = TOKEN_CACHE_KEY_LEN;
1287+
ctl.keysize = sizeof(Oid);
13011288
ctl.entrysize = sizeof(RestCatalogTokenCacheEntry);
13021289
ctl.hcxt = RestTokenCacheCtx;
13031290

@@ -1309,7 +1296,7 @@ InitTokenCacheIfNeeded(void)
13091296

13101297
/*
13111298
* Gets an access token from rest catalog. Caches the token per catalog
1312-
* (keyed by catalog) until it is about to expire.
1299+
* (keyed by iceberg_catalog server OID) until it is about to expire.
13131300
*/
13141301
static char *
13151302
GetRestCatalogAccessToken(RestCatalogOptions * opts, bool forceRefreshToken)
@@ -1319,15 +1306,19 @@ GetRestCatalogAccessToken(RestCatalogOptions * opts, bool forceRefreshToken)
13191306
(errcode(ERRCODE_INTERNAL_ERROR),
13201307
errmsg("REST catalog options must not be NULL when fetching access token")));
13211308

1322-
InitTokenCacheIfNeeded();
1323-
1324-
char cacheKey[TOKEN_CACHE_KEY_LEN];
1309+
/*
1310+
* Every resolved RestCatalogOptions originates from
1311+
* BuildRestCatalogOptionsFromServer, which always sets serverOid. A
1312+
* missing OID would silently funnel every catalog into the same cache
1313+
* slot, so trap it loudly here.
1314+
*/
1315+
Assert(OidIsValid(opts->serverOid));
13251316

1326-
BuildTokenCacheKey(cacheKey, opts);
1317+
InitTokenCacheIfNeeded();
13271318

13281319
bool found = false;
13291320
RestCatalogTokenCacheEntry *entry =
1330-
hash_search(RestCatalogTokenCache, cacheKey, HASH_ENTER, &found);
1321+
hash_search(RestCatalogTokenCache, &opts->serverOid, HASH_ENTER, &found);
13311322

13321323
if (!found)
13331324
{

pg_lake_table/src/transaction/track_iceberg_metadata_changes.c

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -656,13 +656,14 @@ BindRelationToXactRestCatalog(Oid relationId)
656656
}
657657

658658
/*
659-
* Both sides of the comparison are the canonical catalog name (lowercase
660-
* "rest" for the built-in catalog, server name as stored in
661-
* pg_foreign_server for user-defined ones). pg_strcasecmp matches the
662-
* casing rules PostgreSQL applies to identifier resolution.
659+
* Identity is the iceberg_catalog server OID, not the user-typed name.
660+
* Two requests for the same physical server -- whether the user spelled
661+
* the catalog as 'rest', 'REST', or as the underlying built-in server
662+
* name on different statements -- collapse to the same OID and are
663+
* treated as one catalog. The user-facing names are still reported in
664+
* the error message.
663665
*/
664-
if (pg_strcasecmp(PgLakeXactRestCatalog->catalogOpts->catalog,
665-
resolvedOpts->catalog) != 0)
666+
if (PgLakeXactRestCatalog->catalogOpts->serverOid != resolvedOpts->serverOid)
666667
ereport(ERROR,
667668
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
668669
errmsg("cannot modify tables from different REST catalogs "
@@ -715,9 +716,11 @@ RecordRestCatalogRequestInTx(Oid relationId, RestCatalogOperationType operationT
715716
* bind through BindRelationToXactRestCatalog() at statement time or
716717
* populate catalogOpts via the branch above, so in practice we never
717718
* reach here with a mismatched catalog. Kept as a last line of
718-
* defense for any future code path that forgets to do so.
719+
* defense for any future code path that forgets to do so. See the
720+
* companion comment in BindRelationToXactRestCatalog for why identity
721+
* is the server OID, not the user-typed name.
719722
*/
720-
else if (pg_strcasecmp(PgLakeXactRestCatalog->catalogOpts->catalog, resolvedOpts->catalog) != 0)
723+
else if (PgLakeXactRestCatalog->catalogOpts->serverOid != resolvedOpts->serverOid)
721724
ereport(ERROR,
722725
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
723726
errmsg("cannot modify tables from different REST catalogs "

0 commit comments

Comments
 (0)