From a8e5f53190d43ffac648c648c275e5d24d04e3a8 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 10:12:23 +0800 Subject: [PATCH 01/11] catalog: add pg_foreign_catalog, pg_foreign_volume, pg_lake_table Introduce the three system catalogs backing Iceberg lake-table DDL: - pg_foreign_catalog: named foreign catalog bound to a foreign server - pg_foreign_volume: named foreign volume bound to a foreign server - pg_lake_table: per-relation lake-table metadata (type, catalog, volume, options) Register the headers in the catalog Makefile and bump CATALOG_VERSION_NO. No code references the catalogs yet; DDL/commands land in later commits. OIDs 8549-8558 / 9901-9902 verified free via unused_oids; duplicate_oids clean. --- src/backend/catalog/Makefile | 1 + src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_foreign_catalog.h | 55 ++++++++++++++++++++ src/include/catalog/pg_foreign_volume.h | 55 ++++++++++++++++++++ src/include/catalog/pg_lake_table.h | 66 ++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/include/catalog/pg_foreign_catalog.h create mode 100644 src/include/catalog/pg_foreign_volume.h create mode 100644 src/include/catalog/pg_lake_table.h diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 5679abf152a..44598e25cff 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -102,6 +102,7 @@ CATALOG_HEADERS := \ pg_subscription_rel.h gp_partition_template.h pg_task.h pg_task_run_history.h \ pg_profile.h pg_password_history.h pg_directory_table.h gp_storage_server.h \ gp_storage_user_mapping.h pg_tag.h pg_tag_description.h \ + pg_foreign_catalog.h pg_foreign_volume.h pg_lake_table.h \ gp_matview_aux.h \ gp_matview_tables.h diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 3de2e549f4c..3d5e3915585 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -60,6 +60,6 @@ */ /* 3yyymmddN */ -#define CATALOG_VERSION_NO 302512051 +#define CATALOG_VERSION_NO 302607021 #endif diff --git a/src/include/catalog/pg_foreign_catalog.h b/src/include/catalog/pg_foreign_catalog.h new file mode 100644 index 00000000000..7623cd9e807 --- /dev/null +++ b/src/include/catalog/pg_foreign_catalog.h @@ -0,0 +1,55 @@ +/*------------------------------------------------------------------------- + * + * pg_foreign_catalog.h + * definition of the "foreign catalog" system catalog (pg_foreign_catalog) + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_foreign_catalog.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_FOREIGN_CATALOG_H +#define PG_FOREIGN_CATALOG_H + +#include "catalog/genbki.h" +#include "catalog/pg_foreign_catalog_d.h" + +/* ---------------- + * pg_foreign_catalog definition. cpp turns this into + * typedef struct FormData_pg_foreign_catalog + * ---------------- + */ +CATALOG(pg_foreign_catalog,8549,ForeignCatalogRelationId) +{ + Oid oid; /* oid */ + + NameData fcname; /* foreign catalog name */ + + Oid fcowner BKI_LOOKUP(pg_authid); /* owner of the foreign catalog */ + + Oid fcserver BKI_LOOKUP(pg_foreign_server); /* foreign server this catalog belongs to */ + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + text fcoptions[1]; /* foreign catalog options */ +#endif +} FormData_pg_foreign_catalog; + +/* ---------------- + * Form_pg_foreign_catalog corresponds to a pointer to a tuple with + * the format of pg_foreign_catalog relation. + * ---------------- + */ +typedef FormData_pg_foreign_catalog *Form_pg_foreign_catalog; + +DECLARE_TOAST(pg_foreign_catalog, 8550, 8551); + +DECLARE_UNIQUE_INDEX_PKEY(pg_foreign_catalog_oid_index, 8552, ForeignCatalogOidIndexId, on pg_foreign_catalog using btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_foreign_catalog_name_index, 8553, ForeignCatalogNameIndexId, on pg_foreign_catalog using btree(fcname name_ops)); + +#endif /* PG_FOREIGN_CATALOG_H */ diff --git a/src/include/catalog/pg_foreign_volume.h b/src/include/catalog/pg_foreign_volume.h new file mode 100644 index 00000000000..669bb1246d0 --- /dev/null +++ b/src/include/catalog/pg_foreign_volume.h @@ -0,0 +1,55 @@ +/*------------------------------------------------------------------------- + * + * pg_foreign_volume.h + * definition of the "foreign volume" system catalog (pg_foreign_volume) + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_foreign_volume.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_FOREIGN_VOLUME_H +#define PG_FOREIGN_VOLUME_H + +#include "catalog/genbki.h" +#include "catalog/pg_foreign_volume_d.h" + +/* ---------------- + * pg_foreign_volume definition. cpp turns this into + * typedef struct FormData_pg_foreign_volume + * ---------------- + */ +CATALOG(pg_foreign_volume,8554,ForeignVolumeRelationId) +{ + Oid oid; /* oid */ + + NameData fvname; /* foreign volume name */ + + Oid fvowner BKI_LOOKUP(pg_authid); /* owner of the foreign volume */ + + Oid fvserver BKI_LOOKUP(pg_foreign_server); /* foreign server this volume belongs to */ + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + text fvoptions[1]; /* foreign volume options */ +#endif +} FormData_pg_foreign_volume; + +/* ---------------- + * Form_pg_foreign_volume corresponds to a pointer to a tuple with + * the format of pg_foreign_volume relation. + * ---------------- + */ +typedef FormData_pg_foreign_volume *Form_pg_foreign_volume; + +DECLARE_TOAST(pg_foreign_volume, 8555, 8556); + +DECLARE_UNIQUE_INDEX_PKEY(pg_foreign_volume_oid_index, 8557, ForeignVolumeOidIndexId, on pg_foreign_volume using btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_foreign_volume_name_index, 8558, ForeignVolumeNameIndexId, on pg_foreign_volume using btree(fvname name_ops)); + +#endif /* PG_FOREIGN_VOLUME_H */ diff --git a/src/include/catalog/pg_lake_table.h b/src/include/catalog/pg_lake_table.h new file mode 100644 index 00000000000..37c8a208d31 --- /dev/null +++ b/src/include/catalog/pg_lake_table.h @@ -0,0 +1,66 @@ +/*------------------------------------------------------------------------- + * + * pg_lake_table.h + * definition of the "lake table" system catalog (pg_lake_table) + * + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_lake_table.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_LAKE_TABLE_H +#define PG_LAKE_TABLE_H + +#include "catalog/genbki.h" +#include "catalog/pg_lake_table_d.h" +#include "nodes/pg_list.h" + +/* ---------------- + * pg_lake_table definition. cpp turns this into + * typedef struct FormData_pg_lake_table + * ---------------- + */ +CATALOG(pg_lake_table,9901,LakeTableRelationId) +{ + Oid ltrelid BKI_LOOKUP(pg_class); /* OID of the lake table relation */ + Oid ltforeign_catalog BKI_LOOKUP_OPT(pg_foreign_catalog); /* OID of foreign catalog */ + Oid ltforeign_volume BKI_LOOKUP_OPT(pg_foreign_volume); /* OID of foreign volume */ + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + text lttable_type; /* table type: ICEBERG, etc. */ + text ltoptions[1]; /* lake table options */ +#endif +} FormData_pg_lake_table; + +/* ---------------- + * Form_pg_lake_table corresponds to a pointer to a tuple with + * the format of pg_lake_table relation. + * ---------------- + */ +typedef FormData_pg_lake_table *Form_pg_lake_table; + +DECLARE_TOAST(pg_lake_table, 9903, 9904); + +DECLARE_UNIQUE_INDEX_PKEY(pg_lake_table_relid_index, 9902, LakeTableRelidIndexId, on pg_lake_table using btree(ltrelid oid_ops)); + +/* ---------------- + * Lake table structure for caching + * ---------------- + */ +typedef struct LakeTable +{ + Oid relid; /* OID of the lake table relation */ + char *table_type; /* table type: ICEBERG, etc. */ + char *foreign_catalog; /* foreign catalog name */ + char *foreign_volume; /* foreign volume name */ + List *options; /* lake table options */ +} LakeTable; + +#endif /* PG_LAKE_TABLE_H */ From c2bb7c0e6cc6bb5d37755d2e875403cf7b50581f Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 10:50:06 +0800 Subject: [PATCH 02/11] nodes: add parse nodes for Iceberg lake-table DDL Add three statement parse nodes and their hand-maintained node-support plumbing (copy/equal/out/read + fast serialization), mirroring the existing CreateDirectoryTableStmt and CreateForeignServerStmt patterns: - CreateLakeTableStmt (CREATE ICEBERG TABLE ...; embeds CreateStmt) - CreateForeignCatalogStmt (CREATE FOREIGN CATALOG ...) - CreateForeignVolumeStmt (CREATE FOREIGN VOLUME ...) Nodes are not yet produced by the grammar or dispatched; grammar and command handling land in later commits. ObjectType additions are deferred to the command commit to keep exhaustive switches complete. --- src/backend/nodes/copyfuncs.funcs.c | 65 ++++++++++++++++++++++++++++ src/backend/nodes/copyfuncs.switch.c | 9 ++++ src/backend/nodes/equalfuncs.c | 46 ++++++++++++++++++++ src/backend/nodes/outfast.c | 31 +++++++++++++ src/backend/nodes/outfuncs.c | 15 +++++++ src/backend/nodes/readfast.c | 50 +++++++++++++++++++++ src/include/nodes/nodes.h | 3 ++ src/include/nodes/parsenodes.h | 27 ++++++++++++ 8 files changed, 246 insertions(+) diff --git a/src/backend/nodes/copyfuncs.funcs.c b/src/backend/nodes/copyfuncs.funcs.c index 6f312d17574..2b7cd7cfe3e 100644 --- a/src/backend/nodes/copyfuncs.funcs.c +++ b/src/backend/nodes/copyfuncs.funcs.c @@ -2748,6 +2748,32 @@ _copyCreateForeignServerStmt(const CreateForeignServerStmt *from) return newnode; } +static CreateForeignCatalogStmt * +_copyCreateForeignCatalogStmt(const CreateForeignCatalogStmt *from) +{ + CreateForeignCatalogStmt *newnode = makeNode(CreateForeignCatalogStmt); + + COPY_STRING_FIELD(catalogname); + COPY_STRING_FIELD(servername); + COPY_SCALAR_FIELD(if_not_exists); + COPY_NODE_FIELD(options); + + return newnode; +} + +static CreateForeignVolumeStmt * +_copyCreateForeignVolumeStmt(const CreateForeignVolumeStmt *from) +{ + CreateForeignVolumeStmt *newnode = makeNode(CreateForeignVolumeStmt); + + COPY_STRING_FIELD(volumename); + COPY_STRING_FIELD(servername); + COPY_SCALAR_FIELD(if_not_exists); + COPY_NODE_FIELD(options); + + return newnode; +} + static AlterForeignServerStmt * _copyAlterForeignServerStmt(const AlterForeignServerStmt *from) { @@ -3358,6 +3384,45 @@ _copyCreateDirectoryTableStmt(const CreateDirectoryTableStmt *from) return newnode; } +static CreateLakeTableStmt * +_copyCreateLakeTableStmt(const CreateLakeTableStmt *from) +{ + CreateLakeTableStmt *newnode = makeNode(CreateLakeTableStmt); + + COPY_NODE_FIELD(base.relation); + COPY_NODE_FIELD(base.tableElts); + COPY_NODE_FIELD(base.inhRelations); + COPY_NODE_FIELD(base.partbound); + COPY_NODE_FIELD(base.partspec); + COPY_NODE_FIELD(base.ofTypename); + COPY_NODE_FIELD(base.constraints); + COPY_NODE_FIELD(base.options); + COPY_SCALAR_FIELD(base.oncommit); + COPY_STRING_FIELD(base.tablespacename); + COPY_STRING_FIELD(base.accessMethod); + COPY_SCALAR_FIELD(base.if_not_exists); + COPY_SCALAR_FIELD(base.gp_style_alter_part); + COPY_NODE_FIELD(base.distributedBy); + COPY_NODE_FIELD(base.partitionBy); + COPY_SCALAR_FIELD(base.relKind); + COPY_SCALAR_FIELD(base.ownerid); + COPY_SCALAR_FIELD(base.buildAoBlkdir); + COPY_NODE_FIELD(base.attr_encodings); + COPY_SCALAR_FIELD(base.isCtas); + COPY_NODE_FIELD(base.intoQuery); + COPY_NODE_FIELD(base.intoPolicy); + COPY_NODE_FIELD(base.part_idx_oids); + COPY_NODE_FIELD(base.part_idx_names); + COPY_NODE_FIELD(base.tags); + COPY_SCALAR_FIELD(base.origin); + COPY_STRING_FIELD(table_type); + COPY_STRING_FIELD(foreign_catalog); + COPY_STRING_FIELD(foreign_volume); + COPY_NODE_FIELD(options); + + return newnode; +} + static AlterDirectoryTableStmt * _copyAlterDirectoryTableStmt(const AlterDirectoryTableStmt *from) { diff --git a/src/backend/nodes/copyfuncs.switch.c b/src/backend/nodes/copyfuncs.switch.c index 69dcef19150..585e90be3c1 100644 --- a/src/backend/nodes/copyfuncs.switch.c +++ b/src/backend/nodes/copyfuncs.switch.c @@ -567,6 +567,12 @@ case T_CreateForeignServerStmt: retval = _copyCreateForeignServerStmt(from); break; + case T_CreateForeignCatalogStmt: + retval = _copyCreateForeignCatalogStmt(from); + break; + case T_CreateForeignVolumeStmt: + retval = _copyCreateForeignVolumeStmt(from); + break; case T_AlterForeignServerStmt: retval = _copyAlterForeignServerStmt(from); break; @@ -699,6 +705,9 @@ case T_CreateDirectoryTableStmt: retval = _copyCreateDirectoryTableStmt(from); break; + case T_CreateLakeTableStmt: + retval = _copyCreateLakeTableStmt(from); + break; case T_AlterDirectoryTableStmt: retval = _copyAlterDirectoryTableStmt(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index eb77ea9169f..8a8984237dd 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2163,6 +2163,28 @@ _equalCreateForeignServerStmt(const CreateForeignServerStmt *a, const CreateFore return true; } +static bool +_equalCreateForeignCatalogStmt(const CreateForeignCatalogStmt *a, const CreateForeignCatalogStmt *b) +{ + COMPARE_STRING_FIELD(catalogname); + COMPARE_STRING_FIELD(servername); + COMPARE_SCALAR_FIELD(if_not_exists); + COMPARE_NODE_FIELD(options); + + return true; +} + +static bool +_equalCreateForeignVolumeStmt(const CreateForeignVolumeStmt *a, const CreateForeignVolumeStmt *b) +{ + COMPARE_STRING_FIELD(volumename); + COMPARE_STRING_FIELD(servername); + COMPARE_SCALAR_FIELD(if_not_exists); + COMPARE_NODE_FIELD(options); + + return true; +} + static bool _equalAddForeignSegStmt(const AddForeignSegStmt *a, const AddForeignSegStmt *b) { @@ -3486,6 +3508,20 @@ _equalCreateDirectoryTableStmt(const CreateDirectoryTableStmt *a, const CreateDi return true; } +static bool +_equalCreateLakeTableStmt(const CreateLakeTableStmt *a, const CreateLakeTableStmt *b) +{ + if (!_equalCreateStmt(&a->base, &b->base)) + return false; + + COMPARE_STRING_FIELD(table_type); + COMPARE_STRING_FIELD(foreign_catalog); + COMPARE_STRING_FIELD(foreign_volume); + COMPARE_NODE_FIELD(options); + + return true; +} + static bool _equalAlterDirectoryTableStmt(const AlterDirectoryTableStmt *a, const AlterDirectoryTableStmt *b) { @@ -4247,6 +4283,12 @@ equal(const void *a, const void *b) case T_CreateForeignServerStmt: retval = _equalCreateForeignServerStmt(a, b); break; + case T_CreateForeignCatalogStmt: + retval = _equalCreateForeignCatalogStmt(a, b); + break; + case T_CreateForeignVolumeStmt: + retval = _equalCreateForeignVolumeStmt(a, b); + break; case T_AddForeignSegStmt: retval = _equalAddForeignSegStmt(a, b); break; @@ -4602,6 +4644,10 @@ equal(const void *a, const void *b) retval = _equalCreateDirectoryTableStmt(a, b); break; + case T_CreateLakeTableStmt: + retval = _equalCreateLakeTableStmt(a, b); + break; + case T_AlterDirectoryTableStmt: retval = _equalAlterDirectoryTableStmt(a, b); break; diff --git a/src/backend/nodes/outfast.c b/src/backend/nodes/outfast.c index f31bfa87045..42907eccab3 100644 --- a/src/backend/nodes/outfast.c +++ b/src/backend/nodes/outfast.c @@ -673,6 +673,28 @@ _outCreateForeignServerStmt(StringInfo str, CreateForeignServerStmt *node) WRITE_NODE_FIELD(options); } +static void +_outCreateForeignCatalogStmt(StringInfo str, CreateForeignCatalogStmt *node) +{ + WRITE_NODE_TYPE("CREATEFOREIGNCATALOGSTMT"); + + WRITE_STRING_FIELD(catalogname); + WRITE_STRING_FIELD(servername); + WRITE_BOOL_FIELD(if_not_exists); + WRITE_NODE_FIELD(options); +} + +static void +_outCreateForeignVolumeStmt(StringInfo str, CreateForeignVolumeStmt *node) +{ + WRITE_NODE_TYPE("CREATEFOREIGNVOLUMESTMT"); + + WRITE_STRING_FIELD(volumename); + WRITE_STRING_FIELD(servername); + WRITE_BOOL_FIELD(if_not_exists); + WRITE_NODE_FIELD(options); +} + static void _outAddForeignSegstmt(StringInfo str, AddForeignSegStmt *node) { @@ -1818,6 +1840,12 @@ _outNode(StringInfo str, void *obj) case T_CreateForeignServerStmt: _outCreateForeignServerStmt(str, obj); break; + case T_CreateForeignCatalogStmt: + _outCreateForeignCatalogStmt(str, obj); + break; + case T_CreateForeignVolumeStmt: + _outCreateForeignVolumeStmt(str, obj); + break; case T_AddForeignSegStmt: _outAddForeignSegstmt(str, obj); break; @@ -1940,6 +1968,9 @@ _outNode(StringInfo str, void *obj) case T_CreateDirectoryTableStmt: _outCreateDirectoryTableStmt(str, obj); break; + case T_CreateLakeTableStmt: + _outCreateLakeTableStmt(str, obj); + break; case T_AlterDirectoryTableStmt: _outAlterDirectoryTableStmt(str, obj); break; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index c48ded5a813..e8ea5b7bafc 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -4270,6 +4270,18 @@ _outCreateDirectoryTableStmt(StringInfo str, const CreateDirectoryTableStmt *nod WRITE_STRING_FIELD(location); } +static void +_outCreateLakeTableStmt(StringInfo str, const CreateLakeTableStmt *node) +{ + WRITE_NODE_TYPE("CREATELAKETABLESTMT"); + + _outCreateStmtInfo(str, (const CreateStmt *) node); + WRITE_STRING_FIELD(table_type); + WRITE_STRING_FIELD(foreign_catalog); + WRITE_STRING_FIELD(foreign_volume); + WRITE_NODE_FIELD(options); +} + static void _outAlterDirectoryTableStmt(StringInfo str, const AlterDirectoryTableStmt *node) { @@ -5612,6 +5624,9 @@ outNode(StringInfo str, const void *obj) case T_CreateDirectoryTableStmt: _outCreateDirectoryTableStmt(str, obj); break; + case T_CreateLakeTableStmt: + _outCreateLakeTableStmt(str, obj); + break; case T_AlterDirectoryTableStmt: _outAlterDirectoryTableStmt(str, obj); break; diff --git a/src/backend/nodes/readfast.c b/src/backend/nodes/readfast.c index ff3cb5eaddf..adb7d9e4811 100644 --- a/src/backend/nodes/readfast.c +++ b/src/backend/nodes/readfast.c @@ -1598,6 +1598,32 @@ _readCreateForeignServerStmt(void) READ_DONE(); } +static CreateForeignCatalogStmt * +_readCreateForeignCatalogStmt(void) +{ + READ_LOCALS(CreateForeignCatalogStmt); + + READ_STRING_FIELD(catalogname); + READ_STRING_FIELD(servername); + READ_BOOL_FIELD(if_not_exists); + READ_NODE_FIELD(options); + + READ_DONE(); +} + +static CreateForeignVolumeStmt * +_readCreateForeignVolumeStmt(void) +{ + READ_LOCALS(CreateForeignVolumeStmt); + + READ_STRING_FIELD(volumename); + READ_STRING_FIELD(servername); + READ_BOOL_FIELD(if_not_exists); + READ_NODE_FIELD(options); + + READ_DONE(); +} + static AddForeignSegStmt * _readAddForeignSegStmt(void) { @@ -1910,6 +1936,21 @@ _readCreateDirectoryTableStmt(void) READ_DONE(); } +static CreateLakeTableStmt * +_readCreateLakeTableStmt(void) +{ + READ_LOCALS(CreateLakeTableStmt); + + _readCreateStmt_common(&local_node->base); + + READ_STRING_FIELD(table_type); + READ_STRING_FIELD(foreign_catalog); + READ_STRING_FIELD(foreign_volume); + READ_NODE_FIELD(options); + + READ_DONE(); +} + static AlterDirectoryTableStmt * _readAlterDirectoryTableStmt(void) { @@ -2881,6 +2922,12 @@ readNodeBinary(void) case T_CreateForeignServerStmt: return_value = _readCreateForeignServerStmt(); break; + case T_CreateForeignCatalogStmt: + return_value = _readCreateForeignCatalogStmt(); + break; + case T_CreateForeignVolumeStmt: + return_value = _readCreateForeignVolumeStmt(); + break; case T_AddForeignSegStmt: return_value = _readAddForeignSegStmt(); break; @@ -2991,6 +3038,9 @@ readNodeBinary(void) case T_CreateDirectoryTableStmt: return_value = _readCreateDirectoryTableStmt(); break; + case T_CreateLakeTableStmt: + return_value = _readCreateLakeTableStmt(); + break; case T_AlterDirectoryTableStmt: return_value = _readAlterDirectoryTableStmt(); break; diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index bd2c1bcf58c..9efc253e44b 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -526,6 +526,8 @@ typedef enum NodeTag T_AlterFdwStmt, T_CreateForeignServerStmt, T_AlterForeignServerStmt, + T_CreateForeignCatalogStmt, + T_CreateForeignVolumeStmt, T_CreateStorageServerStmt, T_AlterStorageServerStmt, T_DropStorageServerStmt, @@ -572,6 +574,7 @@ typedef enum NodeTag T_CreateDirectoryTableStmt, T_AlterDirectoryTableStmt, T_DropDirectoryTableStmt, + T_CreateLakeTableStmt, T_CreateFileSpaceStmt, T_FileSpaceEntry, T_DropFileSpaceStmt, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b79846b3d6d..956e90f115e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3271,6 +3271,24 @@ typedef struct CreateForeignServerStmt List *options; /* generic options to server */ } CreateForeignServerStmt; +typedef struct CreateForeignCatalogStmt +{ + NodeTag type; + char *catalogname; /* foreign catalog name */ + char *servername; /* server name */ + bool if_not_exists; /* just do nothing if it already exists? */ + List *options; /* generic options to catalog */ +} CreateForeignCatalogStmt; + +typedef struct CreateForeignVolumeStmt +{ + NodeTag type; + char *volumename; /* foreign volume name */ + char *servername; /* server name */ + bool if_not_exists; /* just do nothing if it already exists? */ + List *options; /* generic options to volume */ +} CreateForeignVolumeStmt; + typedef struct AlterForeignServerStmt { NodeTag type; @@ -3778,6 +3796,15 @@ typedef struct CreateDirectoryTableStmt char *location; /* dtlocation for pg_directory_table */ } CreateDirectoryTableStmt; +typedef struct CreateLakeTableStmt +{ + CreateStmt base; /* base table creation info */ + char *table_type; /* lake table type, e.g. "ICEBERG" */ + char *foreign_catalog; /* foreign catalog name, or NULL */ + char *foreign_volume; /* foreign volume name, or NULL */ + List *options; /* lake-table-specific options */ +} CreateLakeTableStmt; + typedef struct AlterDirectoryTableStmt { NodeTag type; From b028fb8479c2b9843fafa44e6a524452aa65fd03 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 11:05:08 +0800 Subject: [PATCH 03/11] parser: add grammar for CREATE ICEBERG TABLE / FOREIGN CATALOG / VOLUME Add the ICEBERG and VOLUME unreserved keywords and the CREATE-side grammar productions that build the parse nodes from the previous commit: - CREATE ICEBERG TABLE name (cols) [FOREIGN CATALOG c] [FOREIGN VOLUME v] OPTIONS (...) -> CreateLakeTableStmt (forced DISTRIBUTED RANDOMLY) - CREATE FOREIGN CATALOG name SERVER s OPTIONS (...) -> CreateForeignCatalogStmt - CREATE FOREIGN VOLUME name SERVER s OPTIONS (...) -> CreateForeignVolumeStmt Statements parse but are not yet dispatched; command handling, ObjectType entries and DROP support land in the next commit. Verified: bison reports no grammar conflicts; the statements parse and reach ProcessUtility. --- src/backend/parser/gram.y | 156 +++++++++++++++++++++++++++++++++++- src/include/parser/kwlist.h | 2 + 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index bc657554219..c2bf21f7898 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -313,6 +313,7 @@ static void check_expressions_in_partition_key(PartitionSpec *spec, core_yyscan_ CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateStorageServerStmt CreateStorageUserMappingStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateDirectoryTableStmt + CreateLakeTableStmt CreateForeignCatalogStmt CreateForeignVolumeStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt CreateWarehouseStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -410,6 +411,7 @@ static void check_expressions_in_partition_key(PartitionSpec *spec, core_yyscan_ %type OptProfileElem %type opt_type +%type OptForeignCatalog OptForeignVolume %type foreign_server_version opt_foreign_server_version %type opt_in_database @@ -830,7 +832,7 @@ static void check_expressions_in_partition_key(PartitionSpec *spec, core_yyscan_ HANDLER HAVING HEADER_P HOLD HOUR_P - IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE + ICEBERG IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE INCLUDING INCREMENT INCREMENTAL INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION @@ -883,7 +885,7 @@ static void check_expressions_in_partition_key(PartitionSpec *spec, core_yyscan_ UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VOLATILE + VERBOSE VERSION_P VIEW VIEWS VOLATILE VOLUME WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1535,6 +1537,9 @@ stmt: | CreateConversionStmt | CreateDomainStmt | CreateDirectoryTableStmt + | CreateLakeTableStmt + | CreateForeignCatalogStmt + | CreateForeignVolumeStmt | CreateExtensionStmt | CreateExternalStmt | CreateFdwStmt @@ -9093,6 +9098,149 @@ CreateDirectoryTableStmt: } ; +/***************************************************************************** + * + * QUERY: + * CREATE FOREIGN CATALOG name SERVER server_name OPTIONS (...) + * + *****************************************************************************/ + +CreateForeignCatalogStmt: + CREATE FOREIGN CATALOG_P name SERVER name create_generic_options + { + CreateForeignCatalogStmt *n = makeNode(CreateForeignCatalogStmt); + n->catalogname = $4; + n->servername = $6; + n->options = $7; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE FOREIGN CATALOG_P IF_P NOT EXISTS name SERVER name create_generic_options + { + CreateForeignCatalogStmt *n = makeNode(CreateForeignCatalogStmt); + n->catalogname = $7; + n->servername = $9; + n->options = $10; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + +/***************************************************************************** + * + * QUERY: + * CREATE FOREIGN VOLUME name SERVER server_name OPTIONS (...) + * + *****************************************************************************/ + +CreateForeignVolumeStmt: + CREATE FOREIGN VOLUME name SERVER name create_generic_options + { + CreateForeignVolumeStmt *n = makeNode(CreateForeignVolumeStmt); + n->volumename = $4; + n->servername = $6; + n->options = $7; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE FOREIGN VOLUME IF_P NOT EXISTS name SERVER name create_generic_options + { + CreateForeignVolumeStmt *n = makeNode(CreateForeignVolumeStmt); + n->volumename = $7; + n->servername = $9; + n->options = $10; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + +OptForeignCatalog: + CATALOG_P name { $$ = $2; } + | /*EMPTY*/ { $$ = NULL; } + ; + +OptForeignVolume: + VOLUME name { $$ = $2; } + | /*EMPTY*/ { $$ = NULL; } + ; + +/***************************************************************************** + * + * QUERY: + * CREATE ICEBERG TABLE relname (columns) + * [FOREIGN CATALOG cat] [FOREIGN VOLUME vol] OPTIONS (...) + * + * A lake table stores its data on external object storage; fragments are + * not hash-distributed across segments, so the distribution policy is + * forced to RANDOM to keep UPDATE/DELETE correct. + * + *****************************************************************************/ + +CreateLakeTableStmt: + CREATE ICEBERG TABLE qualified_name '(' OptTableElementList ')' + OptForeignCatalog OptForeignVolume create_generic_options + OptDistributedBy table_access_method_clause + { + CreateLakeTableStmt *n = makeNode(CreateLakeTableStmt); + $4->relpersistence = RELPERSISTENCE_PERMANENT; + n->base.relation = $4; + n->base.tableElts = $6; + n->base.inhRelations = NIL; + n->base.ofTypename = NULL; + n->base.constraints = NIL; + n->base.options = NIL; + n->base.oncommit = ONCOMMIT_NOOP; + n->base.tablespacename = NULL; + n->base.accessMethod = $12 ? $12 : pstrdup("iceberg"); + n->base.if_not_exists = false; + n->base.relKind = RELKIND_RELATION; + n->table_type = pstrdup("ICEBERG"); + n->foreign_catalog = $8 ? pstrdup($8) : NULL; + n->foreign_volume = $9 ? pstrdup($9) : NULL; + n->options = $10; + if ($11 != NULL) + ereport(WARNING, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("DISTRIBUTED clause has no effect for lake tables, using DISTRIBUTED RANDOMLY"))); + n->base.distributedBy = makeNode(DistributedBy); + n->base.distributedBy->ptype = POLICYTYPE_PARTITIONED; + n->base.distributedBy->keyCols = NIL; + n->base.distributedBy->numsegments = -1; + $$ = (Node *) n; + } + | CREATE ICEBERG TABLE IF_P NOT EXISTS qualified_name '(' OptTableElementList ')' + OptForeignCatalog OptForeignVolume create_generic_options + OptDistributedBy table_access_method_clause + { + CreateLakeTableStmt *n = makeNode(CreateLakeTableStmt); + $7->relpersistence = RELPERSISTENCE_PERMANENT; + n->base.relation = $7; + n->base.tableElts = $9; + n->base.inhRelations = NIL; + n->base.ofTypename = NULL; + n->base.constraints = NIL; + n->base.options = NIL; + n->base.oncommit = ONCOMMIT_NOOP; + n->base.tablespacename = NULL; + n->base.accessMethod = $15 ? $15 : pstrdup("iceberg"); + n->base.if_not_exists = true; + n->base.relKind = RELKIND_RELATION; + n->table_type = pstrdup("ICEBERG"); + n->foreign_catalog = $11 ? pstrdup($11) : NULL; + n->foreign_volume = $12 ? pstrdup($12) : NULL; + n->options = $13; + if ($14 != NULL) + ereport(WARNING, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("DISTRIBUTED clause has no effect for lake tables, using DISTRIBUTED RANDOMLY"))); + n->base.distributedBy = makeNode(DistributedBy); + n->base.distributedBy->ptype = POLICYTYPE_PARTITIONED; + n->base.distributedBy->keyCols = NIL; + n->base.distributedBy->numsegments = -1; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -21175,6 +21323,7 @@ unreserved_keyword: | HOLD | HOST | HOUR_P + | ICEBERG | IDENTITY_P | IF_P | IGNORE_P @@ -21415,6 +21564,7 @@ unreserved_keyword: | VIEW | VIEWS | VOLATILE + | VOLUME | WAREHOUSE | WAREHOUSE_SIZE | WEB /* gp */ @@ -22162,6 +22312,7 @@ bare_label_keyword: | HEADER_P | HOLD | HOST + | ICEBERG | IDENTITY_P | IF_P | IGNORE_P @@ -22465,6 +22616,7 @@ bare_label_keyword: | VIEW | VIEWS | VOLATILE + | VOLUME | WAREHOUSE | WAREHOUSE_SIZE | WEB diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 24b6936bd46..795b317f175 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -225,6 +225,7 @@ PG_KEYWORD("header", HEADER_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("hold", HOLD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("host", HOST, UNRESERVED_KEYWORD, BARE_LABEL) /* GPDB */ PG_KEYWORD("hour", HOUR_P, UNRESERVED_KEYWORD, AS_LABEL) +PG_KEYWORD("iceberg", ICEBERG, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("identity", IDENTITY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("if", IF_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("ignore", IGNORE_P, UNRESERVED_KEYWORD, BARE_LABEL) @@ -548,6 +549,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("volume", VOLUME, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("warehouse", WAREHOUSE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("warehouse_size", WAREHOUSE_SIZE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("web", WEB, UNRESERVED_KEYWORD, BARE_LABEL) From 5a679b20f8d6bc870ccfbc1aac9c23ae81d42d7b Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 12:32:13 +0800 Subject: [PATCH 04/11] commands: implement CREATE/DROP FOREIGN CATALOG and FOREIGN VOLUME Make the foreign catalog and foreign volume DDL commands functional on top of the previously added grammar, parse nodes and system catalogs: * CreateForeignCatalog()/CreateForeignVolume() insert into pg_foreign_catalog/pg_foreign_volume, check server existence and USAGE privilege, record dependencies on the server and owner, and dispatch to segments with preassigned OIDs. * Lookup helpers get_foreign_catalog_oid(), get_foreign_volume_oid() and GetForeignVolumeByName(); both objects are unique on (name, server). * New object infrastructure: OBJECT_FOREIGN_CATALOG/OBJECT_FOREIGN_VOLUME and OCLASS_FOREIGN_CATALOG/OCLASS_FOREIGN_VOLUME with handlers in all exhaustive switches (objectaddress, dependency, aclchk, event trigger, seclabel, dropcmds, alter). Ownership checks go through the generic object_ownercheck() via the new ObjectProperty entries. * Four new syscaches: FOREIGNCATALOGNAME/FOREIGNCATALOGOID and FOREIGNVOLUMENAMESERVER/FOREIGNVOLUMEOID. * DROP CATALOG / DROP VOLUME grammar via drop_type_name, going through the regular RemoveObjects() path with dependency handling, so DROP SERVER ... CASCADE also removes dependent catalogs and volumes. * utility.c dispatch and CREATE/DROP FOREIGN CATALOG|VOLUME command tags. --- src/backend/catalog/aclchk.c | 12 ++ src/backend/catalog/dependency.c | 12 ++ src/backend/catalog/objectaddress.c | 154 ++++++++++++++++ src/backend/catalog/oid_dispatch.c | 36 ++++ src/backend/commands/alter.c | 2 + src/backend/commands/dropcmds.c | 8 + src/backend/commands/event_trigger.c | 12 ++ src/backend/commands/foreigncmds.c | 262 +++++++++++++++++++++++++++ src/backend/commands/seclabel.c | 2 + src/backend/foreign/foreign.c | 94 ++++++++++ src/backend/parser/gram.y | 2 + src/backend/tcop/utility.c | 26 +++ src/backend/utils/cache/syscache.c | 26 +++ src/include/catalog/dependency.h | 4 +- src/include/catalog/oid_dispatch.h | 4 + src/include/commands/defrem.h | 2 + src/include/foreign/foreign.h | 12 ++ src/include/nodes/parsenodes.h | 2 + src/include/tcop/cmdtaglist.h | 4 + src/include/utils/syscache.h | 4 + 20 files changed, 679 insertions(+), 1 deletion(-) diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 28e37f72ba1..573310a42e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -3050,6 +3050,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_FOREIGN_SERVER: msg = gettext_noop("permission denied for foreign server %s"); break; + case OBJECT_FOREIGN_CATALOG: + msg = gettext_noop("permission denied for foreign catalog %s"); + break; + case OBJECT_FOREIGN_VOLUME: + msg = gettext_noop("permission denied for foreign volume %s"); + break; case OBJECT_FOREIGN_TABLE: msg = gettext_noop("permission denied for foreign table %s"); break; @@ -3198,6 +3204,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_FOREIGN_SERVER: msg = gettext_noop("must be owner of foreign server %s"); break; + case OBJECT_FOREIGN_CATALOG: + msg = gettext_noop("must be owner of foreign catalog %s"); + break; + case OBJECT_FOREIGN_VOLUME: + msg = gettext_noop("must be owner of foreign volume %s"); + break; case OBJECT_FOREIGN_TABLE: msg = gettext_noop("must be owner of foreign table %s"); break; diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index f48824d9edc..2621f12ed19 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -42,8 +42,10 @@ #include "catalog/pg_directory_table.h" #include "catalog/pg_event_trigger.h" #include "catalog/pg_extension.h" +#include "catalog/pg_foreign_catalog.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_foreign_volume.h" #include "catalog/pg_init_privs.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject.h" @@ -226,6 +228,8 @@ static const Oid object_classes[] = { ExtprotocolRelationId, /* OCLASS_EXTPROTOCOL */ GpMatviewAuxId, /* OCLASS_MATVIEW_AUX */ TaskRelationId, /* OCLASS_TASK */ + ForeignCatalogRelationId, /* OCLASS_FOREIGN_CATALOG */ + ForeignVolumeRelationId, /* OCLASS_FOREIGN_VOLUME */ }; /* @@ -1629,6 +1633,8 @@ doDeletion(const ObjectAddress *object, int flags) case OCLASS_TSTEMPLATE: case OCLASS_FDW: case OCLASS_FOREIGN_SERVER: + case OCLASS_FOREIGN_CATALOG: + case OCLASS_FOREIGN_VOLUME: case OCLASS_USER_MAPPING: case OCLASS_DEFACL: case OCLASS_EVENT_TRIGGER: @@ -3141,6 +3147,12 @@ getObjectClass(const ObjectAddress *object) case TagDescriptionRelationId: return OCLASS_TAG_DESCRIPTION; + case ForeignCatalogRelationId: + return OCLASS_FOREIGN_CATALOG; + + case ForeignVolumeRelationId: + return OCLASS_FOREIGN_VOLUME; + default: { struct CustomObjectClass *coc; diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index db5294c89e9..3432197bfb7 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -40,8 +40,10 @@ #include "catalog/pg_event_trigger.h" #include "catalog/pg_extension.h" #include "catalog/pg_extprotocol.h" +#include "catalog/pg_foreign_catalog.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_foreign_volume.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject.h" #include "catalog/pg_largeobject_metadata.h" @@ -305,6 +307,34 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_FOREIGN_SERVER, true }, + { + "foreign catalog", + ForeignCatalogRelationId, + ForeignCatalogOidIndexId, + FOREIGNCATALOGOID, + FOREIGNCATALOGNAME, + Anum_pg_foreign_catalog_oid, + Anum_pg_foreign_catalog_fcname, + InvalidAttrNumber, + Anum_pg_foreign_catalog_fcowner, + InvalidAttrNumber, + OBJECT_FOREIGN_CATALOG, + true + }, + { + "foreign volume", + ForeignVolumeRelationId, + ForeignVolumeOidIndexId, + FOREIGNVOLUMEOID, + FOREIGNVOLUMENAME, + Anum_pg_foreign_volume_oid, + Anum_pg_foreign_volume_fvname, + InvalidAttrNumber, + Anum_pg_foreign_volume_fvowner, + InvalidAttrNumber, + OBJECT_FOREIGN_VOLUME, + true + }, { "storage server", StorageServerRelationId, @@ -995,6 +1025,14 @@ static const struct object_type_map /* OCLASS_TAG */ { "tag", OBJECT_TAG + }, + /* OCLASS_FOREIGN_CATALOG */ + { + "catalog", OBJECT_FOREIGN_CATALOG + }, + /* OCLASS_FOREIGN_VOLUME */ + { + "volume", OBJECT_FOREIGN_VOLUME } }; @@ -1165,6 +1203,8 @@ get_object_address(ObjectType objtype, Node *object, case OBJECT_LANGUAGE: case OBJECT_FDW: case OBJECT_FOREIGN_SERVER: + case OBJECT_FOREIGN_CATALOG: + case OBJECT_FOREIGN_VOLUME: case OBJECT_EVENT_TRIGGER: case OBJECT_EXTPROTOCOL: case OBJECT_PARAMETER_ACL: @@ -1472,6 +1512,16 @@ get_object_address_unqualified(ObjectType objtype, address.objectId = get_foreign_server_oid(name, missing_ok); address.objectSubId = 0; break; + case OBJECT_FOREIGN_CATALOG: + address.classId = ForeignCatalogRelationId; + address.objectId = get_foreign_catalog_oid(name, missing_ok); + address.objectSubId = 0; + break; + case OBJECT_FOREIGN_VOLUME: + address.classId = ForeignVolumeRelationId; + address.objectId = get_foreign_volume_oid(name, missing_ok); + address.objectSubId = 0; + break; case OBJECT_EVENT_TRIGGER: address.classId = EventTriggerRelationId; address.objectId = get_event_trigger_oid(name, missing_ok); @@ -2506,6 +2556,8 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_EXTENSION: case OBJECT_FDW: case OBJECT_FOREIGN_SERVER: + case OBJECT_FOREIGN_CATALOG: + case OBJECT_FOREIGN_VOLUME: case OBJECT_STORAGE_SERVER: case OBJECT_LANGUAGE: case OBJECT_PARAMETER_ACL: @@ -2663,6 +2715,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_EXTENSION: case OBJECT_FDW: case OBJECT_FOREIGN_SERVER: + case OBJECT_FOREIGN_CATALOG: + case OBJECT_FOREIGN_VOLUME: case OBJECT_LANGUAGE: case OBJECT_PUBLICATION: case OBJECT_SCHEMA: @@ -3955,6 +4009,50 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case OCLASS_FOREIGN_CATALOG: + { + HeapTuple catTup; + Form_pg_foreign_catalog catForm; + + catTup = SearchSysCache1(FOREIGNCATALOGOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(catTup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for foreign catalog %u", + object->objectId); + break; + } + + catForm = (Form_pg_foreign_catalog) GETSTRUCT(catTup); + appendStringInfo(&buffer, _("catalog %s"), + NameStr(catForm->fcname)); + ReleaseSysCache(catTup); + break; + } + + case OCLASS_FOREIGN_VOLUME: + { + HeapTuple volTup; + Form_pg_foreign_volume volForm; + + volTup = SearchSysCache1(FOREIGNVOLUMEOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(volTup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for foreign volume %u", + object->objectId); + break; + } + + volForm = (Form_pg_foreign_volume) GETSTRUCT(volTup); + appendStringInfo(&buffer, _("volume %s"), + NameStr(volForm->fvname)); + ReleaseSysCache(volTup); + break; + } + case OCLASS_USER_MAPPING: { HeapTuple tup; @@ -4949,6 +5047,14 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "server"); break; + case OCLASS_FOREIGN_CATALOG: + appendStringInfoString(&buffer, "catalog"); + break; + + case OCLASS_FOREIGN_VOLUME: + appendStringInfoString(&buffer, "volume"); + break; + case OCLASS_USER_MAPPING: appendStringInfoString(&buffer, "user mapping"); break; @@ -6043,6 +6149,54 @@ getObjectIdentityParts(const ObjectAddress *object, break; } + case OCLASS_FOREIGN_CATALOG: + { + HeapTuple catTup; + Form_pg_foreign_catalog catForm; + + catTup = SearchSysCache1(FOREIGNCATALOGOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(catTup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for foreign catalog %u", + object->objectId); + break; + } + + catForm = (Form_pg_foreign_catalog) GETSTRUCT(catTup); + appendStringInfoString(&buffer, + quote_identifier(NameStr(catForm->fcname))); + if (objname) + *objname = list_make1(pstrdup(NameStr(catForm->fcname))); + ReleaseSysCache(catTup); + break; + } + + case OCLASS_FOREIGN_VOLUME: + { + HeapTuple volTup; + Form_pg_foreign_volume volForm; + + volTup = SearchSysCache1(FOREIGNVOLUMEOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(volTup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for foreign volume %u", + object->objectId); + break; + } + + volForm = (Form_pg_foreign_volume) GETSTRUCT(volTup); + appendStringInfoString(&buffer, + quote_identifier(NameStr(volForm->fvname))); + if (objname) + *objname = list_make1(pstrdup(NameStr(volForm->fvname))); + ReleaseSysCache(volTup); + break; + } + case OCLASS_STORAGE_SERVER: { StorageServer *srv; diff --git a/src/backend/catalog/oid_dispatch.c b/src/backend/catalog/oid_dispatch.c index 888c66b6a73..5271c65cc9f 100644 --- a/src/backend/catalog/oid_dispatch.c +++ b/src/backend/catalog/oid_dispatch.c @@ -98,8 +98,10 @@ #include "catalog/pg_extension.h" #include "catalog/pg_extprotocol.h" #include "catalog/pg_event_trigger.h" +#include "catalog/pg_foreign_catalog.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" +#include "catalog/pg_foreign_volume.h" #include "catalog/pg_language.h" #include "catalog/pg_largeobject_metadata.h" #include "catalog/pg_namespace.h" @@ -880,6 +882,40 @@ GetNewOidForForeignServer(Relation relation, Oid indexId, AttrNumber oidcolumn, } +Oid +GetNewOidForForeignCatalog(Relation relation, Oid indexId, AttrNumber oidcolumn, + char *catname) +{ + OidAssignment key; + + Assert(RelationGetRelid(relation) == ForeignCatalogRelationId); + Assert(indexId == ForeignCatalogOidIndexId); + Assert(oidcolumn == Anum_pg_foreign_catalog_oid); + + memset(&key, 0, sizeof(OidAssignment)); + key.type = T_OidAssignment; + key.objname = catname; + return GetNewOrPreassignedOid(relation, indexId, oidcolumn, &key); + +} + +Oid +GetNewOidForForeignVolume(Relation relation, Oid indexId, AttrNumber oidcolumn, + char *volumename) +{ + OidAssignment key; + + Assert(RelationGetRelid(relation) == ForeignVolumeRelationId); + Assert(indexId == ForeignVolumeOidIndexId); + Assert(oidcolumn == Anum_pg_foreign_volume_oid); + + memset(&key, 0, sizeof(OidAssignment)); + key.type = T_OidAssignment; + key.objname = volumename; + return GetNewOrPreassignedOid(relation, indexId, oidcolumn, &key); + +} + Oid GetNewOidForStorageServer(Relation relation, Oid indexId, AttrNumber oidcolumn, char *srvname) diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index 31c290530d7..b125d7b560b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -774,6 +774,8 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case OCLASS_STORAGE_USER_MAPPING: case OCLASS_TAG: case OCLASS_TAG_DESCRIPTION: + case OCLASS_FOREIGN_CATALOG: + case OCLASS_FOREIGN_VOLUME: /* ignore object types that don't have schema-qualified names */ break; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 9bed9866aac..1a6a3e38ef4 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -456,6 +456,14 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("server \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_FOREIGN_CATALOG: + msg = gettext_noop("foreign catalog \"%s\" does not exist, skipping"); + name = strVal(object); + break; + case OBJECT_FOREIGN_VOLUME: + msg = gettext_noop("foreign volume \"%s\" does not exist, skipping"); + name = strVal(object); + break; case OBJECT_STORAGE_SERVER: msg = gettext_noop("storage server \"%s\" does not exist, skipping"); name = strVal(object); diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index f46567a5b0c..e609f9bb0f1 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -965,6 +965,8 @@ EventTriggerSupportsObjectType(ObjectType obtype) case OBJECT_EXTENSION: case OBJECT_FDW: case OBJECT_FOREIGN_SERVER: + case OBJECT_FOREIGN_CATALOG: + case OBJECT_FOREIGN_VOLUME: case OBJECT_FOREIGN_TABLE: case OBJECT_FUNCTION: case OBJECT_INDEX: @@ -1067,6 +1069,8 @@ EventTriggerSupportsObjectClass(ObjectClass objclass) case OCLASS_TSCONFIG: case OCLASS_FDW: case OCLASS_FOREIGN_SERVER: + case OCLASS_FOREIGN_CATALOG: + case OCLASS_FOREIGN_VOLUME: case OCLASS_USER_MAPPING: case OCLASS_DEFACL: case OCLASS_EXTENSION: @@ -2063,6 +2067,10 @@ stringify_grant_objtype(ObjectType objtype) return "FOREIGN DATA WRAPPER"; case OBJECT_FOREIGN_SERVER: return "FOREIGN SERVER"; + case OBJECT_FOREIGN_CATALOG: + return "FOREIGN CATALOG"; + case OBJECT_FOREIGN_VOLUME: + return "FOREIGN VOLUME"; case OBJECT_STORAGE_SERVER: return "STORAGE SERVER"; case OBJECT_FUNCTION: @@ -2157,6 +2165,10 @@ stringify_adefprivs_objtype(ObjectType objtype) return "FOREIGN DATA WRAPPERS"; case OBJECT_FOREIGN_SERVER: return "FOREIGN SERVERS"; + case OBJECT_FOREIGN_CATALOG: + return "FOREIGN CATALOGS"; + case OBJECT_FOREIGN_VOLUME: + return "FOREIGN VOLUMES"; case OBJECT_STORAGE_SERVER: return "STORAGE SERVERS"; case OBJECT_FUNCTION: diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index e260af42188..c9a1e53787e 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -23,10 +23,12 @@ #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/oid_dispatch.h" +#include "catalog/pg_foreign_catalog.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" #include "catalog/pg_foreign_table.h" #include "catalog/pg_foreign_table_seg.h" +#include "catalog/pg_foreign_volume.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" @@ -1026,6 +1028,266 @@ CreateForeignServer(CreateForeignServerStmt *stmt) } +/* + * Create a foreign catalog + */ +ObjectAddress +CreateForeignCatalog(CreateForeignCatalogStmt *stmt) +{ + Relation rel; + Datum catalogoptions; + Datum values[Natts_pg_foreign_catalog]; + bool nulls[Natts_pg_foreign_catalog]; + HeapTuple tuple; + Oid catalogId; + Oid ownerId; + AclResult aclresult; + ObjectAddress myself; + ObjectAddress referenced; + ForeignServer *server; + + rel = table_open(ForeignCatalogRelationId, RowExclusiveLock); + + /* For now the owner cannot be specified on create. Use effective user ID. */ + ownerId = GetUserId(); + + /* + * Check that there is no other foreign catalog by this name. Catalog + * names are global (like server names): every reference syntax (DROP + * CATALOG, the CATALOG clause of CREATE ICEBERG TABLE, GUCs) identifies + * a catalog by bare name, so the name alone must be unique. If there is + * one, do nothing if IF NOT EXISTS was specified. + */ + catalogId = get_foreign_catalog_oid(stmt->catalogname, true); + if (OidIsValid(catalogId)) + { + if (stmt->if_not_exists) + { + /* + * If we are in an extension script, insist that the pre-existing + * object be a member of the extension, to avoid security risks. + */ + ObjectAddressSet(myself, ForeignCatalogRelationId, catalogId); + checkMembershipInCurrentExtension(&myself); + + /* OK to skip */ + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("foreign catalog \"%s\" already exists, skipping", + stmt->catalogname))); + table_close(rel, RowExclusiveLock); + return InvalidObjectAddress; + } + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("foreign catalog \"%s\" already exists", + stmt->catalogname))); + } + + /* + * Check that the server exists and that we have USAGE on it. + */ + server = GetForeignServerByName(stmt->servername, false); + + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, ownerId, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server->servername); + + /* + * Insert tuple into pg_foreign_catalog. + */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + catalogId = GetNewOidForForeignCatalog(rel, ForeignCatalogOidIndexId, + Anum_pg_foreign_catalog_oid, + stmt->catalogname); + values[Anum_pg_foreign_catalog_oid - 1] = ObjectIdGetDatum(catalogId); + values[Anum_pg_foreign_catalog_fcname - 1] = + DirectFunctionCall1(namein, CStringGetDatum(stmt->catalogname)); + values[Anum_pg_foreign_catalog_fcowner - 1] = ObjectIdGetDatum(ownerId); + values[Anum_pg_foreign_catalog_fcserver - 1] = ObjectIdGetDatum(server->serverid); + + /* Add catalog options; there is no validator for them */ + catalogoptions = transformGenericOptions(ForeignCatalogRelationId, + PointerGetDatum(NULL), + stmt->options, + InvalidOid); + + if (PointerIsValid(DatumGetPointer(catalogoptions))) + values[Anum_pg_foreign_catalog_fcoptions - 1] = catalogoptions; + else + nulls[Anum_pg_foreign_catalog_fcoptions - 1] = true; + + tuple = heap_form_tuple(rel->rd_att, values, nulls); + + CatalogTupleInsert(rel, tuple); + + heap_freetuple(tuple); + + /* record dependencies */ + myself.classId = ForeignCatalogRelationId; + myself.objectId = catalogId; + myself.objectSubId = 0; + + referenced.classId = ForeignServerRelationId; + referenced.objectId = server->serverid; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + recordDependencyOnOwner(ForeignCatalogRelationId, catalogId, ownerId); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + /* Post creation hook for new foreign catalog */ + InvokeObjectPostCreateHook(ForeignCatalogRelationId, catalogId, 0); + + if (Gp_role == GP_ROLE_DISPATCH) + { + CdbDispatchUtilityStatement((Node *) stmt, + DF_WITH_SNAPSHOT | DF_CANCEL_ON_ERROR | DF_NEED_TWO_PHASE, + GetAssignedOidsForDispatch(), + NULL); + } + + table_close(rel, RowExclusiveLock); + + return myself; +} + + +/* + * Create a foreign volume + */ +ObjectAddress +CreateForeignVolume(CreateForeignVolumeStmt *stmt) +{ + Relation rel; + Datum volumeoptions; + Datum values[Natts_pg_foreign_volume]; + bool nulls[Natts_pg_foreign_volume]; + HeapTuple tuple; + Oid volumeId; + Oid ownerId; + AclResult aclresult; + ObjectAddress myself; + ObjectAddress referenced; + ForeignServer *server; + + rel = table_open(ForeignVolumeRelationId, RowExclusiveLock); + + /* For now the owner cannot be specified on create. Use effective user ID. */ + ownerId = GetUserId(); + + /* + * Check that there is no other foreign volume by this name. Volume + * names are global (like server names): every reference syntax (DROP + * VOLUME, the VOLUME clause of CREATE ICEBERG TABLE, GUCs) identifies + * a volume by bare name, so the name alone must be unique. If there is + * one, do nothing if IF NOT EXISTS was specified. + */ + volumeId = get_foreign_volume_oid(stmt->volumename, true); + if (OidIsValid(volumeId)) + { + if (stmt->if_not_exists) + { + /* + * If we are in an extension script, insist that the pre-existing + * object be a member of the extension, to avoid security risks. + */ + ObjectAddressSet(myself, ForeignVolumeRelationId, volumeId); + checkMembershipInCurrentExtension(&myself); + + /* OK to skip */ + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("foreign volume \"%s\" already exists, skipping", + stmt->volumename))); + table_close(rel, RowExclusiveLock); + return InvalidObjectAddress; + } + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("foreign volume \"%s\" already exists", + stmt->volumename))); + } + + /* + * Check that the server exists and that we have USAGE on it. + */ + server = GetForeignServerByName(stmt->servername, false); + + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, ownerId, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server->servername); + + /* + * Insert tuple into pg_foreign_volume. + */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + volumeId = GetNewOidForForeignVolume(rel, ForeignVolumeOidIndexId, + Anum_pg_foreign_volume_oid, + stmt->volumename); + values[Anum_pg_foreign_volume_oid - 1] = ObjectIdGetDatum(volumeId); + values[Anum_pg_foreign_volume_fvname - 1] = + DirectFunctionCall1(namein, CStringGetDatum(stmt->volumename)); + values[Anum_pg_foreign_volume_fvowner - 1] = ObjectIdGetDatum(ownerId); + values[Anum_pg_foreign_volume_fvserver - 1] = ObjectIdGetDatum(server->serverid); + + /* Add volume options; there is no validator for them */ + volumeoptions = transformGenericOptions(ForeignVolumeRelationId, + PointerGetDatum(NULL), + stmt->options, + InvalidOid); + + if (PointerIsValid(DatumGetPointer(volumeoptions))) + values[Anum_pg_foreign_volume_fvoptions - 1] = volumeoptions; + else + nulls[Anum_pg_foreign_volume_fvoptions - 1] = true; + + tuple = heap_form_tuple(rel->rd_att, values, nulls); + + CatalogTupleInsert(rel, tuple); + + heap_freetuple(tuple); + + /* record dependencies */ + myself.classId = ForeignVolumeRelationId; + myself.objectId = volumeId; + myself.objectSubId = 0; + + referenced.classId = ForeignServerRelationId; + referenced.objectId = server->serverid; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + recordDependencyOnOwner(ForeignVolumeRelationId, volumeId, ownerId); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + /* Post creation hook for new foreign volume */ + InvokeObjectPostCreateHook(ForeignVolumeRelationId, volumeId, 0); + + if (Gp_role == GP_ROLE_DISPATCH) + { + CdbDispatchUtilityStatement((Node *) stmt, + DF_WITH_SNAPSHOT | DF_CANCEL_ON_ERROR | DF_NEED_TWO_PHASE, + GetAssignedOidsForDispatch(), + NULL); + } + + table_close(rel, RowExclusiveLock); + + return myself; +} + + /* * Alter foreign server */ diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index d4018ac0348..cd64f90f1d7 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -76,6 +76,8 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_EXTENSION: case OBJECT_FDW: case OBJECT_FOREIGN_SERVER: + case OBJECT_FOREIGN_CATALOG: + case OBJECT_FOREIGN_VOLUME: case OBJECT_INDEX: case OBJECT_OPCLASS: case OBJECT_OPERATOR: diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 0fadf562ee4..331ef9bc40b 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -15,10 +15,12 @@ #include "access/htup_details.h" #include "access/reloptions.h" #include "access/table.h" +#include "catalog/pg_foreign_catalog.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" #include "catalog/pg_foreign_table.h" #include "catalog/pg_foreign_table_seg.h" +#include "catalog/pg_foreign_volume.h" #include "catalog/pg_user_mapping.h" #include "cdb/cdbgang.h" #include "cdb/cdbutil.h" @@ -982,6 +984,98 @@ get_foreign_server_oid(const char *servername, bool missing_ok) return oid; } +/* + * get_foreign_catalog_oid - given a foreign catalog name, look up the OID + * + * If missing_ok is false, throw an error if name not found. If true, just + * return InvalidOid. + */ +Oid +get_foreign_catalog_oid(const char *catalogname, bool missing_ok) +{ + Oid oid; + + oid = GetSysCacheOid1(FOREIGNCATALOGNAME, + Anum_pg_foreign_catalog_oid, + CStringGetDatum(catalogname)); + if (!OidIsValid(oid) && !missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign catalog \"%s\" does not exist", + catalogname))); + + return oid; +} + +/* + * get_foreign_volume_oid - given a foreign volume name, look up the OID + * + * If missing_ok is false, throw an error if name not found. If true, just + * return InvalidOid. + */ +Oid +get_foreign_volume_oid(const char *volumename, bool missing_ok) +{ + Oid oid; + + oid = GetSysCacheOid1(FOREIGNVOLUMENAME, + Anum_pg_foreign_volume_oid, + CStringGetDatum(volumename)); + if (!OidIsValid(oid) && !missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign volume \"%s\" does not exist", + volumename))); + + return oid; +} + +/* + * GetForeignVolumeByName - look up a foreign volume by name + */ +ForeignVolume * +GetForeignVolumeByName(const char *volumename, bool missing_ok) +{ + HeapTuple tp; + Form_pg_foreign_volume fvform; + ForeignVolume *volume; + Datum datum; + bool isnull; + + tp = SearchSysCache1(FOREIGNVOLUMENAME, + PointerGetDatum(volumename)); + if (!HeapTupleIsValid(tp)) + { + if (!missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign volume \"%s\" does not exist", + volumename))); + return NULL; + } + + fvform = (Form_pg_foreign_volume) GETSTRUCT(tp); + + volume = (ForeignVolume *) palloc(sizeof(ForeignVolume)); + volume->volumeid = fvform->oid; + volume->serverid = fvform->fvserver; + volume->volumename = pstrdup(NameStr(fvform->fvname)); + + /* Extract the volume options */ + datum = SysCacheGetAttr(FOREIGNVOLUMENAME, + tp, + Anum_pg_foreign_volume_fvoptions, + &isnull); + if (isnull) + volume->options = NIL; + else + volume->options = untransformRelOptions(datum); + + ReleaseSysCache(tp); + + return volume; +} + /* * Get a copy of an existing local path for a given join relation. * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c2bf21f7898..ae79cb403de 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -10577,6 +10577,8 @@ drop_type_name: | PUBLICATION { $$ = OBJECT_PUBLICATION; } | SCHEMA { $$ = OBJECT_SCHEMA; } | SERVER { $$ = OBJECT_FOREIGN_SERVER; } + | CATALOG_P { $$ = OBJECT_FOREIGN_CATALOG; } + | VOLUME { $$ = OBJECT_FOREIGN_VOLUME; } | PROTOCOL { $$ = OBJECT_EXTPROTOCOL; } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 021c69ad031..f439fdc1582 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -210,6 +210,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateExtensionStmt: case T_CreateFdwStmt: case T_CreateForeignServerStmt: + case T_CreateForeignCatalogStmt: + case T_CreateForeignVolumeStmt: case T_CreateForeignTableStmt: case T_AddForeignSegStmt: case T_CreateFunctionStmt: @@ -2183,6 +2185,14 @@ ProcessUtilitySlow(ParseState *pstate, address = CreateForeignServer((CreateForeignServerStmt *) parsetree); break; + case T_CreateForeignCatalogStmt: + address = CreateForeignCatalog((CreateForeignCatalogStmt *) parsetree); + break; + + case T_CreateForeignVolumeStmt: + address = CreateForeignVolume((CreateForeignVolumeStmt *) parsetree); + break; + case T_AlterForeignServerStmt: address = AlterForeignServer((AlterForeignServerStmt *) parsetree); break; @@ -3255,6 +3265,14 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_CREATE_SERVER; break; + case T_CreateForeignCatalogStmt: + tag = CMDTAG_CREATE_FOREIGN_CATALOG; + break; + + case T_CreateForeignVolumeStmt: + tag = CMDTAG_CREATE_FOREIGN_VOLUME; + break; + case T_AlterForeignServerStmt: tag = CMDTAG_ALTER_SERVER; break; @@ -3421,6 +3439,12 @@ CreateCommandTag(Node *parsetree) case OBJECT_FOREIGN_SERVER: tag = CMDTAG_DROP_SERVER; break; + case OBJECT_FOREIGN_CATALOG: + tag = CMDTAG_DROP_FOREIGN_CATALOG; + break; + case OBJECT_FOREIGN_VOLUME: + tag = CMDTAG_DROP_FOREIGN_VOLUME; + break; case OBJECT_STORAGE_SERVER: tag = CMDTAG_DROP_STORAGE_SERVER; break; @@ -4199,6 +4223,8 @@ GetCommandLogLevel(Node *parsetree) case T_CreateFdwStmt: case T_AlterFdwStmt: case T_CreateForeignServerStmt: + case T_CreateForeignCatalogStmt: + case T_CreateForeignVolumeStmt: case T_AlterForeignServerStmt: case T_CreateStorageServerStmt: case T_AlterStorageServerStmt: diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index a8901a957eb..556c7536205 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -44,9 +44,11 @@ #include "catalog/pg_enum.h" #include "catalog/pg_event_trigger.h" #include "catalog/pg_extension.h" +#include "catalog/pg_foreign_catalog.h" #include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_foreign_server.h" #include "catalog/pg_foreign_table.h" +#include "catalog/pg_foreign_volume.h" #include "catalog/pg_language.h" #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" @@ -363,6 +365,18 @@ static const struct cachedesc cacheinfo[] = { KEY(Anum_pg_foreign_data_wrapper_oid), 2 }, + [FOREIGNCATALOGNAME] = { + ForeignCatalogRelationId, + ForeignCatalogNameIndexId, + KEY(Anum_pg_foreign_catalog_fcname), + 2 + }, + [FOREIGNCATALOGOID] = { + ForeignCatalogRelationId, + ForeignCatalogOidIndexId, + KEY(Anum_pg_foreign_catalog_oid), + 2 + }, [FOREIGNSERVERNAME] = { ForeignServerRelationId, ForeignServerNameIndexId, @@ -393,6 +407,18 @@ static const struct cachedesc cacheinfo[] = { KEY(Anum_pg_foreign_table_ftrelid), 4 }, + [FOREIGNVOLUMENAME] = { + ForeignVolumeRelationId, + ForeignVolumeNameIndexId, + KEY(Anum_pg_foreign_volume_fvname), + 2 + }, + [FOREIGNVOLUMEOID] = { + ForeignVolumeRelationId, + ForeignVolumeOidIndexId, + KEY(Anum_pg_foreign_volume_oid), + 2 + }, [GPPOLICYID] = { GpPolicyRelationId, GpPolicyLocalOidIndexId, diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 6a7ae2abea9..ac2ccb596b1 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -151,9 +151,11 @@ typedef enum ObjectClass OCLASS_EXTPROTOCOL, /* pg_extprotocol */ OCLASS_MATVIEW_AUX, /* gp_matview_aux */ OCLASS_TASK, /* pg_task */ + OCLASS_FOREIGN_CATALOG, /* pg_foreign_catalog */ + OCLASS_FOREIGN_VOLUME, /* pg_foreign_volume */ } ObjectClass; -#define LAST_OCLASS OCLASS_TASK +#define LAST_OCLASS OCLASS_FOREIGN_VOLUME /* flag bits for performDeletion/performMultipleDeletions: */ #define PERFORM_DELETION_INTERNAL 0x0001 /* internal action */ diff --git a/src/include/catalog/oid_dispatch.h b/src/include/catalog/oid_dispatch.h index 6ccd20ac4bd..9c0574faf38 100644 --- a/src/include/catalog/oid_dispatch.h +++ b/src/include/catalog/oid_dispatch.h @@ -65,6 +65,10 @@ extern Oid GetNewOidForForeignDataWrapper(Relation relation, Oid indexId, AttrNu char *fdwname); extern Oid GetNewOidForForeignServer(Relation relation, Oid indexId, AttrNumber oidcolumn, char *srvname); +extern Oid GetNewOidForForeignCatalog(Relation relation, Oid indexId, AttrNumber oidcolumn, + char *catname); +extern Oid GetNewOidForForeignVolume(Relation relation, Oid indexId, AttrNumber oidcolumn, + char *volumename); extern Oid GetNewOidForStorageServer(Relation relation, Oid indexId, AttrNumber oidcolumn, char *srvname); extern Oid GetNewOidForLanguage(Relation relation, Oid indexId, AttrNumber oidcolumn, diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index 7af15a37f52..72f3562a033 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -133,6 +133,8 @@ extern ObjectAddress CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt extern ObjectAddress AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt); extern ObjectAddress CreateForeignServer(CreateForeignServerStmt *stmt); extern ObjectAddress AlterForeignServer(AlterForeignServerStmt *stmt); +extern ObjectAddress CreateForeignCatalog(CreateForeignCatalogStmt *stmt); +extern ObjectAddress CreateForeignVolume(CreateForeignVolumeStmt *stmt); extern ObjectAddress CreateStorageServer(CreateStorageServerStmt *stmt); extern ObjectAddress AlterStorageServer(AlterStorageServerStmt *stmt); extern Oid RemoveStorageServer(DropStorageServerStmt *stmt); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index f95f0d331e7..417ac80d5fb 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -62,6 +62,14 @@ typedef struct ForeignTable int32 num_segments; /* the number of segments of the foreign table */ } ForeignTable; +typedef struct ForeignVolume +{ + Oid volumeid; /* volume Oid */ + Oid serverid; /* server Oid */ + char *volumename; /* name of the volume */ + List *options; /* fvoptions as DefElem list */ +} ForeignVolume; + /* Flags for GetForeignServerExtended */ #define FSV_MISSING_OK 0x01 @@ -84,11 +92,15 @@ extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *fdwname, bool missing_ok); extern ForeignTable *GetForeignTable(Oid relid); extern bool rel_is_external_table(Oid relid); +extern ForeignVolume *GetForeignVolumeByName(const char *volumename, + bool missing_ok); extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum); extern Oid get_foreign_data_wrapper_oid(const char *fdwname, bool missing_ok); extern Oid get_foreign_server_oid(const char *servername, bool missing_ok); +extern Oid get_foreign_catalog_oid(const char *catalogname, bool missing_ok); +extern Oid get_foreign_volume_oid(const char *volumename, bool missing_ok); extern Oid GetForeignServerSegByRelid(Oid tableOid); extern List *GetForeignServerSegsByRelId(Oid relid); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 956e90f115e..bac420b1d58 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2260,6 +2260,8 @@ typedef enum ObjectType OBJECT_EXTENSION, OBJECT_FDW, OBJECT_FOREIGN_SERVER, + OBJECT_FOREIGN_CATALOG, + OBJECT_FOREIGN_VOLUME, OBJECT_STORAGE_SERVER, OBJECT_FOREIGN_TABLE, OBJECT_FUNCTION, diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 21db8a4d19d..a1cc6615a7d 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -106,8 +106,10 @@ PG_CMDTAG(CMDTAG_CREATE_DYNAMIC_TABLE, "CREATE DYNAMIC TABLE", true, false, fals PG_CMDTAG(CMDTAG_CREATE_EVENT_TRIGGER, "CREATE EVENT TRIGGER", false, false, false) PG_CMDTAG(CMDTAG_CREATE_EXTENSION, "CREATE EXTENSION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_EXTERNAL, "CREATE EXTERNAL TABLE", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_FOREIGN_CATALOG, "CREATE FOREIGN CATALOG", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FOREIGN_DATA_WRAPPER, "CREATE FOREIGN DATA WRAPPER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FOREIGN_TABLE, "CREATE FOREIGN TABLE", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_FOREIGN_VOLUME, "CREATE FOREIGN VOLUME", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FUNCTION, "CREATE FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_INDEX, "CREATE INDEX", true, false, false) PG_CMDTAG(CMDTAG_CREATE_LANGUAGE, "CREATE LANGUAGE", true, false, false) @@ -176,8 +178,10 @@ PG_CMDTAG(CMDTAG_DROP_DOMAIN, "DROP DOMAIN", true, false, false) PG_CMDTAG(CMDTAG_DROP_DYNAMIC_TABLE, "DROP DYNAMIC TABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_EVENT_TRIGGER, "DROP EVENT TRIGGER", false, false, false) PG_CMDTAG(CMDTAG_DROP_EXTENSION, "DROP EXTENSION", true, false, false) +PG_CMDTAG(CMDTAG_DROP_FOREIGN_CATALOG, "DROP FOREIGN CATALOG", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_DATA_WRAPPER, "DROP FOREIGN DATA WRAPPER", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false) +PG_CMDTAG(CMDTAG_DROP_FOREIGN_VOLUME, "DROP FOREIGN VOLUME", true, false, false) PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false) PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false) diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index e790dfe2af5..5ed42d82b15 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -60,6 +60,8 @@ enum SysCacheIdentifier EVENTTRIGGEROID, EXTPROTOCOLOID, EXTPROTOCOLNAME, + FOREIGNCATALOGNAME, + FOREIGNCATALOGOID, FOREIGNDATAWRAPPERNAME, FOREIGNDATAWRAPPEROID, FOREIGNSERVERNAME, @@ -67,6 +69,8 @@ enum SysCacheIdentifier STORAGESERVERNAME, STORAGESERVEROID, FOREIGNTABLEREL, + FOREIGNVOLUMENAME, + FOREIGNVOLUMEOID, GPPOLICYID, AORELID, INDEXRELID, From b4704be776f814b46a5a59eb543df80fad32a864 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 13:04:41 +0800 Subject: [PATCH 05/11] commands: implement CREATE ICEBERG TABLE Make CREATE ICEBERG TABLE functional on top of the existing grammar, parse nodes and pg_lake_table catalog: * laketablecmds.c: CreateLakeTable() inserts the pg_lake_table entry after DefineRelation and records dependencies on the table's foreign catalog and volume; ValidateLakeTableOptions() runs the same resolution on the QD before DefineRelation so validation failures don't surface as QE-annotated errors; RemoveLakeTableEntry() cleans up on drop (hooked into heap_drop_with_catalog). * iceberg_default_catalog / iceberg_default_volume GUCs (synchronized to QEs) provide defaults when CREATE ICEBERG TABLE has no CATALOG or VOLUME clause; their check hooks verify the object exists. * The iceberg table access method is resolved strictly by name (get_table_am_oid) and is expected to be provided by a datalake extension; without it, CREATE ICEBERG TABLE fails up front with a hint. The kernel does not hardcode any extension name or AM OID. * Guard rails: reject the iceberg AM for every creation path other than CreateLakeTableStmt (CREATE TABLE ... USING iceberg, CTAS, matview, default_table_access_method, partition children), reject ALTER TABLE ... SET ACCESS METHOD to or from iceberg, and reject SET DISTRIBUTED BY on lake tables, which must stay DISTRIBUTED RANDOMLY. A relation created with the iceberg AM but without its pg_lake_table metadata would be unusable and undroppable. * utility.c dispatch (transformCreateStmt works on the embedded CreateStmt) and the CREATE LAKE TABLE command tag. --- src/backend/catalog/heap.c | 5 + src/backend/commands/Makefile | 1 + src/backend/commands/laketablecmds.c | 469 +++++++++++++++++++++++++++ src/backend/commands/tablecmds.c | 46 +++ src/backend/tcop/utility.c | 72 ++++ src/backend/utils/misc/guc_tables.c | 23 ++ src/include/commands/laketablecmds.h | 47 +++ src/include/tcop/cmdtaglist.h | 1 + src/include/utils/sync_guc_name.h | 2 + 9 files changed, 666 insertions(+) create mode 100644 src/backend/commands/laketablecmds.c create mode 100644 src/include/commands/laketablecmds.h diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 0b62b174afa..b6171061700 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -72,6 +72,7 @@ #include "catalog/storage.h" #include "catalog/storage_directory_table.h" #include "catalog/storage_xlog.h" +#include "commands/laketablecmds.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" #include "miscadmin.h" @@ -2322,6 +2323,10 @@ heap_drop_with_catalog(Oid relid) */ CheckTableForSerializableConflictIn(rel); + /* If this is a lake table, remove its pg_lake_table entry */ + if (RelationIsIcebergTable(rel)) + RemoveLakeTableEntry(relid); + /* * Delete pg_foreign_table tuple first. */ diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 3451b45d115..1a7d2d51db1 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -42,6 +42,7 @@ OBJS = \ foreigncmds.o \ functioncmds.o \ indexcmds.o \ + laketablecmds.o \ lockcmds.o \ matview.o \ opclasscmds.o \ diff --git a/src/backend/commands/laketablecmds.c b/src/backend/commands/laketablecmds.c new file mode 100644 index 00000000000..80771f0b1aa --- /dev/null +++ b/src/backend/commands/laketablecmds.c @@ -0,0 +1,469 @@ +/*------------------------------------------------------------------------- + * + * laketablecmds.c + * lake table creation/manipulation commands + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/backend/commands/laketablecmds.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/htup_details.h" +#include "access/reloptions.h" +#include "access/table.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_foreign_catalog.h" +#include "catalog/pg_foreign_volume.h" +#include "catalog/pg_lake_table.h" +#include "commands/defrem.h" +#include "commands/laketablecmds.h" +#include "foreign/foreign.h" +#include "miscadmin.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/rel.h" + +/* GUC variables for default Iceberg catalog and volume */ +char *iceberg_default_catalog = NULL; +char *iceberg_default_volume = NULL; + +/* + * check_iceberg_default_catalog: validate new iceberg_default_catalog GUC value + */ +bool +check_iceberg_default_catalog(char **newval, void **extra, GucSource source) +{ + /* + * If we aren't inside a transaction, or connected to a database, we + * cannot do the catalog accesses necessary to verify the name. Must + * accept the value on faith. + */ + if (IsTransactionState() && MyDatabaseId != InvalidOid) + { + if (**newval != '\0') + { + Oid catalog_oid = get_foreign_catalog_oid(*newval, true); + + if (!OidIsValid(catalog_oid)) + { + /* + * When source == PGC_S_TEST, don't throw a hard error for a + * nonexistent catalog, only a NOTICE. See comments in guc.h. + */ + if (source == PGC_S_TEST) + { + ereport(NOTICE, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign catalog \"%s\" does not exist", + *newval))); + } + else + { + GUC_check_errdetail("Foreign catalog \"%s\" does not exist.", + *newval); + return false; + } + } + } + } + + return true; +} + +/* + * check_iceberg_default_volume: validate new iceberg_default_volume GUC value + */ +bool +check_iceberg_default_volume(char **newval, void **extra, GucSource source) +{ + /* + * If we aren't inside a transaction, or connected to a database, we + * cannot do the catalog accesses necessary to verify the name. Must + * accept the value on faith. + */ + if (IsTransactionState() && MyDatabaseId != InvalidOid) + { + if (**newval != '\0') + { + Oid volume_oid = get_foreign_volume_oid(*newval, true); + + if (!OidIsValid(volume_oid)) + { + /* + * When source == PGC_S_TEST, don't throw a hard error for a + * nonexistent volume, only a NOTICE. See comments in guc.h. + */ + if (source == PGC_S_TEST) + { + ereport(NOTICE, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("foreign volume \"%s\" does not exist", + *newval))); + } + else + { + GUC_check_errdetail("Foreign volume \"%s\" does not exist.", + *newval); + return false; + } + } + } + } + + return true; +} + +/* + * GetDefaultIcebergCatalog -- get the name of the current default Iceberg catalog + * + * Returns NULL if no default catalog is set. + * This function hides the iceberg_default_catalog GUC variable. + */ +const char * +GetDefaultIcebergCatalog(void) +{ + if (iceberg_default_catalog == NULL || iceberg_default_catalog[0] == '\0') + return NULL; + + /* + * Verify that the catalog still exists. We don't cache this because + * the catalog could be dropped after the GUC was set. + */ + if (!OidIsValid(get_foreign_catalog_oid(iceberg_default_catalog, true))) + return NULL; + + return iceberg_default_catalog; +} + +/* + * GetDefaultIcebergVolume -- get the name of the current default Iceberg volume + * + * Returns NULL if no default volume is set. + * This function hides the iceberg_default_volume GUC variable. + */ +const char * +GetDefaultIcebergVolume(void) +{ + if (iceberg_default_volume == NULL || iceberg_default_volume[0] == '\0') + return NULL; + + /* + * Verify that the volume still exists. We don't cache this because + * the volume could be dropped after the GUC was set. + */ + if (!OidIsValid(get_foreign_volume_oid(iceberg_default_volume, true))) + return NULL; + + return iceberg_default_volume; +} + +/* + * GetIcebergTableAmOid + * + * Look up the OID of the iceberg table access method, which is provided by + * a datalake extension rather than the kernel. Returns InvalidOid if the + * access method is not installed and missing_ok is true. + */ +Oid +GetIcebergTableAmOid(bool missing_ok) +{ + return get_table_am_oid(ICEBERG_TABLE_AM_NAME, missing_ok); +} + +/* + * RelationIsIcebergTable + * + * True iff the relation uses the iceberg table access method. Resolved by + * access method name so the kernel does not depend on any particular + * extension's OID assignments. + */ +bool +RelationIsIcebergTable(Relation rel) +{ + Oid iceberg_amoid; + + if (!OidIsValid(rel->rd_rel->relam)) + return false; + + iceberg_amoid = GetIcebergTableAmOid(true); + + return OidIsValid(iceberg_amoid) && rel->rd_rel->relam == iceberg_amoid; +} + +/* + * Validate table type + */ +static void +validate_table_type(const char *table_type) +{ + if (!table_type) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table type cannot be NULL"))); + + if (strcmp(table_type, "ICEBERG") != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unsupported table type \"%s\"", table_type), + errhint("The only supported table type is ICEBERG."))); +} + +/* + * Validate foreign catalog exists + */ +static Oid +validate_foreign_catalog(const char *catalog_name) +{ + if (!catalog_name || catalog_name[0] == '\0') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("no foreign catalog specified"), + errhint("Specify CATALOG in CREATE ICEBERG TABLE or set iceberg_default_catalog."))); + + return get_foreign_catalog_oid(catalog_name, false); +} + +/* + * Validate foreign volume exists + */ +static Oid +validate_foreign_volume(const char *volume_name) +{ + if (!volume_name || volume_name[0] == '\0') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("no foreign volume specified"), + errhint("Specify VOLUME in CREATE ICEBERG TABLE or set iceberg_default_volume."))); + + return get_foreign_volume_oid(volume_name, false); +} + +/* + * ResolveLakeTableOptions + * + * Resolve and validate the table type, catalog and volume of a + * CreateLakeTableStmt, returning the catalog/volume OIDs. + * + * Also exposed (via ValidateLakeTableOptions) so ProcessUtilitySlow can run + * the validation on the QD before DefineRelation: DefineRelation dispatches + * the statement to the QEs, so a validation failure raised only inside + * CreateLakeTable() would surface as a confusing QE-annotated error. + */ +static void +ResolveLakeTableOptions(CreateLakeTableStmt *stmt, + Oid *catalog_oid_out, Oid *volume_oid_out) +{ + const char *catalog_name; + const char *volume_name; + + /* + * Lake tables are unusable without an extension providing the iceberg + * table access method; check it first so the install hint takes + * precedence over catalog/volume resolution errors. + */ + if (!OidIsValid(GetIcebergTableAmOid(true))) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("table access method \"%s\" does not exist", + ICEBERG_TABLE_AM_NAME), + errhint("CREATE ICEBERG TABLE requires an extension that provides the \"%s\" table access method.", + ICEBERG_TABLE_AM_NAME))); + + /* + * The grammar accepts a USING clause, but an iceberg table must use the + * iceberg access method: a lake table created with another AM would get + * pg_lake_table metadata without lake-table semantics (and DROP TABLE, + * which detects lake tables by their AM, would leave that metadata + * behind as an orphaned row). + */ + if (stmt->base.accessMethod != NULL && + strcmp(stmt->base.accessMethod, ICEBERG_TABLE_AM_NAME) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("access method \"%s\" is not supported for iceberg tables", + stmt->base.accessMethod), + errhint("Omit the USING clause; CREATE ICEBERG TABLE always uses the \"%s\" access method.", + ICEBERG_TABLE_AM_NAME))); + + /* Validate table type */ + validate_table_type(stmt->table_type); + + /* + * Determine catalog name: use explicit value if provided, otherwise + * fall back to the iceberg_default_catalog GUC. When the GUC is set + * but its catalog has been dropped, say so instead of the generic + * "no foreign catalog specified". + */ + catalog_name = stmt->foreign_catalog; + if (catalog_name == NULL || catalog_name[0] == '\0') + { + catalog_name = GetDefaultIcebergCatalog(); + if (catalog_name == NULL && + iceberg_default_catalog != NULL && iceberg_default_catalog[0] != '\0') + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("default iceberg catalog \"%s\" does not exist", + iceberg_default_catalog), + errhint("Set iceberg_default_catalog to an existing foreign catalog."))); + } + + /* + * Determine volume name: use explicit value if provided, otherwise + * fall back to the iceberg_default_volume GUC. + */ + volume_name = stmt->foreign_volume; + if (volume_name == NULL || volume_name[0] == '\0') + { + volume_name = GetDefaultIcebergVolume(); + if (volume_name == NULL && + iceberg_default_volume != NULL && iceberg_default_volume[0] != '\0') + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("default iceberg volume \"%s\" does not exist", + iceberg_default_volume), + errhint("Set iceberg_default_volume to an existing foreign volume."))); + } + + *catalog_oid_out = validate_foreign_catalog(catalog_name); + + /* + * A volume is required for every lake table, even when the catalog + * vends the table's physical location: the QEs still read and write + * the data files through the volume's storage endpoint and + * credentials. Without this check the missing volume only surfaces + * later, deep in the access method's create path. + */ + *volume_oid_out = validate_foreign_volume(volume_name); +} + +/* + * ValidateLakeTableOptions + * + * QD-side pre-DefineRelation validation wrapper; see ResolveLakeTableOptions. + */ +void +ValidateLakeTableOptions(CreateLakeTableStmt *stmt) +{ + Oid catalog_oid; + Oid volume_oid; + + ResolveLakeTableOptions(stmt, &catalog_oid, &volume_oid); +} + +/* + * CreateLakeTable + * + * Create a lake table entry in pg_lake_table after the base table has been + * created. + */ +void +CreateLakeTable(CreateLakeTableStmt *stmt, Oid relId) +{ + Relation lake_rel; + Datum values[Natts_pg_lake_table]; + bool nulls[Natts_pg_lake_table]; + HeapTuple tuple; + Oid catalog_oid; + Oid volume_oid; + ObjectAddress myself; + ObjectAddress referenced; + + ResolveLakeTableOptions(stmt, &catalog_oid, &volume_oid); + + /* + * Make the just-created base relation (from DefineRelation) visible to + * this command before we record dependencies on it. + */ + CommandCounterIncrement(); + + lake_rel = table_open(LakeTableRelationId, RowExclusiveLock); + + /* + * Insert tuple into pg_lake_table. + */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + values[Anum_pg_lake_table_ltrelid - 1] = ObjectIdGetDatum(relId); + values[Anum_pg_lake_table_ltforeign_catalog - 1] = ObjectIdGetDatum(catalog_oid); + values[Anum_pg_lake_table_ltforeign_volume - 1] = ObjectIdGetDatum(volume_oid); + values[Anum_pg_lake_table_lttable_type - 1] = CStringGetTextDatum(stmt->table_type); + + if (stmt->options) + { + Datum options_datum; + + /* Build standard text[] reloptions from DefElem list */ + options_datum = transformRelOptions((Datum) 0, stmt->options, + NULL, NULL, false, false); + + if (options_datum != (Datum) 0) + values[Anum_pg_lake_table_ltoptions - 1] = options_datum; + else + nulls[Anum_pg_lake_table_ltoptions - 1] = true; + } + else + { + nulls[Anum_pg_lake_table_ltoptions - 1] = true; + } + + tuple = heap_form_tuple(lake_rel->rd_att, values, nulls); + + CatalogTupleInsert(lake_rel, tuple); + + /* Record dependencies on the foreign catalog and volume */ + myself.classId = RelationRelationId; + myself.objectId = relId; + myself.objectSubId = 0; + + referenced.classId = ForeignCatalogRelationId; + referenced.objectId = catalog_oid; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + referenced.classId = ForeignVolumeRelationId; + referenced.objectId = volume_oid; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + heap_freetuple(tuple); + table_close(lake_rel, RowExclusiveLock); + + CommandCounterIncrement(); + InvokeObjectPostCreateHook(LakeTableRelationId, relId, 0); +} + +/* + * RemoveLakeTableEntry + * + * Remove the pg_lake_table entry for the given relation. + */ +void +RemoveLakeTableEntry(Oid relid) +{ + Relation ltRel; + HeapTuple tup; + ScanKeyData skey; + SysScanDesc scan; + + ltRel = table_open(LakeTableRelationId, RowExclusiveLock); + ScanKeyInit(&skey, + Anum_pg_lake_table_ltrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + scan = systable_beginscan(ltRel, LakeTableRelidIndexId, true, NULL, 1, &skey); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + CatalogTupleDelete(ltRel, &tup->t_self); + systable_endscan(scan); + table_close(ltRel, RowExclusiveLock); +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index b8b69336b89..3786ca44e1a 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -69,6 +69,7 @@ #include "commands/comment.h" #include "commands/createas.h" #include "commands/defrem.h" +#include "commands/laketablecmds.h" #include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/policy.h" @@ -938,6 +939,24 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, amHandlerOid = get_table_am_handler_oid(accessMethod, false); } + /* + * Lake tables must be created through CREATE ICEBERG TABLE, which also + * creates the pg_lake_table catalog entries the iceberg access method + * relies on. A relation created with the iceberg AM through any other + * path (CREATE TABLE ... USING iceberg, CTAS, matview, + * default_table_access_method, partition child) would be unusable and + * undroppable, so reject it up front. CreateLakeTableStmt embeds + * CreateStmt as its first member, so nodeTag() distinguishes the paths. + */ + if (OidIsValid(accessMethodId) && + accessMethodId == GetIcebergTableAmOid(true) && + nodeTag(stmt) != T_CreateLakeTableStmt) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot create table \"%s\" with access method \"%s\"", + stmt->relation->relname, ICEBERG_TABLE_AM_NAME), + errhint("Use CREATE ICEBERG TABLE instead."))); + /* * GPDB: for partitioned tables, inherit reloptions from the parent. * Note this is applicable only if the parent has the same AM as the child. @@ -17059,6 +17078,7 @@ static void ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname) { Oid amoid; + Oid iceberg_amoid; /* Check that the table access method exists */ amoid = get_table_am_oid(amname, false); @@ -17066,6 +17086,24 @@ ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname) if (rel->rd_rel->relam == amoid) return; + /* + * The iceberg AM relies on catalog entries that only the CREATE/DROP + * ICEBERG TABLE paths manage, so a table cannot be converted to or from + * it with SET ACCESS METHOD. + */ + iceberg_amoid = GetIcebergTableAmOid(true); + if (OidIsValid(iceberg_amoid) && amoid == iceberg_amoid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change access method of table \"%s\" to \"%s\"", + RelationGetRelationName(rel), ICEBERG_TABLE_AM_NAME), + errhint("Use CREATE ICEBERG TABLE to create an iceberg table."))); + if (OidIsValid(iceberg_amoid) && rel->rd_rel->relam == iceberg_amoid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change access method of lake table \"%s\"", + RelationGetRelationName(rel)))); + /* Save info for Phase 3 to do the real work */ tab->rewrite |= AT_REWRITE_ACCESS_METHOD; tab->newAccessMethod = amoid; @@ -19759,6 +19797,14 @@ ATExecSetDistributedBy(Relation rel, Node *node, AlterTableCmd *cmd) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("SET DISTRIBUTED REPLICATED is not supported for external table"))); } + + /* Lake tables must remain DISTRIBUTED RANDOMLY */ + if (RelationIsIcebergTable(rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change distribution policy of lake table \"%s\"", + RelationGetRelationName(rel)), + errhint("Lake tables must use DISTRIBUTED RANDOMLY because data is stored on object storage."))); } if (Gp_role == GP_ROLE_DISPATCH) diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index f439fdc1582..300c018ba8a 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -38,6 +38,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/dirtablecmds.h" +#include "commands/laketablecmds.h" #include "commands/discard.h" #include "commands/event_trigger.h" #include "commands/explain.h" @@ -264,6 +265,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_AlterResourceGroupStmt: case T_AlterTagStmt: case T_CreateDirectoryTableStmt: + case T_CreateLakeTableStmt: case T_AlterDirectoryTableStmt: case T_DropDirectoryTableStmt: case T_CreateProfileStmt: @@ -1450,6 +1452,7 @@ ProcessUtilitySlow(ParseState *pstate, case T_CreateStmt: case T_CreateForeignTableStmt: case T_CreateDirectoryTableStmt: + case T_CreateLakeTableStmt: { List *stmts; RangeVar *table_rv = NULL; @@ -1657,6 +1660,70 @@ ProcessUtilitySlow(ParseState *pstate, secondaryObject, stmt); } + else if (IsA(stmt, CreateLakeTableStmt)) + { + CreateLakeTableStmt *cstmt = (CreateLakeTableStmt *) stmt; + Datum toast_options; + static char *validnsps[] = HEAP_RELOPT_NAMESPACES; + + /* Remember transformed RangeVar for LIKE */ + table_rv = cstmt->base.relation; + + /* + * Validate catalog/volume resolution up front: + * the statement is dispatched to the QEs, so a + * failure raised only later inside + * CreateLakeTable() would surface as a confusing + * QE-annotated error. + */ + if (Gp_role == GP_ROLE_DISPATCH) + ValidateLakeTableOptions(cstmt); + + /* + * Create the table itself. Dispatch manually + * below (like the plain CreateStmt path above) + * so that the TOAST table exists before the + * statement is sent and its OID is included in + * the dispatched OID list. + */ + address = DefineRelation(&cstmt->base, + RELKIND_RELATION, + InvalidOid, NULL, + queryString, + false, + true, + NULL); + /* Create the lake table metadata entry */ + CreateLakeTable(cstmt, address.objectId); + EventTriggerCollectSimpleCommand(address, + secondaryObject, + stmt); + + /* + * Lake tables are backed by a real table access + * method, so let NewRelationCreateToastTable + * decide whether a secondary relation is needed, + * just like plain CREATE TABLE. + */ + CommandCounterIncrement(); + + toast_options = transformRelOptions((Datum) 0, + cstmt->base.options, + "toast", + validnsps, + true, + false); + NewRelationCreateToastTable(address.objectId, + toast_options); + + if (Gp_role == GP_ROLE_DISPATCH && ENABLE_DISPATCH()) + CdbDispatchUtilityStatement((Node *) stmt, + DF_CANCEL_ON_ERROR | + DF_NEED_TWO_PHASE | + DF_WITH_SNAPSHOT, + GetAssignedOidsForDispatch(), + NULL); + } else if (IsA(stmt, TableLikeClause)) { /* @@ -3329,6 +3396,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_CREATE_DIRECTORY_TABLE; break; + case T_CreateLakeTableStmt: + tag = CMDTAG_CREATE_LAKE_TABLE; + break; + case T_AlterDirectoryTableStmt: tag = CMDTAG_ALTER_DIRECTORY_TABLE; break; @@ -4237,6 +4308,7 @@ GetCommandLogLevel(Node *parsetree) case T_DropStorageUserMappingStmt: case T_ImportForeignSchemaStmt: case T_CreateDirectoryTableStmt: + case T_CreateLakeTableStmt: case T_AlterDirectoryTableStmt: case T_DropDirectoryTableStmt: lev = LOGSTMT_DDL; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 1ff92994527..c4498a09fc4 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -39,6 +39,7 @@ #include "catalog/storage_directory_table.h" #include "commands/async.h" #include "commands/tablespace.h" +#include "commands/laketablecmds.h" #include "commands/trigger.h" #include "commands/user.h" #include "commands/vacuum.h" @@ -4119,6 +4120,28 @@ struct config_string ConfigureNamesString[] = check_temp_tablespaces, assign_temp_tablespaces, NULL }, + { + {"iceberg_default_catalog", PGC_USERSET, CLIENT_CONN_STATEMENT, + gettext_noop("Sets the default foreign catalog to create Iceberg tables in."), + gettext_noop("An empty string means no default catalog."), + GUC_IS_NAME + }, + &iceberg_default_catalog, + "", + check_iceberg_default_catalog, NULL, NULL + }, + + { + {"iceberg_default_volume", PGC_USERSET, CLIENT_CONN_STATEMENT, + gettext_noop("Sets the default foreign volume to create Iceberg tables in."), + gettext_noop("An empty string means no default volume."), + GUC_IS_NAME + }, + &iceberg_default_volume, + "", + check_iceberg_default_volume, NULL, NULL + }, + { {"createrole_self_grant", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets whether a CREATEROLE user automatically grants " diff --git a/src/include/commands/laketablecmds.h b/src/include/commands/laketablecmds.h new file mode 100644 index 00000000000..05564a79a98 --- /dev/null +++ b/src/include/commands/laketablecmds.h @@ -0,0 +1,47 @@ +/*------------------------------------------------------------------------- + * + * laketablecmds.h + * prototypes for laketablecmds.c. + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/laketablecmds.h + * + *------------------------------------------------------------------------- + */ +#ifndef LAKETABLECMDS_H +#define LAKETABLECMDS_H + +#include "catalog/pg_lake_table.h" +#include "nodes/parsenodes.h" +#include "utils/guc.h" +#include "utils/rel.h" + +/* + * Name of the table access method lake tables are created with. The + * kernel only provides the DDL scaffolding; the access method itself is + * provided by a datalake extension. + */ +#define ICEBERG_TABLE_AM_NAME "iceberg" + +/* GUC variables */ +extern char *iceberg_default_catalog; +extern char *iceberg_default_volume; + +/* GUC check hooks */ +extern bool check_iceberg_default_catalog(char **newval, void **extra, GucSource source); +extern bool check_iceberg_default_volume(char **newval, void **extra, GucSource source); + +/* Functions to get default values */ +extern const char *GetDefaultIcebergCatalog(void); +extern const char *GetDefaultIcebergVolume(void); + +/* Lake table management */ +extern Oid GetIcebergTableAmOid(bool missing_ok); +extern bool RelationIsIcebergTable(Relation rel); +extern void ValidateLakeTableOptions(CreateLakeTableStmt *stmt); +extern void CreateLakeTable(CreateLakeTableStmt *stmt, Oid relId); +extern void RemoveLakeTableEntry(Oid relid); + +#endif /* LAKETABLECMDS_H */ diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index a1cc6615a7d..fec05c924ad 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -112,6 +112,7 @@ PG_CMDTAG(CMDTAG_CREATE_FOREIGN_TABLE, "CREATE FOREIGN TABLE", true, false, fals PG_CMDTAG(CMDTAG_CREATE_FOREIGN_VOLUME, "CREATE FOREIGN VOLUME", true, false, false) PG_CMDTAG(CMDTAG_CREATE_FUNCTION, "CREATE FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_INDEX, "CREATE INDEX", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_LAKE_TABLE, "CREATE LAKE TABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_LANGUAGE, "CREATE LANGUAGE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_MATERIALIZED_VIEW, "CREATE MATERIALIZED VIEW", true, false, false) PG_CMDTAG(CMDTAG_CREATE_OPERATOR, "CREATE OPERATOR", true, false, false) diff --git a/src/include/utils/sync_guc_name.h b/src/include/utils/sync_guc_name.h index dfb6e946bac..6c829ace83a 100644 --- a/src/include/utils/sync_guc_name.h +++ b/src/include/utils/sync_guc_name.h @@ -122,6 +122,8 @@ "gp_workfile_limit_per_query", "gp_write_shared_snapshot", "hash_mem_multiplier", + "iceberg_default_catalog", + "iceberg_default_volume", "ignore_system_indexes", "ignore_checksum_failure", "IntervalStyle", From a81bd6b626348b8b1bea760d51a6c8d6342306a0 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 13:22:20 +0800 Subject: [PATCH 06/11] bin: pg_dump skip and psql tab completion for lake-table DDL pg_dump cannot reproduce lake tables (their data lives in external object storage managed through the iceberg access method's foreign catalog and volume), so skip them with a warning, matching how other unsupported access methods are handled. The access method is matched by name, not by a hardcoded OID. psql tab completion learns CREATE FOREIGN CATALOG/VOLUME ... SERVER ... OPTIONS, CREATE ICEBERG TABLE, DROP CATALOG/VOLUME with CASCADE/ RESTRICT, and completes foreign catalog and volume names after the CATALOG and VOLUME keywords. --- src/bin/pg_dump/pg_dump.c | 13 ++++++++++ src/bin/psql/tab-complete.c | 51 +++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 7604417e262..1f091d26f83 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -2134,6 +2134,19 @@ selectDumpableTable(TableInfo *tbinfo, Archive *fout) pg_log_warning("unsupport am pax yet, current relation \"%s\" will be ignore", tbinfo->dobj.name); } + + /* + * Lake tables cannot be reproduced by pg_dump: their data lives in + * external object storage managed through the iceberg access method's + * foreign catalog and volume. Skip them. + */ + if (tbinfo->amname && strcmp(tbinfo->amname, "iceberg") == 0) + { + tbinfo->dobj.dump = DUMP_COMPONENT_NONE; + + pg_log_warning("iceberg table \"%s\" is not supported by pg_dump and will be ignored", + tbinfo->dobj.name); + } } /* diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 81a81578c0b..48903015c2f 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1077,6 +1077,16 @@ static const SchemaQuery Query_for_trigger_of_table = { " FROM pg_catalog.pg_foreign_server "\ " WHERE srvname LIKE '%s'" +#define Query_for_list_of_foreign_catalogs \ +" SELECT fcname "\ +" FROM pg_catalog.pg_foreign_catalog "\ +" WHERE fcname LIKE '%s'" + +#define Query_for_list_of_foreign_volumes \ +" SELECT fvname "\ +" FROM pg_catalog.pg_foreign_volume "\ +" WHERE fvname LIKE '%s'" + #define Query_for_list_of_user_mappings \ " SELECT usename "\ " FROM pg_catalog.pg_user_mappings "\ @@ -1257,6 +1267,7 @@ static const pgsql_thing_t words_after_create[] = { {"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates}, {"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so * skip it */ + {"CATALOG", Query_for_list_of_foreign_catalogs, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER}, {"COLLATION", NULL, NULL, &Query_for_list_of_collations}, /* @@ -1274,10 +1285,13 @@ static const pgsql_thing_t words_after_create[] = { {"EVENT TRIGGER", NULL, NULL, NULL}, {"EXTENSION", Query_for_list_of_extensions}, {"EXTERNAL TABLE", NULL, NULL, NULL}, + {"FOREIGN CATALOG", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, {"FOREIGN DATA WRAPPER", NULL, NULL, NULL}, {"FOREIGN TABLE", NULL, NULL, NULL}, + {"FOREIGN VOLUME", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, {"FUNCTION", NULL, NULL, Query_for_list_of_functions}, {"GROUP", Query_for_list_of_roles}, + {"ICEBERG TABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, {"INCREMENTAL MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews, NULL, THING_NO_DROP | THING_NO_ALTER}, {"INDEX", NULL, NULL, &Query_for_list_of_indexes}, {"LANGUAGE", Query_for_list_of_languages}, @@ -1324,6 +1338,7 @@ static const pgsql_thing_t words_after_create[] = { {"USER MAPPING FOR", NULL, NULL, NULL}, {"STORAGE USER MAPPING FOR", NULL, NULL, NULL}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, + {"VOLUME", Query_for_list_of_foreign_volumes, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER}, {"WAREHOUSE", NULL}, {NULL} /* end of list */ }; @@ -3042,7 +3057,27 @@ psql_completion(const char *text, int start, int end) /* CREATE FOREIGN */ else if (Matches("CREATE", "FOREIGN")) - COMPLETE_WITH("DATA WRAPPER", "TABLE"); + COMPLETE_WITH("CATALOG", "DATA WRAPPER", "TABLE", "VOLUME"); + + /* CREATE FOREIGN CATALOG */ + else if (Matches("CREATE", "FOREIGN", "CATALOG", MatchAny)) + COMPLETE_WITH("SERVER"); + else if (Matches("CREATE", "FOREIGN", "CATALOG", MatchAny, "SERVER")) + COMPLETE_WITH_QUERY(Query_for_list_of_servers); + else if (Matches("CREATE", "FOREIGN", "CATALOG", MatchAny, "SERVER", MatchAny)) + COMPLETE_WITH("OPTIONS"); + + /* CREATE FOREIGN VOLUME */ + else if (Matches("CREATE", "FOREIGN", "VOLUME", MatchAny)) + COMPLETE_WITH("SERVER"); + else if (Matches("CREATE", "FOREIGN", "VOLUME", MatchAny, "SERVER")) + COMPLETE_WITH_QUERY(Query_for_list_of_servers); + else if (Matches("CREATE", "FOREIGN", "VOLUME", MatchAny, "SERVER", MatchAny)) + COMPLETE_WITH("OPTIONS"); + + /* CREATE ICEBERG */ + else if (Matches("CREATE", "ICEBERG")) + COMPLETE_WITH("TABLE"); /* CREATE FOREIGN DATA WRAPPER */ else if (Matches("CREATE", "FOREIGN", "DATA", "WRAPPER", MatchAny)) @@ -3801,7 +3836,7 @@ psql_completion(const char *text, int start, int end) /* DROP */ /* Complete DROP object with CASCADE / RESTRICT */ else if (Matches("DROP", - "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW", + "CATALOG|COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW|VOLUME", MatchAny) || Matches("DROP", "ACCESS", "METHOD", MatchAny) || (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) && @@ -4040,6 +4075,18 @@ psql_completion(const char *text, int start, int end) else if (TailMatches("FOREIGN", "SERVER")) COMPLETE_WITH_QUERY(Query_for_list_of_servers); +/* CATALOG, e.g. the CATALOG clause of CREATE ICEBERG TABLE */ + else if (TailMatches("CATALOG") && + !TailMatches("CREATE", MatchAny, MatchAny) && + !TailMatches("FOREIGN", MatchAny)) + COMPLETE_WITH_QUERY(Query_for_list_of_foreign_catalogs); + +/* VOLUME, e.g. the VOLUME clause of CREATE ICEBERG TABLE */ + else if (TailMatches("VOLUME") && + !TailMatches("CREATE", MatchAny, MatchAny) && + !TailMatches("FOREIGN", MatchAny)) + COMPLETE_WITH_QUERY(Query_for_list_of_foreign_volumes); + /* STORAGE SERVER */ else if (TailMatches("ALTER", "STORAGE", "SERVER")) COMPLETE_WITH_QUERY(Query_for_list_of_storage_servers); From 73172e8933a70826d4eb66bd436487786364d3c0 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Thu, 2 Jul 2026 16:08:25 +0800 Subject: [PATCH 07/11] tests: add lake_table regression test, refresh catalog expecteds Add a lake_table test to the greenplum_schedule covering the new DDL end to end: CREATE/DROP FOREIGN CATALOG and FOREIGN VOLUME (duplicates, IF NOT EXISTS/IF EXISTS, missing server), segment dispatch, object descriptions, CREATE ICEBERG TABLE with explicit CATALOG/VOLUME clauses and via the iceberg_default_catalog/volume GUCs, the forced random distribution policy, every iceberg-AM misuse guard, ownership checks, and dependency behavior (RESTRICT errors, CASCADE, pg_lake_table cleanup on drop). The iceberg access method is simulated with a heap-backed CREATE ACCESS METHOD, since the kernel resolves it by name and the real AM comes from a datalake extension. Refresh expected output of tests that enumerate system catalogs for the three new ones: misc_sanity (pg_lake_table's toast-less varlena columns), sanity_check (catalog list), and oidjoins (BKI_LOOKUP references, including pg_lake_table.ltforeign_catalog pointing at pg_foreign_catalog). --- .../src/test/regress/expected/oidjoins.out | 7 + .../test/regress/expected/sanity_check.out | 3 + src/test/regress/expected/lake_table.out | 288 ++++++++++++++++++ src/test/regress/expected/oidjoins.out | 7 + src/test/regress/expected/sanity_check.out | 3 + src/test/regress/greenplum_schedule | 3 + src/test/regress/sql/lake_table.sql | 142 +++++++++ .../singlenode_regress/expected/oidjoins.out | 7 + .../expected/sanity_check.out | 3 + 9 files changed, 463 insertions(+) create mode 100644 src/test/regress/expected/lake_table.out create mode 100644 src/test/regress/sql/lake_table.sql diff --git a/contrib/pax_storage/src/test/regress/expected/oidjoins.out b/contrib/pax_storage/src/test/regress/expected/oidjoins.out index 19094e111dc..6dc91c68aec 100644 --- a/contrib/pax_storage/src/test/regress/expected/oidjoins.out +++ b/contrib/pax_storage/src/test/regress/expected/oidjoins.out @@ -235,6 +235,10 @@ NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} +NOTICE: checking pg_foreign_catalog {fcowner} => pg_authid {oid} +NOTICE: checking pg_foreign_catalog {fcserver} => pg_foreign_server {oid} +NOTICE: checking pg_foreign_volume {fvowner} => pg_authid {oid} +NOTICE: checking pg_foreign_volume {fvserver} => pg_foreign_server {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} NOTICE: checking pg_user_mapping {umserver} => pg_foreign_server {oid} NOTICE: checking pg_compression {compconstructor} => pg_proc {oid} @@ -247,6 +251,9 @@ NOTICE: checking pg_foreign_table {ftrelid} => pg_class {oid} NOTICE: checking pg_foreign_table {ftserver} => pg_foreign_server {oid} NOTICE: checking pg_foreign_table_seg {ftsrelid} => pg_class {oid} NOTICE: checking pg_foreign_table_seg {ftsserver} => pg_foreign_server {oid} +NOTICE: checking pg_lake_table {ltrelid} => pg_class {oid} +NOTICE: checking pg_lake_table {ltforeign_catalog} => pg_foreign_catalog {oid} +NOTICE: checking pg_lake_table {ltforeign_volume} => pg_foreign_volume {oid} NOTICE: checking pg_policy {polrelid} => pg_class {oid} NOTICE: checking pg_policy {polroles} => pg_authid {oid} NOTICE: checking pg_default_acl {defaclrole} => pg_authid {oid} diff --git a/contrib/pax_storage/src/test/regress/expected/sanity_check.out b/contrib/pax_storage/src/test/regress/expected/sanity_check.out index b61eee481bd..2e36e3abd35 100644 --- a/contrib/pax_storage/src/test/regress/expected/sanity_check.out +++ b/contrib/pax_storage/src/test/regress/expected/sanity_check.out @@ -112,13 +112,16 @@ pg_enum|t pg_event_trigger|t pg_extension|t pg_extprotocol|t +pg_foreign_catalog|t pg_foreign_data_wrapper|t pg_foreign_server|t pg_foreign_table|t pg_foreign_table_seg|t +pg_foreign_volume|t pg_index|t pg_inherits|t pg_init_privs|t +pg_lake_table|t pg_language|t pg_largeobject|t pg_largeobject_metadata|t diff --git a/src/test/regress/expected/lake_table.out b/src/test/regress/expected/lake_table.out new file mode 100644 index 00000000000..a97e854c27c --- /dev/null +++ b/src/test/regress/expected/lake_table.out @@ -0,0 +1,288 @@ +-- +-- Test lake table DDL: FOREIGN CATALOG, FOREIGN VOLUME, ICEBERG TABLE +-- +-- Display the lake table catalogs +\d+ pg_foreign_catalog + Table "pg_catalog.pg_foreign_catalog" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +-----------+--------+-----------+----------+---------+----------+--------------+------------- + oid | oid | | not null | | plain | | + fcname | name | | not null | | plain | | + fcowner | oid | | not null | | plain | | + fcserver | oid | | not null | | plain | | + fcoptions | text[] | C | | | extended | | +Indexes: + "pg_foreign_catalog_oid_index" PRIMARY KEY, btree (oid) + "pg_foreign_catalog_name_index" UNIQUE CONSTRAINT, btree (fcname) + +\d+ pg_foreign_volume + Table "pg_catalog.pg_foreign_volume" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +-----------+--------+-----------+----------+---------+----------+--------------+------------- + oid | oid | | not null | | plain | | + fvname | name | | not null | | plain | | + fvowner | oid | | not null | | plain | | + fvserver | oid | | not null | | plain | | + fvoptions | text[] | C | | | extended | | +Indexes: + "pg_foreign_volume_oid_index" PRIMARY KEY, btree (oid) + "pg_foreign_volume_name_index" UNIQUE CONSTRAINT, btree (fvname) + +\d+ pg_lake_table + Table "pg_catalog.pg_lake_table" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +-------------------+--------+-----------+----------+---------+----------+--------------+------------- + ltrelid | oid | | not null | | plain | | + ltforeign_catalog | oid | | not null | | plain | | + ltforeign_volume | oid | | not null | | plain | | + lttable_type | text | C | | | extended | | + ltoptions | text[] | C | | | extended | | +Indexes: + "pg_lake_table_relid_index" PRIMARY KEY, btree (ltrelid) + +-- Setup: foreign servers for the catalogs and volumes to hang off +CREATE FOREIGN DATA WRAPPER lake_test_fdw; +CREATE SERVER lake_test_srv FOREIGN DATA WRAPPER lake_test_fdw; +CREATE SERVER lake_test_srv2 FOREIGN DATA WRAPPER lake_test_fdw; +-- CREATE FOREIGN CATALOG +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv OPTIONS (type 'hive', uri 'thrift://localhost:9083'); +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv; -- fail, duplicate +ERROR: foreign catalog "lake_test_cat" already exists +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv; -- skip with notice +NOTICE: foreign catalog "lake_test_cat" already exists, skipping +-- catalog names are global: the same name on another server is still a duplicate +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv2; -- fail, duplicate +ERROR: foreign catalog "lake_test_cat" already exists +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv2; -- skip with notice +NOTICE: foreign catalog "lake_test_cat" already exists, skipping +CREATE FOREIGN CATALOG lake_test_bad SERVER no_such_server; -- fail, no server +ERROR: server "no_such_server" does not exist +SELECT fcname, fcoptions FROM pg_foreign_catalog WHERE fcname LIKE 'lake\_test%' ORDER BY 1; + fcname | fcoptions +---------------+----------------------------------------- + lake_test_cat | {type=hive,uri=thrift://localhost:9083} +(1 row) + +-- CREATE FOREIGN VOLUME +CREATE FOREIGN VOLUME lake_test_vol SERVER lake_test_srv OPTIONS (path 's3://bucket/prefix'); +CREATE FOREIGN VOLUME lake_test_vol SERVER lake_test_srv; -- fail, duplicate +ERROR: foreign volume "lake_test_vol" already exists +CREATE FOREIGN VOLUME IF NOT EXISTS lake_test_vol SERVER lake_test_srv; -- skip with notice +NOTICE: foreign volume "lake_test_vol" already exists, skipping +-- volume names are global: the same name on another server is still a duplicate +CREATE FOREIGN VOLUME lake_test_vol SERVER lake_test_srv2; -- fail, duplicate +ERROR: foreign volume "lake_test_vol" already exists +CREATE FOREIGN VOLUME IF NOT EXISTS lake_test_vol SERVER lake_test_srv2; -- skip with notice +NOTICE: foreign volume "lake_test_vol" already exists, skipping +CREATE FOREIGN VOLUME lake_test_bad SERVER no_such_server; -- fail, no server +ERROR: server "no_such_server" does not exist +SELECT fvname, fvoptions FROM pg_foreign_volume WHERE fvname LIKE 'lake\_test%' ORDER BY 1; + fvname | fvoptions +---------------+--------------------------- + lake_test_vol | {path=s3://bucket/prefix} +(1 row) + +-- Object descriptions +SELECT pg_catalog.pg_describe_object('pg_foreign_catalog'::regclass, oid, 0) + FROM pg_foreign_catalog WHERE fcname = 'lake_test_cat'; + pg_describe_object +----------------------- + catalog lake_test_cat +(1 row) + +SELECT pg_catalog.pg_describe_object('pg_foreign_volume'::regclass, oid, 0) + FROM pg_foreign_volume WHERE fvname = 'lake_test_vol'; + pg_describe_object +---------------------- + volume lake_test_vol +(1 row) + +-- Catalog and volume rows are dispatched to all segments +SELECT count(DISTINCT gp_segment_id) > 1 AS on_all_segments + FROM gp_dist_random('pg_foreign_catalog') WHERE fcname = 'lake_test_cat'; + on_all_segments +----------------- + t +(1 row) + +SELECT count(DISTINCT gp_segment_id) > 1 AS on_all_segments + FROM gp_dist_random('pg_foreign_volume') WHERE fvname = 'lake_test_vol'; + on_all_segments +----------------- + t +(1 row) + +-- Without a provider extension there is no iceberg table AM +CREATE ICEBERG TABLE lake_test_t0 (a int) CATALOG lake_test_cat VOLUME lake_test_vol; -- fail with hint +ERROR: table access method "iceberg" does not exist +HINT: CREATE ICEBERG TABLE requires an extension that provides the "iceberg" table access method. +-- The default catalog/volume GUCs verify that the object exists +SET iceberg_default_catalog = 'no_such_catalog'; -- fail +ERROR: invalid value for parameter "iceberg_default_catalog": "no_such_catalog" +DETAIL: Foreign catalog "no_such_catalog" does not exist. +SET iceberg_default_volume = 'no_such_volume'; -- fail +ERROR: invalid value for parameter "iceberg_default_volume": "no_such_volume" +DETAIL: Foreign volume "no_such_volume" does not exist. +-- Simulate a datalake provider with a heap-backed iceberg AM +CREATE ACCESS METHOD iceberg TYPE TABLE HANDLER heap_tableam_handler; +-- CREATE ICEBERG TABLE with explicit catalog and volume +CREATE ICEBERG TABLE lake_test_t1 (a int, b text) CATALOG lake_test_cat VOLUME lake_test_vol OPTIONS (fileformat 'parquet'); +SELECT c.relname, lt.lttable_type, lt.ltoptions, fc.fcname, fv.fvname + FROM pg_lake_table lt + JOIN pg_class c ON c.oid = lt.ltrelid + JOIN pg_foreign_catalog fc ON fc.oid = lt.ltforeign_catalog + JOIN pg_foreign_volume fv ON fv.oid = lt.ltforeign_volume + ORDER BY 1; + relname | lttable_type | ltoptions | fcname | fvname +--------------+--------------+----------------------+---------------+--------------- + lake_test_t1 | ICEBERG | {fileformat=parquet} | lake_test_cat | lake_test_vol +(1 row) + +SELECT a.amname FROM pg_am a JOIN pg_class c ON c.relam = a.oid WHERE c.relname = 'lake_test_t1'; + amname +--------- + iceberg +(1 row) + +-- Lake tables are always DISTRIBUTED RANDOMLY (policytype 'p', no distkey) +SELECT policytype, distkey FROM gp_distribution_policy WHERE localoid = 'lake_test_t1'::regclass; + policytype | distkey +------------+--------- + p | +(1 row) + +-- The (heap-backed) table is usable +INSERT INTO lake_test_t1 VALUES (1, 'x'), (2, 'y'); +SELECT count(*) FROM lake_test_t1; + count +------- + 2 +(1 row) + +-- Lake tables get a TOAST table like plain tables, so wide values work +SELECT reltoastrelid <> 0 AS has_toast FROM pg_class WHERE relname = 'lake_test_t1'; + has_toast +----------- + t +(1 row) + +INSERT INTO lake_test_t1 VALUES (3, repeat('x', 500000)); +SELECT a, length(b) FROM lake_test_t1 WHERE a = 3; + a | length +---+-------- + 3 | 500000 +(1 row) + +-- Catalog and volume are both required +CREATE ICEBERG TABLE lake_test_t2 (a int) VOLUME lake_test_vol; -- fail, no catalog +ERROR: no foreign catalog specified +HINT: Specify CATALOG in CREATE ICEBERG TABLE or set iceberg_default_catalog. +CREATE ICEBERG TABLE lake_test_t2 (a int) CATALOG lake_test_cat; -- fail, no volume +ERROR: no foreign volume specified +HINT: Specify VOLUME in CREATE ICEBERG TABLE or set iceberg_default_volume. +-- ... unless the GUCs provide defaults +SET iceberg_default_catalog = 'lake_test_cat'; +SET iceberg_default_volume = 'lake_test_vol'; +CREATE ICEBERG TABLE lake_test_t2 (a int); +RESET iceberg_default_catalog; +RESET iceberg_default_volume; +-- A DISTRIBUTED clause is ignored with a warning +CREATE ICEBERG TABLE lake_test_t3 (a int) CATALOG lake_test_cat VOLUME lake_test_vol DISTRIBUTED BY (a); +WARNING: DISTRIBUTED clause has no effect for lake tables, using DISTRIBUTED RANDOMLY +SELECT policytype, distkey FROM gp_distribution_policy WHERE localoid = 'lake_test_t3'::regclass; + policytype | distkey +------------+--------- + p | +(1 row) + +-- CREATE ICEBERG TABLE only accepts the iceberg access method +CREATE ICEBERG TABLE lake_test_bad0 (a int) CATALOG lake_test_cat VOLUME lake_test_vol USING heap; -- fail +ERROR: access method "heap" is not supported for iceberg tables +HINT: Omit the USING clause; CREATE ICEBERG TABLE always uses the "iceberg" access method. +CREATE ICEBERG TABLE lake_test_t4 (a int) CATALOG lake_test_cat VOLUME lake_test_vol USING iceberg; -- explicit iceberg is fine +-- The iceberg AM is rejected for every path other than CREATE ICEBERG TABLE +CREATE TABLE lake_test_bad1 (a int) USING iceberg DISTRIBUTED RANDOMLY; -- fail +ERROR: cannot create table "lake_test_bad1" with access method "iceberg" +HINT: Use CREATE ICEBERG TABLE instead. +CREATE TABLE lake_test_bad2 USING iceberg AS SELECT 1 AS a DISTRIBUTED RANDOMLY; -- fail +ERROR: cannot create table "lake_test_bad2" with access method "iceberg" +HINT: Use CREATE ICEBERG TABLE instead. +SET default_table_access_method = iceberg; +CREATE TABLE lake_test_bad3 (a int) DISTRIBUTED RANDOMLY; -- fail +ERROR: cannot create table "lake_test_bad3" with access method "iceberg" +HINT: Use CREATE ICEBERG TABLE instead. +RESET default_table_access_method; +CREATE TABLE lake_test_heap (a int) DISTRIBUTED RANDOMLY; +ALTER TABLE lake_test_heap SET ACCESS METHOD iceberg; -- fail +ERROR: cannot change access method of table "lake_test_heap" to "iceberg" +HINT: Use CREATE ICEBERG TABLE to create an iceberg table. +ALTER TABLE lake_test_t1 SET ACCESS METHOD heap; -- fail +ERROR: cannot change access method of lake table "lake_test_t1" +ALTER TABLE lake_test_t1 SET DISTRIBUTED BY (a); -- fail +ERROR: cannot change distribution policy of lake table "lake_test_t1" +HINT: Lake tables must use DISTRIBUTED RANDOMLY because data is stored on object storage. +-- Only the owner can drop a catalog or volume +CREATE ROLE regress_lake_user; +NOTICE: resource queue required -- using default resource queue "pg_default" +SET ROLE regress_lake_user; +DROP CATALOG lake_test_cat; -- fail, not owner +ERROR: must be owner of foreign catalog lake_test_cat +DROP VOLUME lake_test_vol; -- fail, not owner +ERROR: must be owner of foreign volume lake_test_vol +RESET ROLE; +-- Dependencies: the server holds the catalog/volume, which hold the tables +DROP SERVER lake_test_srv; -- fail, catalog and volume depend on it +ERROR: cannot drop server lake_test_srv because other objects depend on it +DETAIL: catalog lake_test_cat depends on server lake_test_srv +volume lake_test_vol depends on server lake_test_srv +table lake_test_t1 depends on volume lake_test_vol +table lake_test_t2 depends on volume lake_test_vol +table lake_test_t3 depends on volume lake_test_vol +table lake_test_t4 depends on volume lake_test_vol +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP CATALOG lake_test_cat; -- fail, tables depend on it +ERROR: cannot drop catalog lake_test_cat because other objects depend on it +DETAIL: table lake_test_t1 depends on catalog lake_test_cat +table lake_test_t2 depends on catalog lake_test_cat +table lake_test_t3 depends on catalog lake_test_cat +table lake_test_t4 depends on catalog lake_test_cat +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- Dropping a lake table removes its pg_lake_table entry +DROP TABLE lake_test_t1; +SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid + WHERE c.relname = 'lake_test_t1'; + count +------- + 0 +(1 row) + +-- DROP CATALOG ... CASCADE takes the remaining tables with it +DROP CATALOG lake_test_cat CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table lake_test_t2 +drop cascades to table lake_test_t3 +drop cascades to table lake_test_t4 +SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid + WHERE c.relname LIKE 'lake\_test%'; + count +------- + 0 +(1 row) + +-- DROP variants +DROP CATALOG lake_test_cat; -- fail, already gone +ERROR: foreign catalog "lake_test_cat" does not exist +DROP CATALOG IF EXISTS lake_test_cat; -- skip with notice +NOTICE: foreign catalog "lake_test_cat" does not exist, skipping +DROP VOLUME lake_test_vol; +DROP VOLUME lake_test_vol; -- fail, already gone +ERROR: foreign volume "lake_test_vol" does not exist +DROP VOLUME IF EXISTS lake_test_vol; -- skip with notice +NOTICE: foreign volume "lake_test_vol" does not exist, skipping +-- Cleanup +DROP TABLE lake_test_heap; +DROP ROLE regress_lake_user; +DROP SERVER lake_test_srv; +DROP SERVER lake_test_srv2; +DROP FOREIGN DATA WRAPPER lake_test_fdw; +DROP ACCESS METHOD iceberg; diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 19094e111dc..6dc91c68aec 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -235,6 +235,10 @@ NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} +NOTICE: checking pg_foreign_catalog {fcowner} => pg_authid {oid} +NOTICE: checking pg_foreign_catalog {fcserver} => pg_foreign_server {oid} +NOTICE: checking pg_foreign_volume {fvowner} => pg_authid {oid} +NOTICE: checking pg_foreign_volume {fvserver} => pg_foreign_server {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} NOTICE: checking pg_user_mapping {umserver} => pg_foreign_server {oid} NOTICE: checking pg_compression {compconstructor} => pg_proc {oid} @@ -247,6 +251,9 @@ NOTICE: checking pg_foreign_table {ftrelid} => pg_class {oid} NOTICE: checking pg_foreign_table {ftserver} => pg_foreign_server {oid} NOTICE: checking pg_foreign_table_seg {ftsrelid} => pg_class {oid} NOTICE: checking pg_foreign_table_seg {ftsserver} => pg_foreign_server {oid} +NOTICE: checking pg_lake_table {ltrelid} => pg_class {oid} +NOTICE: checking pg_lake_table {ltforeign_catalog} => pg_foreign_catalog {oid} +NOTICE: checking pg_lake_table {ltforeign_volume} => pg_foreign_volume {oid} NOTICE: checking pg_policy {polrelid} => pg_class {oid} NOTICE: checking pg_policy {polroles} => pg_authid {oid} NOTICE: checking pg_default_acl {defaclrole} => pg_authid {oid} diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index 4625d100bf0..f5d55362906 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -124,13 +124,16 @@ pg_enum|t pg_event_trigger|t pg_extension|t pg_extprotocol|t +pg_foreign_catalog|t pg_foreign_data_wrapper|t pg_foreign_server|t pg_foreign_table|t pg_foreign_table_seg|t +pg_foreign_volume|t pg_index|t pg_inherits|t pg_init_privs|t +pg_lake_table|t pg_language|t pg_largeobject|t pg_largeobject_metadata|t diff --git a/src/test/regress/greenplum_schedule b/src/test/regress/greenplum_schedule index 84e8766844b..44a916cc218 100755 --- a/src/test/regress/greenplum_schedule +++ b/src/test/regress/greenplum_schedule @@ -358,6 +358,9 @@ test: am_encoding # tests of directory table test: directory_table +# tests of lake table DDL (foreign catalog, foreign volume, iceberg table) +test: lake_table + # test if motion sockets are created with the gp_segment_configuration.address test: motion_socket diff --git a/src/test/regress/sql/lake_table.sql b/src/test/regress/sql/lake_table.sql new file mode 100644 index 00000000000..f9683baf604 --- /dev/null +++ b/src/test/regress/sql/lake_table.sql @@ -0,0 +1,142 @@ +-- +-- Test lake table DDL: FOREIGN CATALOG, FOREIGN VOLUME, ICEBERG TABLE +-- + +-- Display the lake table catalogs +\d+ pg_foreign_catalog +\d+ pg_foreign_volume +\d+ pg_lake_table + +-- Setup: foreign servers for the catalogs and volumes to hang off +CREATE FOREIGN DATA WRAPPER lake_test_fdw; +CREATE SERVER lake_test_srv FOREIGN DATA WRAPPER lake_test_fdw; +CREATE SERVER lake_test_srv2 FOREIGN DATA WRAPPER lake_test_fdw; + +-- CREATE FOREIGN CATALOG +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv OPTIONS (type 'hive', uri 'thrift://localhost:9083'); +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv; -- fail, duplicate +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv; -- skip with notice +-- catalog names are global: the same name on another server is still a duplicate +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv2; -- fail, duplicate +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv2; -- skip with notice +CREATE FOREIGN CATALOG lake_test_bad SERVER no_such_server; -- fail, no server +SELECT fcname, fcoptions FROM pg_foreign_catalog WHERE fcname LIKE 'lake\_test%' ORDER BY 1; + +-- CREATE FOREIGN VOLUME +CREATE FOREIGN VOLUME lake_test_vol SERVER lake_test_srv OPTIONS (path 's3://bucket/prefix'); +CREATE FOREIGN VOLUME lake_test_vol SERVER lake_test_srv; -- fail, duplicate +CREATE FOREIGN VOLUME IF NOT EXISTS lake_test_vol SERVER lake_test_srv; -- skip with notice +-- volume names are global: the same name on another server is still a duplicate +CREATE FOREIGN VOLUME lake_test_vol SERVER lake_test_srv2; -- fail, duplicate +CREATE FOREIGN VOLUME IF NOT EXISTS lake_test_vol SERVER lake_test_srv2; -- skip with notice +CREATE FOREIGN VOLUME lake_test_bad SERVER no_such_server; -- fail, no server +SELECT fvname, fvoptions FROM pg_foreign_volume WHERE fvname LIKE 'lake\_test%' ORDER BY 1; + +-- Object descriptions +SELECT pg_catalog.pg_describe_object('pg_foreign_catalog'::regclass, oid, 0) + FROM pg_foreign_catalog WHERE fcname = 'lake_test_cat'; +SELECT pg_catalog.pg_describe_object('pg_foreign_volume'::regclass, oid, 0) + FROM pg_foreign_volume WHERE fvname = 'lake_test_vol'; + +-- Catalog and volume rows are dispatched to all segments +SELECT count(DISTINCT gp_segment_id) > 1 AS on_all_segments + FROM gp_dist_random('pg_foreign_catalog') WHERE fcname = 'lake_test_cat'; +SELECT count(DISTINCT gp_segment_id) > 1 AS on_all_segments + FROM gp_dist_random('pg_foreign_volume') WHERE fvname = 'lake_test_vol'; + +-- Without a provider extension there is no iceberg table AM +CREATE ICEBERG TABLE lake_test_t0 (a int) CATALOG lake_test_cat VOLUME lake_test_vol; -- fail with hint + +-- The default catalog/volume GUCs verify that the object exists +SET iceberg_default_catalog = 'no_such_catalog'; -- fail +SET iceberg_default_volume = 'no_such_volume'; -- fail + +-- Simulate a datalake provider with a heap-backed iceberg AM +CREATE ACCESS METHOD iceberg TYPE TABLE HANDLER heap_tableam_handler; + +-- CREATE ICEBERG TABLE with explicit catalog and volume +CREATE ICEBERG TABLE lake_test_t1 (a int, b text) CATALOG lake_test_cat VOLUME lake_test_vol OPTIONS (fileformat 'parquet'); +SELECT c.relname, lt.lttable_type, lt.ltoptions, fc.fcname, fv.fvname + FROM pg_lake_table lt + JOIN pg_class c ON c.oid = lt.ltrelid + JOIN pg_foreign_catalog fc ON fc.oid = lt.ltforeign_catalog + JOIN pg_foreign_volume fv ON fv.oid = lt.ltforeign_volume + ORDER BY 1; +SELECT a.amname FROM pg_am a JOIN pg_class c ON c.relam = a.oid WHERE c.relname = 'lake_test_t1'; +-- Lake tables are always DISTRIBUTED RANDOMLY (policytype 'p', no distkey) +SELECT policytype, distkey FROM gp_distribution_policy WHERE localoid = 'lake_test_t1'::regclass; + +-- The (heap-backed) table is usable +INSERT INTO lake_test_t1 VALUES (1, 'x'), (2, 'y'); +SELECT count(*) FROM lake_test_t1; + +-- Lake tables get a TOAST table like plain tables, so wide values work +SELECT reltoastrelid <> 0 AS has_toast FROM pg_class WHERE relname = 'lake_test_t1'; +INSERT INTO lake_test_t1 VALUES (3, repeat('x', 500000)); +SELECT a, length(b) FROM lake_test_t1 WHERE a = 3; + +-- Catalog and volume are both required +CREATE ICEBERG TABLE lake_test_t2 (a int) VOLUME lake_test_vol; -- fail, no catalog +CREATE ICEBERG TABLE lake_test_t2 (a int) CATALOG lake_test_cat; -- fail, no volume + +-- ... unless the GUCs provide defaults +SET iceberg_default_catalog = 'lake_test_cat'; +SET iceberg_default_volume = 'lake_test_vol'; +CREATE ICEBERG TABLE lake_test_t2 (a int); +RESET iceberg_default_catalog; +RESET iceberg_default_volume; + +-- A DISTRIBUTED clause is ignored with a warning +CREATE ICEBERG TABLE lake_test_t3 (a int) CATALOG lake_test_cat VOLUME lake_test_vol DISTRIBUTED BY (a); +SELECT policytype, distkey FROM gp_distribution_policy WHERE localoid = 'lake_test_t3'::regclass; + +-- CREATE ICEBERG TABLE only accepts the iceberg access method +CREATE ICEBERG TABLE lake_test_bad0 (a int) CATALOG lake_test_cat VOLUME lake_test_vol USING heap; -- fail +CREATE ICEBERG TABLE lake_test_t4 (a int) CATALOG lake_test_cat VOLUME lake_test_vol USING iceberg; -- explicit iceberg is fine + +-- The iceberg AM is rejected for every path other than CREATE ICEBERG TABLE +CREATE TABLE lake_test_bad1 (a int) USING iceberg DISTRIBUTED RANDOMLY; -- fail +CREATE TABLE lake_test_bad2 USING iceberg AS SELECT 1 AS a DISTRIBUTED RANDOMLY; -- fail +SET default_table_access_method = iceberg; +CREATE TABLE lake_test_bad3 (a int) DISTRIBUTED RANDOMLY; -- fail +RESET default_table_access_method; +CREATE TABLE lake_test_heap (a int) DISTRIBUTED RANDOMLY; +ALTER TABLE lake_test_heap SET ACCESS METHOD iceberg; -- fail +ALTER TABLE lake_test_t1 SET ACCESS METHOD heap; -- fail +ALTER TABLE lake_test_t1 SET DISTRIBUTED BY (a); -- fail + +-- Only the owner can drop a catalog or volume +CREATE ROLE regress_lake_user; +SET ROLE regress_lake_user; +DROP CATALOG lake_test_cat; -- fail, not owner +DROP VOLUME lake_test_vol; -- fail, not owner +RESET ROLE; + +-- Dependencies: the server holds the catalog/volume, which hold the tables +DROP SERVER lake_test_srv; -- fail, catalog and volume depend on it +DROP CATALOG lake_test_cat; -- fail, tables depend on it + +-- Dropping a lake table removes its pg_lake_table entry +DROP TABLE lake_test_t1; +SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid + WHERE c.relname = 'lake_test_t1'; + +-- DROP CATALOG ... CASCADE takes the remaining tables with it +DROP CATALOG lake_test_cat CASCADE; +SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid + WHERE c.relname LIKE 'lake\_test%'; + +-- DROP variants +DROP CATALOG lake_test_cat; -- fail, already gone +DROP CATALOG IF EXISTS lake_test_cat; -- skip with notice +DROP VOLUME lake_test_vol; +DROP VOLUME lake_test_vol; -- fail, already gone +DROP VOLUME IF EXISTS lake_test_vol; -- skip with notice + +-- Cleanup +DROP TABLE lake_test_heap; +DROP ROLE regress_lake_user; +DROP SERVER lake_test_srv; +DROP SERVER lake_test_srv2; +DROP FOREIGN DATA WRAPPER lake_test_fdw; +DROP ACCESS METHOD iceberg; diff --git a/src/test/singlenode_regress/expected/oidjoins.out b/src/test/singlenode_regress/expected/oidjoins.out index b1dca18dc97..b6bb1a499ce 100644 --- a/src/test/singlenode_regress/expected/oidjoins.out +++ b/src/test/singlenode_regress/expected/oidjoins.out @@ -235,6 +235,10 @@ NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} +NOTICE: checking pg_foreign_catalog {fcowner} => pg_authid {oid} +NOTICE: checking pg_foreign_catalog {fcserver} => pg_foreign_server {oid} +NOTICE: checking pg_foreign_volume {fvowner} => pg_authid {oid} +NOTICE: checking pg_foreign_volume {fvserver} => pg_foreign_server {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} NOTICE: checking pg_user_mapping {umserver} => pg_foreign_server {oid} NOTICE: checking pg_compression {compconstructor} => pg_proc {oid} @@ -247,6 +251,9 @@ NOTICE: checking pg_foreign_table {ftrelid} => pg_class {oid} NOTICE: checking pg_foreign_table {ftserver} => pg_foreign_server {oid} NOTICE: checking pg_foreign_table_seg {ftsrelid} => pg_class {oid} NOTICE: checking pg_foreign_table_seg {ftsserver} => pg_foreign_server {oid} +NOTICE: checking pg_lake_table {ltrelid} => pg_class {oid} +NOTICE: checking pg_lake_table {ltforeign_catalog} => pg_foreign_catalog {oid} +NOTICE: checking pg_lake_table {ltforeign_volume} => pg_foreign_volume {oid} NOTICE: checking pg_policy {polrelid} => pg_class {oid} NOTICE: checking pg_policy {polroles} => pg_authid {oid} NOTICE: checking pg_default_acl {defaclrole} => pg_authid {oid} diff --git a/src/test/singlenode_regress/expected/sanity_check.out b/src/test/singlenode_regress/expected/sanity_check.out index a5a122ac32d..844f094fa57 100644 --- a/src/test/singlenode_regress/expected/sanity_check.out +++ b/src/test/singlenode_regress/expected/sanity_check.out @@ -135,13 +135,16 @@ pg_enum|t pg_event_trigger|t pg_extension|t pg_extprotocol|t +pg_foreign_catalog|t pg_foreign_data_wrapper|t pg_foreign_server|t pg_foreign_table|t pg_foreign_table_seg|t +pg_foreign_volume|t pg_index|t pg_inherits|t pg_init_privs|t +pg_lake_table|t pg_language|t pg_largeobject|t pg_largeobject_metadata|t From 5be5f9ffd3515bcb7e376faf1ae9260212cdf201 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Mon, 6 Jul 2026 17:50:11 +0800 Subject: [PATCH 08/11] Use ASF license header for new lake-table source files Address review: new community-authored files should carry the standard Apache-2.0 header instead of the PostgreSQL boilerplate. --- src/backend/commands/laketablecmds.c | 16 +++++++++++++++- src/include/catalog/pg_foreign_catalog.h | 18 ++++++++++++++++-- src/include/catalog/pg_foreign_volume.h | 18 ++++++++++++++++-- src/include/catalog/pg_lake_table.h | 17 +++++++++++++++-- src/include/commands/laketablecmds.h | 18 ++++++++++++++++-- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/backend/commands/laketablecmds.c b/src/backend/commands/laketablecmds.c index 80771f0b1aa..02037e39baa 100644 --- a/src/backend/commands/laketablecmds.c +++ b/src/backend/commands/laketablecmds.c @@ -3,8 +3,22 @@ * laketablecmds.c * lake table creation/manipulation commands * - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. * * IDENTIFICATION * src/backend/commands/laketablecmds.c diff --git a/src/include/catalog/pg_foreign_catalog.h b/src/include/catalog/pg_foreign_catalog.h index 7623cd9e807..1f3df631447 100644 --- a/src/include/catalog/pg_foreign_catalog.h +++ b/src/include/catalog/pg_foreign_catalog.h @@ -3,8 +3,22 @@ * pg_foreign_catalog.h * definition of the "foreign catalog" system catalog (pg_foreign_catalog) * - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. * * src/include/catalog/pg_foreign_catalog.h * diff --git a/src/include/catalog/pg_foreign_volume.h b/src/include/catalog/pg_foreign_volume.h index 669bb1246d0..ab44d6a0a18 100644 --- a/src/include/catalog/pg_foreign_volume.h +++ b/src/include/catalog/pg_foreign_volume.h @@ -3,8 +3,22 @@ * pg_foreign_volume.h * definition of the "foreign volume" system catalog (pg_foreign_volume) * - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. * * src/include/catalog/pg_foreign_volume.h * diff --git a/src/include/catalog/pg_lake_table.h b/src/include/catalog/pg_lake_table.h index 37c8a208d31..7d6da077ce3 100644 --- a/src/include/catalog/pg_lake_table.h +++ b/src/include/catalog/pg_lake_table.h @@ -3,9 +3,22 @@ * pg_lake_table.h * definition of the "lake table" system catalog (pg_lake_table) * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. * * src/include/catalog/pg_lake_table.h * diff --git a/src/include/commands/laketablecmds.h b/src/include/commands/laketablecmds.h index 05564a79a98..98904902073 100644 --- a/src/include/commands/laketablecmds.h +++ b/src/include/commands/laketablecmds.h @@ -3,8 +3,22 @@ * laketablecmds.h * prototypes for laketablecmds.c. * - * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. * * src/include/commands/laketablecmds.h * From 272d910f26107d1e328d3fe49596faef0d093056 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Mon, 6 Jul 2026 17:58:21 +0800 Subject: [PATCH 09/11] Move ASF license header to top of file header block Match the community convention (see contrib/pax_storage pax_gbench.cc): the Apache-2.0 license block comes first, followed by the file name and IDENTIFICATION. --- src/backend/commands/laketablecmds.c | 6 +++--- src/include/catalog/pg_foreign_catalog.h | 6 +++--- src/include/catalog/pg_foreign_volume.h | 6 +++--- src/include/catalog/pg_lake_table.h | 6 +++--- src/include/commands/laketablecmds.h | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/backend/commands/laketablecmds.c b/src/backend/commands/laketablecmds.c index 02037e39baa..90cfb6fa4b5 100644 --- a/src/backend/commands/laketablecmds.c +++ b/src/backend/commands/laketablecmds.c @@ -1,7 +1,4 @@ /*------------------------------------------------------------------------- - * - * laketablecmds.c - * lake table creation/manipulation commands * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -20,6 +17,9 @@ * specific language governing permissions and limitations * under the License. * + * laketablecmds.c + * lake table creation/manipulation commands + * * IDENTIFICATION * src/backend/commands/laketablecmds.c * diff --git a/src/include/catalog/pg_foreign_catalog.h b/src/include/catalog/pg_foreign_catalog.h index 1f3df631447..b6325915b22 100644 --- a/src/include/catalog/pg_foreign_catalog.h +++ b/src/include/catalog/pg_foreign_catalog.h @@ -1,7 +1,4 @@ /*------------------------------------------------------------------------- - * - * pg_foreign_catalog.h - * definition of the "foreign catalog" system catalog (pg_foreign_catalog) * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -20,6 +17,9 @@ * specific language governing permissions and limitations * under the License. * + * pg_foreign_catalog.h + * definition of the "foreign catalog" system catalog (pg_foreign_catalog) + * * src/include/catalog/pg_foreign_catalog.h * * NOTES diff --git a/src/include/catalog/pg_foreign_volume.h b/src/include/catalog/pg_foreign_volume.h index ab44d6a0a18..d3e377f1939 100644 --- a/src/include/catalog/pg_foreign_volume.h +++ b/src/include/catalog/pg_foreign_volume.h @@ -1,7 +1,4 @@ /*------------------------------------------------------------------------- - * - * pg_foreign_volume.h - * definition of the "foreign volume" system catalog (pg_foreign_volume) * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -20,6 +17,9 @@ * specific language governing permissions and limitations * under the License. * + * pg_foreign_volume.h + * definition of the "foreign volume" system catalog (pg_foreign_volume) + * * src/include/catalog/pg_foreign_volume.h * * NOTES diff --git a/src/include/catalog/pg_lake_table.h b/src/include/catalog/pg_lake_table.h index 7d6da077ce3..d22ce5a077f 100644 --- a/src/include/catalog/pg_lake_table.h +++ b/src/include/catalog/pg_lake_table.h @@ -1,7 +1,4 @@ /*------------------------------------------------------------------------- - * - * pg_lake_table.h - * definition of the "lake table" system catalog (pg_lake_table) * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -20,6 +17,9 @@ * specific language governing permissions and limitations * under the License. * + * pg_lake_table.h + * definition of the "lake table" system catalog (pg_lake_table) + * * src/include/catalog/pg_lake_table.h * * NOTES diff --git a/src/include/commands/laketablecmds.h b/src/include/commands/laketablecmds.h index 98904902073..3b7b949a7fa 100644 --- a/src/include/commands/laketablecmds.h +++ b/src/include/commands/laketablecmds.h @@ -1,7 +1,4 @@ /*------------------------------------------------------------------------- - * - * laketablecmds.h - * prototypes for laketablecmds.c. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -20,6 +17,9 @@ * specific language governing permissions and limitations * under the License. * + * laketablecmds.h + * prototypes for laketablecmds.c. + * * src/include/commands/laketablecmds.h * *------------------------------------------------------------------------- From c70fbf9ac729f75b6cb8d736f2a88b7a031aecea Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Tue, 7 Jul 2026 15:38:13 +0800 Subject: [PATCH 10/11] foreigncmds: flatten IF NOT EXISTS duplicate handling Address review: invert the if_not_exists check and error out early so the skip path is no longer nested in an else branch, in both CreateForeignCatalog and CreateForeignVolume. --- src/backend/commands/foreigncmds.c | 68 ++++++++++++++---------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index c9a1e53787e..b5e6adc95f1 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -1061,28 +1061,26 @@ CreateForeignCatalog(CreateForeignCatalogStmt *stmt) catalogId = get_foreign_catalog_oid(stmt->catalogname, true); if (OidIsValid(catalogId)) { - if (stmt->if_not_exists) - { - /* - * If we are in an extension script, insist that the pre-existing - * object be a member of the extension, to avoid security risks. - */ - ObjectAddressSet(myself, ForeignCatalogRelationId, catalogId); - checkMembershipInCurrentExtension(&myself); - - /* OK to skip */ - ereport(NOTICE, - (errcode(ERRCODE_DUPLICATE_OBJECT), - errmsg("foreign catalog \"%s\" already exists, skipping", - stmt->catalogname))); - table_close(rel, RowExclusiveLock); - return InvalidObjectAddress; - } - else + if (!stmt->if_not_exists) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("foreign catalog \"%s\" already exists", stmt->catalogname))); + + /* + * If we are in an extension script, insist that the pre-existing + * object be a member of the extension, to avoid security risks. + */ + ObjectAddressSet(myself, ForeignCatalogRelationId, catalogId); + checkMembershipInCurrentExtension(&myself); + + /* OK to skip */ + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("foreign catalog \"%s\" already exists, skipping", + stmt->catalogname))); + table_close(rel, RowExclusiveLock); + return InvalidObjectAddress; } /* @@ -1191,28 +1189,26 @@ CreateForeignVolume(CreateForeignVolumeStmt *stmt) volumeId = get_foreign_volume_oid(stmt->volumename, true); if (OidIsValid(volumeId)) { - if (stmt->if_not_exists) - { - /* - * If we are in an extension script, insist that the pre-existing - * object be a member of the extension, to avoid security risks. - */ - ObjectAddressSet(myself, ForeignVolumeRelationId, volumeId); - checkMembershipInCurrentExtension(&myself); - - /* OK to skip */ - ereport(NOTICE, - (errcode(ERRCODE_DUPLICATE_OBJECT), - errmsg("foreign volume \"%s\" already exists, skipping", - stmt->volumename))); - table_close(rel, RowExclusiveLock); - return InvalidObjectAddress; - } - else + if (!stmt->if_not_exists) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("foreign volume \"%s\" already exists", stmt->volumename))); + + /* + * If we are in an extension script, insist that the pre-existing + * object be a member of the extension, to avoid security risks. + */ + ObjectAddressSet(myself, ForeignVolumeRelationId, volumeId); + checkMembershipInCurrentExtension(&myself); + + /* OK to skip */ + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("foreign volume \"%s\" already exists, skipping", + stmt->volumename))); + table_close(rel, RowExclusiveLock); + return InvalidObjectAddress; } /* From 5105d024172b5eb674ca5fdf97f301dfca18433b Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Fri, 10 Jul 2026 10:12:02 +0800 Subject: [PATCH 11/11] parser/commands: unify lake-table DROP syntax with CREATE Mirror the CREATE side so DROP matches: - DROP CATALOG / DROP VOLUME -> DROP FOREIGN CATALOG / DROP FOREIGN VOLUME (via drop_type_name, same FOREIGN prefix as FOREIGN DATA WRAPPER) - add DROP ICEBERG TABLE [IF EXISTS] as the pair for CREATE ICEBERG TABLE DROP ICEBERG TABLE carries a new DropStmt.isiceberg flag and validates in RangeVarCallbackForDropRelation that the target actually uses the iceberg access method, erroring '"%s" is not an iceberg table' otherwise (mirrors DROP FOREIGN TABLE). Plain DROP TABLE still removes an iceberg table. Adds CMDTAG_DROP_LAKE_TABLE, psql tab completion for the new forms, and refreshes the lake_table regression with new and negative cases. Addresses review feedback on PR #1842. --- src/backend/commands/tablecmds.c | 20 ++++++++++++ src/backend/nodes/copyfuncs.funcs.c | 2 ++ src/backend/nodes/equalfuncs.c | 1 + src/backend/nodes/outfuncs_common.c | 2 ++ src/backend/nodes/readfuncs_common.c | 2 ++ src/backend/parser/gram.y | 29 +++++++++++++++-- src/backend/tcop/utility.c | 5 ++- src/bin/psql/tab-complete.c | 22 ++++++++----- src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + src/test/regress/expected/lake_table.out | 40 +++++++++++++++--------- src/test/regress/sql/lake_table.sql | 29 ++++++++++------- 12 files changed, 119 insertions(+), 35 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3786ca44e1a..cb63d10a8fa 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -256,6 +256,7 @@ struct DropRelationCallbackState { /* These fields are set by RemoveRelations: */ char expected_relkind; + bool iceberg_only; /* DROP ICEBERG TABLE: require iceberg AM */ LOCKMODE heap_lockmode; /* These fields are state to track which subsidiary locks are held: */ Oid heapOid; @@ -1998,6 +1999,7 @@ RemoveRelations(DropStmt *drop) /* Look up the appropriate relation using namespace search. */ state.expected_relkind = relkind; + state.iceberg_only = drop->isiceberg; state.heap_lockmode = drop->concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock; /* We must initialize these fields to show that no locks are held: */ @@ -2219,6 +2221,24 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid, DropErrorMsgWrongType(rel->relname, classform->relkind, state->expected_relkind); + /* + * DROP ICEBERG TABLE must target an iceberg (lake) table. Iceberg tables + * share RELKIND_RELATION with ordinary tables, so the relkind check above + * cannot tell them apart; verify the access method here (same rule as + * RelationIsIcebergTable). If no provider installed the iceberg AM, no + * relation can be an iceberg table, so this always rejects. + */ + if (state->iceberg_only) + { + Oid iceberg_amoid = GetIcebergTableAmOid(true); + + if (!OidIsValid(iceberg_amoid) || classform->relam != iceberg_amoid) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an iceberg table", rel->relname), + errhint("Use DROP TABLE to remove a table."))); + } + /* Allow DROP to either table owner or schema owner */ if (!object_ownercheck(RelationRelationId, relOid, GetUserId()) && !object_ownercheck(NamespaceRelationId, classform->relnamespace, GetUserId())) diff --git a/src/backend/nodes/copyfuncs.funcs.c b/src/backend/nodes/copyfuncs.funcs.c index 2b7cd7cfe3e..81fbaa2330b 100644 --- a/src/backend/nodes/copyfuncs.funcs.c +++ b/src/backend/nodes/copyfuncs.funcs.c @@ -3446,6 +3446,7 @@ _copyDropStmt(const DropStmt *from) COPY_SCALAR_FIELD(missing_ok); COPY_SCALAR_FIELD(concurrent); COPY_SCALAR_FIELD(isdynamic); + COPY_SCALAR_FIELD(isiceberg); return newnode; } @@ -3461,6 +3462,7 @@ _copyDropDirectoryTableStmt(const DropDirectoryTableStmt *from) COPY_SCALAR_FIELD(base.missing_ok); COPY_SCALAR_FIELD(base.concurrent); COPY_SCALAR_FIELD(base.isdynamic); + COPY_SCALAR_FIELD(base.isiceberg); COPY_SCALAR_FIELD(with_content); return newnode; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 8a8984237dd..107b762ae0e 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1493,6 +1493,7 @@ _equalDropStmt(const DropStmt *a, const DropStmt *b) COMPARE_SCALAR_FIELD(missing_ok); COMPARE_SCALAR_FIELD(concurrent); COMPARE_SCALAR_FIELD(isdynamic); + COMPARE_SCALAR_FIELD(isiceberg); return true; } diff --git a/src/backend/nodes/outfuncs_common.c b/src/backend/nodes/outfuncs_common.c index c518e38db0d..e7d6abd13fe 100644 --- a/src/backend/nodes/outfuncs_common.c +++ b/src/backend/nodes/outfuncs_common.c @@ -668,6 +668,7 @@ _outDropStmt(StringInfo str, const DropStmt *node) WRITE_BOOL_FIELD(missing_ok); WRITE_BOOL_FIELD(concurrent); WRITE_BOOL_FIELD(isdynamic); + WRITE_BOOL_FIELD(isiceberg); } static void @@ -1809,6 +1810,7 @@ _outDropStmtInfo(StringInfo str, const DropStmt *node) WRITE_BOOL_FIELD(missing_ok); WRITE_BOOL_FIELD(concurrent); WRITE_BOOL_FIELD(isdynamic); + WRITE_BOOL_FIELD(isiceberg); } static void diff --git a/src/backend/nodes/readfuncs_common.c b/src/backend/nodes/readfuncs_common.c index f895e4a4468..882a38628be 100644 --- a/src/backend/nodes/readfuncs_common.c +++ b/src/backend/nodes/readfuncs_common.c @@ -738,6 +738,7 @@ _readDropStmt_common(DropStmt *local_node) READ_BOOL_FIELD(missing_ok); READ_BOOL_FIELD(concurrent); READ_BOOL_FIELD(isdynamic); + READ_BOOL_FIELD(isiceberg); /* Force 'missing_ok' in QEs */ #ifdef COMPILING_BINARY_FUNCS @@ -1148,6 +1149,7 @@ _readDropStmt(void) READ_BOOL_FIELD(missing_ok); READ_BOOL_FIELD(concurrent); READ_BOOL_FIELD(isdynamic); + READ_BOOL_FIELD(isiceberg); /* Force 'missing_ok' in QEs */ #ifdef COMPILING_BINARY_FUNCS diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ae79cb403de..39f95ab3842 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -10530,6 +10530,31 @@ DropStmt: DROP object_type_any_name IF_P EXISTS any_name_list opt_drop_behavior n->isdynamic = true; $$ = (Node *)n; } +/* DROP ICEBERG TABLE */ + | DROP ICEBERG TABLE IF_P EXISTS any_name_list opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + n->removeType = OBJECT_TABLE; + n->missing_ok = true; + n->objects = $6; + n->behavior = $7; + n->concurrent = false; + n->isdynamic = false; + n->isiceberg = true; + $$ = (Node *)n; + } + | DROP ICEBERG TABLE any_name_list opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + n->removeType = OBJECT_TABLE; + n->missing_ok = false; + n->objects = $4; + n->behavior = $5; + n->concurrent = false; + n->isdynamic = false; + n->isiceberg = true; + $$ = (Node *)n; + } ; /* object types taking any_name/any_name_list */ @@ -10577,8 +10602,8 @@ drop_type_name: | PUBLICATION { $$ = OBJECT_PUBLICATION; } | SCHEMA { $$ = OBJECT_SCHEMA; } | SERVER { $$ = OBJECT_FOREIGN_SERVER; } - | CATALOG_P { $$ = OBJECT_FOREIGN_CATALOG; } - | VOLUME { $$ = OBJECT_FOREIGN_VOLUME; } + | FOREIGN CATALOG_P { $$ = OBJECT_FOREIGN_CATALOG; } + | FOREIGN VOLUME { $$ = OBJECT_FOREIGN_VOLUME; } | PROTOCOL { $$ = OBJECT_EXTPROTOCOL; } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 300c018ba8a..3795a189072 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -3412,7 +3412,10 @@ CreateCommandTag(Node *parsetree) switch (((DropStmt *) parsetree)->removeType) { case OBJECT_TABLE: - tag = CMDTAG_DROP_TABLE; + if (((DropStmt *) parsetree)->isiceberg) + tag = CMDTAG_DROP_LAKE_TABLE; + else + tag = CMDTAG_DROP_TABLE; break; case OBJECT_SEQUENCE: tag = CMDTAG_DROP_SEQUENCE; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 48903015c2f..2235f292804 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1267,7 +1267,6 @@ static const pgsql_thing_t words_after_create[] = { {"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates}, {"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so * skip it */ - {"CATALOG", Query_for_list_of_foreign_catalogs, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER}, {"COLLATION", NULL, NULL, &Query_for_list_of_collations}, /* @@ -1285,13 +1284,13 @@ static const pgsql_thing_t words_after_create[] = { {"EVENT TRIGGER", NULL, NULL, NULL}, {"EXTENSION", Query_for_list_of_extensions}, {"EXTERNAL TABLE", NULL, NULL, NULL}, - {"FOREIGN CATALOG", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, + {"FOREIGN CATALOG", Query_for_list_of_foreign_catalogs, NULL, NULL, NULL, THING_NO_ALTER}, {"FOREIGN DATA WRAPPER", NULL, NULL, NULL}, {"FOREIGN TABLE", NULL, NULL, NULL}, - {"FOREIGN VOLUME", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, + {"FOREIGN VOLUME", Query_for_list_of_foreign_volumes, NULL, NULL, NULL, THING_NO_ALTER}, {"FUNCTION", NULL, NULL, Query_for_list_of_functions}, {"GROUP", Query_for_list_of_roles}, - {"ICEBERG TABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, + {"ICEBERG TABLE", NULL, NULL, &Query_for_list_of_tables, NULL, THING_NO_ALTER}, {"INCREMENTAL MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews, NULL, THING_NO_DROP | THING_NO_ALTER}, {"INDEX", NULL, NULL, &Query_for_list_of_indexes}, {"LANGUAGE", Query_for_list_of_languages}, @@ -1338,7 +1337,6 @@ static const pgsql_thing_t words_after_create[] = { {"USER MAPPING FOR", NULL, NULL, NULL}, {"STORAGE USER MAPPING FOR", NULL, NULL, NULL}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, - {"VOLUME", Query_for_list_of_foreign_volumes, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER}, {"WAREHOUSE", NULL}, {NULL} /* end of list */ }; @@ -3836,7 +3834,7 @@ psql_completion(const char *text, int start, int end) /* DROP */ /* Complete DROP object with CASCADE / RESTRICT */ else if (Matches("DROP", - "CATALOG|COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW|VOLUME", + "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW", MatchAny) || Matches("DROP", "ACCESS", "METHOD", MatchAny) || (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) && @@ -3844,6 +3842,8 @@ psql_completion(const char *text, int start, int end) Matches("DROP", "EVENT", "TRIGGER", MatchAny) || Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) || Matches("DROP", "FOREIGN", "TABLE", MatchAny) || + Matches("DROP", "FOREIGN", "CATALOG|VOLUME", MatchAny) || + Matches("DROP", "ICEBERG", "TABLE", MatchAny) || Matches("DROP", "DIRECTORY", "TABLE", MatchAny) || Matches("DROP", "TEXT", "SEARCH", "CONFIGURATION|DICTIONARY|PARSER|TEMPLATE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); @@ -3854,7 +3854,15 @@ psql_completion(const char *text, int start, int end) else if (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, "(")) COMPLETE_WITH_FUNCTION_ARG(prev2_wd); else if (Matches("DROP", "FOREIGN")) - COMPLETE_WITH("DATA WRAPPER", "TABLE"); + COMPLETE_WITH("CATALOG", "DATA WRAPPER", "TABLE", "VOLUME"); + else if (Matches("DROP", "FOREIGN", "CATALOG")) + COMPLETE_WITH_QUERY(Query_for_list_of_foreign_catalogs); + else if (Matches("DROP", "FOREIGN", "VOLUME")) + COMPLETE_WITH_QUERY(Query_for_list_of_foreign_volumes); + else if (Matches("DROP", "ICEBERG")) + COMPLETE_WITH("TABLE"); + else if (Matches("DROP", "ICEBERG", "TABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); else if (Matches("DROP", "DATABASE", MatchAny)) COMPLETE_WITH("WITH ("); else if (HeadMatches("DROP", "DATABASE") && (ends_with(prev_wd, '('))) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index bac420b1d58..5ebc3142d4b 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3832,6 +3832,7 @@ typedef struct DropStmt bool missing_ok; /* skip error if object is missing? */ bool concurrent; /* drop index concurrently? */ bool isdynamic; /* drop a dynamic table? */ + bool isiceberg; /* drop an iceberg (lake) table? */ } DropStmt; /* ---------------------- diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index fec05c924ad..66684f8d6f9 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -185,6 +185,7 @@ PG_CMDTAG(CMDTAG_DROP_FOREIGN_TABLE, "DROP FOREIGN TABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_FOREIGN_VOLUME, "DROP FOREIGN VOLUME", true, false, false) PG_CMDTAG(CMDTAG_DROP_FUNCTION, "DROP FUNCTION", true, false, false) PG_CMDTAG(CMDTAG_DROP_INDEX, "DROP INDEX", true, false, false) +PG_CMDTAG(CMDTAG_DROP_LAKE_TABLE, "DROP LAKE TABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_LANGUAGE, "DROP LANGUAGE", true, false, false) PG_CMDTAG(CMDTAG_DROP_MATERIALIZED_VIEW, "DROP MATERIALIZED VIEW", true, false, false) PG_CMDTAG(CMDTAG_DROP_OPERATOR, "DROP OPERATOR", true, false, false) diff --git a/src/test/regress/expected/lake_table.out b/src/test/regress/expected/lake_table.out index a97e854c27c..b28d6a185bb 100644 --- a/src/test/regress/expected/lake_table.out +++ b/src/test/regress/expected/lake_table.out @@ -225,9 +225,9 @@ HINT: Lake tables must use DISTRIBUTED RANDOMLY because data is stored on objec CREATE ROLE regress_lake_user; NOTICE: resource queue required -- using default resource queue "pg_default" SET ROLE regress_lake_user; -DROP CATALOG lake_test_cat; -- fail, not owner +DROP FOREIGN CATALOG lake_test_cat; -- fail, not owner ERROR: must be owner of foreign catalog lake_test_cat -DROP VOLUME lake_test_vol; -- fail, not owner +DROP FOREIGN VOLUME lake_test_vol; -- fail, not owner ERROR: must be owner of foreign volume lake_test_vol RESET ROLE; -- Dependencies: the server holds the catalog/volume, which hold the tables @@ -240,7 +240,7 @@ table lake_test_t2 depends on volume lake_test_vol table lake_test_t3 depends on volume lake_test_vol table lake_test_t4 depends on volume lake_test_vol HINT: Use DROP ... CASCADE to drop the dependent objects too. -DROP CATALOG lake_test_cat; -- fail, tables depend on it +DROP FOREIGN CATALOG lake_test_cat; -- fail, tables depend on it ERROR: cannot drop catalog lake_test_cat because other objects depend on it DETAIL: table lake_test_t1 depends on catalog lake_test_cat table lake_test_t2 depends on catalog lake_test_cat @@ -248,7 +248,7 @@ table lake_test_t3 depends on catalog lake_test_cat table lake_test_t4 depends on catalog lake_test_cat HINT: Use DROP ... CASCADE to drop the dependent objects too. -- Dropping a lake table removes its pg_lake_table entry -DROP TABLE lake_test_t1; +DROP ICEBERG TABLE lake_test_t1; SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid WHERE c.relname = 'lake_test_t1'; count @@ -256,11 +256,23 @@ SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid 0 (1 row) --- DROP CATALOG ... CASCADE takes the remaining tables with it -DROP CATALOG lake_test_cat CASCADE; -NOTICE: drop cascades to 3 other objects -DETAIL: drop cascades to table lake_test_t2 -drop cascades to table lake_test_t3 +-- DROP ICEBERG TABLE rejects a non-iceberg table ... +DROP ICEBERG TABLE lake_test_heap; -- fail, not an iceberg table +ERROR: "lake_test_heap" is not an iceberg table +HINT: Use DROP TABLE to remove a table. +-- ... while DROP TABLE still removes an iceberg table (no reverse restriction) +DROP TABLE lake_test_t2; +SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid + WHERE c.relname = 'lake_test_t2'; + count +------- + 0 +(1 row) + +-- DROP FOREIGN CATALOG ... CASCADE takes the remaining tables with it +DROP FOREIGN CATALOG lake_test_cat CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table lake_test_t3 drop cascades to table lake_test_t4 SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid WHERE c.relname LIKE 'lake\_test%'; @@ -270,14 +282,14 @@ SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid (1 row) -- DROP variants -DROP CATALOG lake_test_cat; -- fail, already gone +DROP FOREIGN CATALOG lake_test_cat; -- fail, already gone ERROR: foreign catalog "lake_test_cat" does not exist -DROP CATALOG IF EXISTS lake_test_cat; -- skip with notice +DROP FOREIGN CATALOG IF EXISTS lake_test_cat; -- skip with notice NOTICE: foreign catalog "lake_test_cat" does not exist, skipping -DROP VOLUME lake_test_vol; -DROP VOLUME lake_test_vol; -- fail, already gone +DROP FOREIGN VOLUME lake_test_vol; +DROP FOREIGN VOLUME lake_test_vol; -- fail, already gone ERROR: foreign volume "lake_test_vol" does not exist -DROP VOLUME IF EXISTS lake_test_vol; -- skip with notice +DROP FOREIGN VOLUME IF EXISTS lake_test_vol; -- skip with notice NOTICE: foreign volume "lake_test_vol" does not exist, skipping -- Cleanup DROP TABLE lake_test_heap; diff --git a/src/test/regress/sql/lake_table.sql b/src/test/regress/sql/lake_table.sql index f9683baf604..623f39d841f 100644 --- a/src/test/regress/sql/lake_table.sql +++ b/src/test/regress/sql/lake_table.sql @@ -108,30 +108,37 @@ ALTER TABLE lake_test_t1 SET DISTRIBUTED BY (a); -- fail -- Only the owner can drop a catalog or volume CREATE ROLE regress_lake_user; SET ROLE regress_lake_user; -DROP CATALOG lake_test_cat; -- fail, not owner -DROP VOLUME lake_test_vol; -- fail, not owner +DROP FOREIGN CATALOG lake_test_cat; -- fail, not owner +DROP FOREIGN VOLUME lake_test_vol; -- fail, not owner RESET ROLE; -- Dependencies: the server holds the catalog/volume, which hold the tables DROP SERVER lake_test_srv; -- fail, catalog and volume depend on it -DROP CATALOG lake_test_cat; -- fail, tables depend on it +DROP FOREIGN CATALOG lake_test_cat; -- fail, tables depend on it -- Dropping a lake table removes its pg_lake_table entry -DROP TABLE lake_test_t1; +DROP ICEBERG TABLE lake_test_t1; SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid WHERE c.relname = 'lake_test_t1'; --- DROP CATALOG ... CASCADE takes the remaining tables with it -DROP CATALOG lake_test_cat CASCADE; +-- DROP ICEBERG TABLE rejects a non-iceberg table ... +DROP ICEBERG TABLE lake_test_heap; -- fail, not an iceberg table +-- ... while DROP TABLE still removes an iceberg table (no reverse restriction) +DROP TABLE lake_test_t2; +SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid + WHERE c.relname = 'lake_test_t2'; + +-- DROP FOREIGN CATALOG ... CASCADE takes the remaining tables with it +DROP FOREIGN CATALOG lake_test_cat CASCADE; SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid WHERE c.relname LIKE 'lake\_test%'; -- DROP variants -DROP CATALOG lake_test_cat; -- fail, already gone -DROP CATALOG IF EXISTS lake_test_cat; -- skip with notice -DROP VOLUME lake_test_vol; -DROP VOLUME lake_test_vol; -- fail, already gone -DROP VOLUME IF EXISTS lake_test_vol; -- skip with notice +DROP FOREIGN CATALOG lake_test_cat; -- fail, already gone +DROP FOREIGN CATALOG IF EXISTS lake_test_cat; -- skip with notice +DROP FOREIGN VOLUME lake_test_vol; +DROP FOREIGN VOLUME lake_test_vol; -- fail, already gone +DROP FOREIGN VOLUME IF EXISTS lake_test_vol; -- skip with notice -- Cleanup DROP TABLE lake_test_heap;