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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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; From f21763c6f90d8f24bd8e438a15341e2e7e0ca86b Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Fri, 10 Jul 2026 11:38:21 +0800 Subject: [PATCH 12/18] commands: make foreign catalog type a required first-class column Promote the catalog type from a free-form OPTION to a required TYPE clause backed by a new pg_foreign_catalog.fctype column, per review on PR #1842. Every foreign catalog has a type (hive, hdfs, polaris, ...), so it is a property rather than an option; the value is stored verbatim as an open string and validated by the datalake provider, keeping the kernel provider-agnostic. Bumps CATALOG_VERSION_NO for the new column. --- src/backend/commands/foreigncmds.c | 13 ++++++++++++ src/backend/nodes/copyfuncs.funcs.c | 1 + src/backend/nodes/equalfuncs.c | 1 + src/backend/nodes/outfast.c | 1 + src/backend/nodes/readfast.c | 1 + src/backend/parser/gram.y | 10 +++++---- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_foreign_catalog.h | 1 + src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/lake_table.out | 27 ++++++++++++++---------- src/test/regress/sql/lake_table.sql | 17 ++++++++------- 11 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index b5e6adc95f1..46a90b88ef7 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -1107,6 +1107,19 @@ CreateForeignCatalog(CreateForeignCatalogStmt *stmt) values[Anum_pg_foreign_catalog_fcowner - 1] = ObjectIdGetDatum(ownerId); values[Anum_pg_foreign_catalog_fcserver - 1] = ObjectIdGetDatum(server->serverid); + /* + * The catalog type is a required property (every catalog has a type such + * as 'hive', 'hdfs', ...). The grammar enforces the TYPE clause, so this + * is just a defensive check; the value is stored verbatim and validated by + * the datalake provider rather than the kernel. + */ + if (stmt->catalogtype == NULL || stmt->catalogtype[0] == '\0') + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("foreign catalog type cannot be empty"))); + values[Anum_pg_foreign_catalog_fctype - 1] = + CStringGetTextDatum(stmt->catalogtype); + /* Add catalog options; there is no validator for them */ catalogoptions = transformGenericOptions(ForeignCatalogRelationId, PointerGetDatum(NULL), diff --git a/src/backend/nodes/copyfuncs.funcs.c b/src/backend/nodes/copyfuncs.funcs.c index 81fbaa2330b..d6597996a99 100644 --- a/src/backend/nodes/copyfuncs.funcs.c +++ b/src/backend/nodes/copyfuncs.funcs.c @@ -2755,6 +2755,7 @@ _copyCreateForeignCatalogStmt(const CreateForeignCatalogStmt *from) COPY_STRING_FIELD(catalogname); COPY_STRING_FIELD(servername); + COPY_STRING_FIELD(catalogtype); COPY_SCALAR_FIELD(if_not_exists); COPY_NODE_FIELD(options); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 107b762ae0e..0150a714f65 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2169,6 +2169,7 @@ _equalCreateForeignCatalogStmt(const CreateForeignCatalogStmt *a, const CreateFo { COMPARE_STRING_FIELD(catalogname); COMPARE_STRING_FIELD(servername); + COMPARE_STRING_FIELD(catalogtype); COMPARE_SCALAR_FIELD(if_not_exists); COMPARE_NODE_FIELD(options); diff --git a/src/backend/nodes/outfast.c b/src/backend/nodes/outfast.c index 42907eccab3..90d72d34559 100644 --- a/src/backend/nodes/outfast.c +++ b/src/backend/nodes/outfast.c @@ -680,6 +680,7 @@ _outCreateForeignCatalogStmt(StringInfo str, CreateForeignCatalogStmt *node) WRITE_STRING_FIELD(catalogname); WRITE_STRING_FIELD(servername); + WRITE_STRING_FIELD(catalogtype); WRITE_BOOL_FIELD(if_not_exists); WRITE_NODE_FIELD(options); } diff --git a/src/backend/nodes/readfast.c b/src/backend/nodes/readfast.c index adb7d9e4811..b9fbff9226e 100644 --- a/src/backend/nodes/readfast.c +++ b/src/backend/nodes/readfast.c @@ -1605,6 +1605,7 @@ _readCreateForeignCatalogStmt(void) READ_STRING_FIELD(catalogname); READ_STRING_FIELD(servername); + READ_STRING_FIELD(catalogtype); READ_BOOL_FIELD(if_not_exists); READ_NODE_FIELD(options); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 39f95ab3842..fc28d5ce5bf 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -9106,21 +9106,23 @@ CreateDirectoryTableStmt: *****************************************************************************/ CreateForeignCatalogStmt: - CREATE FOREIGN CATALOG_P name SERVER name create_generic_options + CREATE FOREIGN CATALOG_P name SERVER name TYPE_P Sconst create_generic_options { CreateForeignCatalogStmt *n = makeNode(CreateForeignCatalogStmt); n->catalogname = $4; n->servername = $6; - n->options = $7; + n->catalogtype = $8; + n->options = $9; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE FOREIGN CATALOG_P IF_P NOT EXISTS name SERVER name create_generic_options + | CREATE FOREIGN CATALOG_P IF_P NOT EXISTS name SERVER name TYPE_P Sconst create_generic_options { CreateForeignCatalogStmt *n = makeNode(CreateForeignCatalogStmt); n->catalogname = $7; n->servername = $9; - n->options = $10; + n->catalogtype = $11; + n->options = $12; n->if_not_exists = true; $$ = (Node *) n; } diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 3d5e3915585..53258aca56e 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -60,6 +60,6 @@ */ /* 3yyymmddN */ -#define CATALOG_VERSION_NO 302607021 +#define CATALOG_VERSION_NO 302607101 #endif diff --git a/src/include/catalog/pg_foreign_catalog.h b/src/include/catalog/pg_foreign_catalog.h index b6325915b22..547f01c6a9c 100644 --- a/src/include/catalog/pg_foreign_catalog.h +++ b/src/include/catalog/pg_foreign_catalog.h @@ -50,6 +50,7 @@ CATALOG(pg_foreign_catalog,8549,ForeignCatalogRelationId) Oid fcserver BKI_LOOKUP(pg_foreign_server); /* foreign server this catalog belongs to */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ + text fctype BKI_FORCE_NOT_NULL; /* catalog type, e.g. 'hive' */ text fcoptions[1]; /* foreign catalog options */ #endif } FormData_pg_foreign_catalog; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 5ebc3142d4b..8f4b9aa5c27 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3278,6 +3278,7 @@ typedef struct CreateForeignCatalogStmt NodeTag type; char *catalogname; /* foreign catalog name */ char *servername; /* server name */ + char *catalogtype; /* catalog type, e.g. 'hive' */ bool if_not_exists; /* just do nothing if it already exists? */ List *options; /* generic options to catalog */ } CreateForeignCatalogStmt; diff --git a/src/test/regress/expected/lake_table.out b/src/test/regress/expected/lake_table.out index b28d6a185bb..b2baf847936 100644 --- a/src/test/regress/expected/lake_table.out +++ b/src/test/regress/expected/lake_table.out @@ -10,6 +10,7 @@ fcname | name | | not null | | plain | | fcowner | oid | | not null | | plain | | fcserver | oid | | not null | | plain | | + fctype | text | C | not null | | extended | | fcoptions | text[] | C | | | extended | | Indexes: "pg_foreign_catalog_oid_index" PRIMARY KEY, btree (oid) @@ -44,23 +45,27 @@ Indexes: 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: TYPE is a required first-class property +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv TYPE 'hive' OPTIONS (uri 'thrift://localhost:9083'); +CREATE FOREIGN CATALOG lake_test_notype SERVER lake_test_srv; -- fail, TYPE is required +ERROR: syntax error at or near ";" +LINE 1: ...REATE FOREIGN CATALOG lake_test_notype SERVER lake_test_srv; + ^ +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv TYPE 'hive'; -- 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 +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv TYPE 'hive'; -- 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 +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv2 TYPE 'hive'; -- 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 +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv2 TYPE 'hive'; -- 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 +CREATE FOREIGN CATALOG lake_test_bad SERVER no_such_server TYPE 'hive'; -- 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} +SELECT fcname, fctype, fcoptions FROM pg_foreign_catalog WHERE fcname LIKE 'lake\_test%' ORDER BY 1; + fcname | fctype | fcoptions +---------------+--------+------------------------------- + lake_test_cat | hive | {uri=thrift://localhost:9083} (1 row) -- CREATE FOREIGN VOLUME diff --git a/src/test/regress/sql/lake_table.sql b/src/test/regress/sql/lake_table.sql index 623f39d841f..6f9c7af2e7d 100644 --- a/src/test/regress/sql/lake_table.sql +++ b/src/test/regress/sql/lake_table.sql @@ -12,15 +12,16 @@ 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 +-- CREATE FOREIGN CATALOG: TYPE is a required first-class property +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv TYPE 'hive' OPTIONS (uri 'thrift://localhost:9083'); +CREATE FOREIGN CATALOG lake_test_notype SERVER lake_test_srv; -- fail, TYPE is required +CREATE FOREIGN CATALOG lake_test_cat SERVER lake_test_srv TYPE 'hive'; -- fail, duplicate +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv TYPE 'hive'; -- 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 CATALOG lake_test_cat SERVER lake_test_srv2 TYPE 'hive'; -- fail, duplicate +CREATE FOREIGN CATALOG IF NOT EXISTS lake_test_cat SERVER lake_test_srv2 TYPE 'hive'; -- skip with notice +CREATE FOREIGN CATALOG lake_test_bad SERVER no_such_server TYPE 'hive'; -- fail, no server +SELECT fcname, fctype, 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'); From 7e6e7ca21fd049b9a9a7dc813afa9a444ee38d64 Mon Sep 17 00:00:00 2001 From: liuxiaoyu <45345701+MisterRaindrop@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:51:26 +0800 Subject: [PATCH 13/18] Update src/backend/foreign/foreign.c Co-authored-by: Andrey Sokolov --- src/backend/foreign/foreign.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 331ef9bc40b..aff82399f59 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -1066,10 +1066,7 @@ GetForeignVolumeByName(const char *volumename, bool missing_ok) tp, Anum_pg_foreign_volume_fvoptions, &isnull); - if (isnull) - volume->options = NIL; - else - volume->options = untransformRelOptions(datum); + volume->options = (isnull) ? NIL : untransformRelOptions(datum); ReleaseSysCache(tp); From 70b451aab4dd17576199738301ad9ed0c83198b8 Mon Sep 17 00:00:00 2001 From: liuxiaoyu <45345701+MisterRaindrop@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:52:02 +0800 Subject: [PATCH 14/18] Update src/include/commands/laketablecmds.h Co-authored-by: Andrey Sokolov --- src/include/commands/laketablecmds.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/include/commands/laketablecmds.h b/src/include/commands/laketablecmds.h index 3b7b949a7fa..980d25784d8 100644 --- a/src/include/commands/laketablecmds.h +++ b/src/include/commands/laketablecmds.h @@ -54,7 +54,7 @@ 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 ValidateLakeTableStmt(CreateLakeTableStmt *stmt); extern void CreateLakeTable(CreateLakeTableStmt *stmt, Oid relId); extern void RemoveLakeTableEntry(Oid relid); From 32bda146412fc27bc92fbf90b441a160d4a6b9d4 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Fri, 10 Jul 2026 20:37:08 +0800 Subject: [PATCH 15/18] psql: complete only iceberg tables for DROP ICEBERG TABLE Tab completion for DROP ICEBERG TABLE previously offered every ordinary table. Add Query_for_list_of_iceberg_tables, which filters to relations whose access method is the iceberg AM -- the same rule the backend validates DROP ICEBERG TABLE against -- so completion only suggests tables the command will actually accept. When no provider installed the iceberg AM the scalar subquery is NULL and the list is empty. Addresses review on PR #1842. --- src/bin/psql/tab-complete.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 2235f292804..ead1098f979 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -678,6 +678,23 @@ static const SchemaQuery Query_for_list_of_tables = { .result = "c.relname", }; +/* + * An iceberg (lake) table is an ordinary relation whose access method is the + * "iceberg" AM -- the same rule DROP ICEBERG TABLE validates against. When no + * provider installed that AM the scalar subquery yields NULL and the list is + * empty, which is correct (no relation can be an iceberg table). + */ +static const SchemaQuery Query_for_list_of_iceberg_tables = { + .catname = "pg_catalog.pg_class c", + .selcondition = + "c.relkind IN (" CppAsString2(RELKIND_RELATION) ") AND " + "c.relam = (SELECT oid FROM pg_catalog.pg_am " + "WHERE amname = 'iceberg' AND amtype = 't')", + .viscondition = "pg_catalog.pg_table_is_visible(c.oid)", + .namespace = "c.relnamespace", + .result = "c.relname", +}; + static const SchemaQuery Query_for_list_of_directory_tables = { .catname = "pg_catalog.pg_class c", .selcondition = "c.relkind IN (" CppAsString2(RELKIND_DIRECTORY_TABLE) ")", @@ -1290,7 +1307,7 @@ static const pgsql_thing_t words_after_create[] = { {"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, &Query_for_list_of_tables, NULL, THING_NO_ALTER}, + {"ICEBERG TABLE", NULL, NULL, &Query_for_list_of_iceberg_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}, @@ -3862,7 +3879,7 @@ psql_completion(const char *text, int start, int end) else if (Matches("DROP", "ICEBERG")) COMPLETE_WITH("TABLE"); else if (Matches("DROP", "ICEBERG", "TABLE")) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_iceberg_tables); else if (Matches("DROP", "DATABASE", MatchAny)) COMPLETE_WITH("WITH ("); else if (HeadMatches("DROP", "DATABASE") && (ends_with(prev_wd, '('))) From f6d32869e76b44b0310ab663ff4076413b86f434 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Fri, 10 Jul 2026 20:46:26 +0800 Subject: [PATCH 16/18] commands: finish ValidateLakeTableOptions -> ValidateLakeTableStmt rename The header prototype was renamed to ValidateLakeTableStmt but the definition and its caller still used the old name, breaking the build (-Werror=missing-prototypes). Rename the definition, caller and comments to match. --- src/backend/commands/laketablecmds.c | 6 +++--- src/backend/tcop/utility.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/commands/laketablecmds.c b/src/backend/commands/laketablecmds.c index 90cfb6fa4b5..d5c9274b049 100644 --- a/src/backend/commands/laketablecmds.c +++ b/src/backend/commands/laketablecmds.c @@ -268,7 +268,7 @@ validate_foreign_volume(const char *volume_name) * 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 + * Also exposed (via ValidateLakeTableStmt) 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. @@ -361,12 +361,12 @@ ResolveLakeTableOptions(CreateLakeTableStmt *stmt, } /* - * ValidateLakeTableOptions + * ValidateLakeTableStmt * * QD-side pre-DefineRelation validation wrapper; see ResolveLakeTableOptions. */ void -ValidateLakeTableOptions(CreateLakeTableStmt *stmt) +ValidateLakeTableStmt(CreateLakeTableStmt *stmt) { Oid catalog_oid; Oid volume_oid; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 3795a189072..530078e0799 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -1677,7 +1677,7 @@ ProcessUtilitySlow(ParseState *pstate, * QE-annotated error. */ if (Gp_role == GP_ROLE_DISPATCH) - ValidateLakeTableOptions(cstmt); + ValidateLakeTableStmt(cstmt); /* * Create the table itself. Dispatch manually From afe4142ba7f061c2dec7f5f57c33525f957b9ce6 Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Tue, 14 Jul 2026 10:41:00 +0800 Subject: [PATCH 17/18] commands: reject plain DROP TABLE on an iceberg table Iceberg (lake) tables share RELKIND_RELATION with ordinary tables, so plain DROP TABLE used to drop them and they appeared in DROP TABLE tab completion -- unlike foreign tables. Align them with the foreign-table behavior: - RangeVarCallbackForDropRelation() now rejects an iceberg table on a non-iceberg DROP TABLE with errhint "Use DROP ICEBERG TABLE to remove an iceberg table" (fires under IF EXISTS too); DROP ICEBERG TABLE still rejects ordinary tables. - psql's Query_for_list_of_tables excludes iceberg tables via NOT EXISTS, so ordinary tables stay listed even when no iceberg AM is installed. DROP ICEBERG TABLE continues to complete iceberg tables via its own query. --- src/backend/commands/tablecmds.c | 30 +++++++++++++++++------- src/bin/psql/tab-complete.c | 9 ++++++- src/test/regress/expected/lake_table.out | 16 ++++++++++++- src/test/regress/sql/lake_table.sql | 7 +++++- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb63d10a8fa..641bb653a22 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -2222,21 +2222,33 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid, 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. + * Iceberg (lake) tables share RELKIND_RELATION with ordinary tables and are + * told apart only by their access method. DROP ICEBERG TABLE must target one; + * plain DROP TABLE must NOT (mirrors the foreign-table rule) -- direct the user + * to the matching command in each case. */ - if (state->iceberg_only) + if (state->expected_relkind == RELKIND_RELATION) { Oid iceberg_amoid = GetIcebergTableAmOid(true); + bool is_iceberg = classform->relkind == RELKIND_RELATION && + OidIsValid(iceberg_amoid) && + classform->relam == iceberg_amoid; - if (!OidIsValid(iceberg_amoid) || classform->relam != iceberg_amoid) + if (state->iceberg_only) + { + if (!is_iceberg) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an iceberg table", rel->relname), + errhint("Use DROP TABLE to remove a table."))); + } + else if (is_iceberg) + { ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not an iceberg table", rel->relname), - errhint("Use DROP TABLE to remove a table."))); + errmsg("\"%s\" is not a table", rel->relname), + errhint("Use DROP ICEBERG TABLE to remove an iceberg table."))); + } } /* Allow DROP to either table owner or schema owner */ diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index ead1098f979..b7c83596f79 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -668,11 +668,18 @@ static const SchemaQuery Query_for_list_of_foreign_tables = { .result = "c.relname", }; +/* + * Exclude iceberg tables; Query_for_list_of_iceberg_tables serves DROP ICEBERG + * TABLE. With no iceberg AM, the NOT EXISTS subquery finds no match, so + * ordinary tables remain listed. + */ static const SchemaQuery Query_for_list_of_tables = { .catname = "pg_catalog.pg_class c", .selcondition = "c.relkind IN (" CppAsString2(RELKIND_RELATION) ", " - CppAsString2(RELKIND_PARTITIONED_TABLE) ")", + CppAsString2(RELKIND_PARTITIONED_TABLE) ") AND " + "NOT EXISTS (SELECT 1 FROM pg_catalog.pg_am a " + "WHERE a.oid = c.relam AND a.amname = 'iceberg' AND a.amtype = 't')", .viscondition = "pg_catalog.pg_table_is_visible(c.oid)", .namespace = "c.relnamespace", .result = "c.relname", diff --git a/src/test/regress/expected/lake_table.out b/src/test/regress/expected/lake_table.out index b2baf847936..b3ca57ea1d2 100644 --- a/src/test/regress/expected/lake_table.out +++ b/src/test/regress/expected/lake_table.out @@ -265,8 +265,22 @@ SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid 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) +-- plain DROP TABLE must reject an iceberg table (mirrors foreign-table behavior) DROP TABLE lake_test_t2; +ERROR: "lake_test_t2" is not a table +HINT: Use DROP ICEBERG TABLE to remove an iceberg table. +DROP TABLE IF EXISTS lake_test_t2; -- IF EXISTS does not suppress wrong-type errors +ERROR: "lake_test_t2" is not a table +HINT: Use DROP ICEBERG TABLE to remove an iceberg table. +-- the rejected drops must have left the table and its lake metadata intact +SELECT count(*) FROM pg_lake_table WHERE ltrelid = 'lake_test_t2'::regclass; + count +------- + 1 +(1 row) + +-- the correct command still works +DROP ICEBERG 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 diff --git a/src/test/regress/sql/lake_table.sql b/src/test/regress/sql/lake_table.sql index 6f9c7af2e7d..3cfc4173a74 100644 --- a/src/test/regress/sql/lake_table.sql +++ b/src/test/regress/sql/lake_table.sql @@ -124,8 +124,13 @@ SELECT count(*) FROM pg_lake_table lt JOIN pg_class c ON c.oid = lt.ltrelid -- 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) +-- plain DROP TABLE must reject an iceberg table (mirrors foreign-table behavior) DROP TABLE lake_test_t2; +DROP TABLE IF EXISTS lake_test_t2; -- IF EXISTS does not suppress wrong-type errors +-- the rejected drops must have left the table and its lake metadata intact +SELECT count(*) FROM pg_lake_table WHERE ltrelid = 'lake_test_t2'::regclass; +-- the correct command still works +DROP ICEBERG 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'; From 9e4e0c206382305f2ba0c5b71e976a304ede480f Mon Sep 17 00:00:00 2001 From: MisterRaindrop <278811821@qq.com> Date: Tue, 14 Jul 2026 10:41:11 +0800 Subject: [PATCH 18/18] doc: add reference pages for the iceberg lake-table DDL commands Add SGML ref pages for CREATE/DROP FOREIGN CATALOG, CREATE/DROP FOREIGN VOLUME and CREATE/DROP ICEBERG TABLE, and register them in allfiles.sgml and reference.sgml. This makes "\h " in psql show their synopsis and adds them to the reference manual. sql_help.{c,h} are generated from these by create_help.pl, so they are not edited by hand. --- doc/src/sgml/ref/allfiles.sgml | 6 + doc/src/sgml/ref/create_foreign_catalog.sgml | 144 ++++++++++++++ doc/src/sgml/ref/create_foreign_volume.sgml | 133 +++++++++++++ doc/src/sgml/ref/create_iceberg_table.sgml | 193 +++++++++++++++++++ doc/src/sgml/ref/drop_foreign_catalog.sgml | 117 +++++++++++ doc/src/sgml/ref/drop_foreign_volume.sgml | 117 +++++++++++ doc/src/sgml/ref/drop_iceberg_table.sgml | 119 ++++++++++++ doc/src/sgml/reference.sgml | 6 + 8 files changed, 835 insertions(+) create mode 100644 doc/src/sgml/ref/create_foreign_catalog.sgml create mode 100644 doc/src/sgml/ref/create_foreign_volume.sgml create mode 100644 doc/src/sgml/ref/create_iceberg_table.sgml create mode 100644 doc/src/sgml/ref/drop_foreign_catalog.sgml create mode 100644 doc/src/sgml/ref/drop_foreign_volume.sgml create mode 100644 doc/src/sgml/ref/drop_iceberg_table.sgml diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 3d47bff7fef..41a275686a1 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -69,10 +69,13 @@ Complete list of usable sgml source files in this directory. + + + @@ -118,10 +121,13 @@ Complete list of usable sgml source files in this directory. + + + diff --git a/doc/src/sgml/ref/create_foreign_catalog.sgml b/doc/src/sgml/ref/create_foreign_catalog.sgml new file mode 100644 index 00000000000..ee32abf6b47 --- /dev/null +++ b/doc/src/sgml/ref/create_foreign_catalog.sgml @@ -0,0 +1,144 @@ + + + + + CREATE FOREIGN CATALOG + + + + CREATE FOREIGN CATALOG + 7 + SQL - Language Statements + + + + CREATE FOREIGN CATALOG + define a new foreign catalog + + + + +CREATE FOREIGN CATALOG [ IF NOT EXISTS ] catalog_name + SERVER server_name + TYPE 'catalog_type' + [ OPTIONS ( option 'value' [, ...] ) ] + + + + + Description + + + CREATE FOREIGN CATALOG defines a new foreign catalog. + Foreign catalog names are global within a database. The user who creates + the catalog becomes its owner. + + + + Creating a foreign catalog requires USAGE privilege on + the referenced foreign server. + + + + The required catalog type is stored verbatim. Provider-specific validation + of the catalog type and options is outside the kernel, and generic options + currently have no kernel validator. + + + + + Parameters + + + + IF NOT EXISTS + + + Do not throw an error if a foreign catalog with the same name already + exists. A notice is issued in this case. + + + + + + catalog_name + + + The database-global name of the foreign catalog to be created. + + + + + + server_name + + + The name of an existing foreign server for the catalog. + + + + + + catalog_type + + + The required provider-specific catalog type. The value is stored + verbatim. + + + + + + option + + + The name of a provider-specific option for the catalog. + + + + + + value + + + The value of a provider-specific catalog option. + + + + + + + + Examples + + + Create a Hive catalog that uses the foreign server + hive_srv: + +CREATE FOREIGN CATALOG hive_cat SERVER hive_srv TYPE 'hive' OPTIONS (uri 'thrift://localhost:9083'); + + + + + Compatibility + + + CREATE FOREIGN CATALOG is an + Apache Cloudberry extension and is not defined in + the SQL standard. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/create_foreign_volume.sgml b/doc/src/sgml/ref/create_foreign_volume.sgml new file mode 100644 index 00000000000..7e81a4c134f --- /dev/null +++ b/doc/src/sgml/ref/create_foreign_volume.sgml @@ -0,0 +1,133 @@ + + + + + CREATE FOREIGN VOLUME + + + + CREATE FOREIGN VOLUME + 7 + SQL - Language Statements + + + + CREATE FOREIGN VOLUME + define a new foreign volume + + + + +CREATE FOREIGN VOLUME [ IF NOT EXISTS ] volume_name + SERVER server_name + [ OPTIONS ( option 'value' [, ...] ) ] + + + + + Description + + + CREATE FOREIGN VOLUME defines a new foreign volume. + Foreign volume names are global within a database. The user who creates + the volume becomes its owner. + + + + Creating a foreign volume requires USAGE privilege on + the referenced foreign server. + + + + Volume options are provider-specific. Provider-specific option validation + is outside the kernel, and generic options currently have no kernel + validator. + + + + + Parameters + + + + IF NOT EXISTS + + + Do not throw an error if a foreign volume with the same name already + exists. A notice is issued in this case. + + + + + + volume_name + + + The database-global name of the foreign volume to be created. + + + + + + server_name + + + The name of an existing foreign server for the volume. + + + + + + option + + + The name of a provider-specific option for the volume. + + + + + + value + + + The value of a provider-specific volume option. + + + + + + + + Examples + + + Create a volume for an object storage prefix using the foreign server + s3_srv: + +CREATE FOREIGN VOLUME s3_vol SERVER s3_srv OPTIONS (path 's3://bucket/prefix'); + + + + + Compatibility + + + CREATE FOREIGN VOLUME is an + Apache Cloudberry extension and is not defined in + the SQL standard. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/create_iceberg_table.sgml b/doc/src/sgml/ref/create_iceberg_table.sgml new file mode 100644 index 00000000000..dcff972e45b --- /dev/null +++ b/doc/src/sgml/ref/create_iceberg_table.sgml @@ -0,0 +1,193 @@ + + + + + CREATE ICEBERG TABLE + + + + CREATE ICEBERG TABLE + 7 + SQL - Language Statements + + + + CREATE ICEBERG TABLE + define a new Iceberg table + + + + +CREATE ICEBERG TABLE [ IF NOT EXISTS ] table_name ( [ + column_name data_type [, ... ] + ] ) + [ CATALOG catalog_name ] + [ VOLUME volume_name ] + [ OPTIONS ( option 'value' [, ...] ) ] + [ DISTRIBUTED BY ( column [, ... ] ) | DISTRIBUTED RANDOMLY | DISTRIBUTED REPLICATED ] + [ USING access_method ] + + + + + Description + + + CREATE ICEBERG TABLE defines a new Iceberg table. The + parenthesized element list is required, but it can be empty. The command + requires an extension that provides the iceberg table + access method. + + + + The foreign catalog and volume can be specified by the + CATALOG and VOLUME clauses. If either + clause is omitted, the corresponding iceberg_default_catalog + or iceberg_default_volume configuration parameter is used. + + + + Iceberg tables are always distributed randomly. A + DISTRIBUTED clause is accepted, but a warning is issued + and DISTRIBUTED RANDOMLY is used. The optional + USING clause accepts only iceberg and + is normally omitted. + + + + + Parameters + + + + IF NOT EXISTS + + + Do not throw an error if a relation with the same name already exists. + A notice is issued in this case. + + + + + + table_name + + + The name, optionally schema-qualified, of the Iceberg table to be + created. + + + + + + column_name + + + The name of a column in the new table. + + + + + + data_type + + + The data type of a column in the new table. + + + + + + catalog_name + + + The name of the foreign catalog to use. If omitted, the value of + iceberg_default_catalog is used. + + + + + + volume_name + + + The name of the foreign volume to use. If omitted, the value of + iceberg_default_volume is used. + + + + + + option + + + The name of an option for the Iceberg table. + + + + + + value + + + The value of an Iceberg table option. + + + + + + column + + + A column named in a DISTRIBUTED BY clause. The clause + is accepted with a warning; the table is distributed randomly instead. + + + + + + access_method + + + The table access method. The only accepted value is + iceberg. + + + + + + + + Examples + + + Create an Iceberg table using an explicit foreign catalog and volume: + +CREATE ICEBERG TABLE t (a int, b text) CATALOG hive_cat VOLUME s3_vol OPTIONS (fileformat 'parquet'); + + + + + Compatibility + + + CREATE ICEBERG TABLE is an + Apache Cloudberry extension and is not defined in + the SQL standard. + + + + + See Also + + + + + + + + + diff --git a/doc/src/sgml/ref/drop_foreign_catalog.sgml b/doc/src/sgml/ref/drop_foreign_catalog.sgml new file mode 100644 index 00000000000..330425abb61 --- /dev/null +++ b/doc/src/sgml/ref/drop_foreign_catalog.sgml @@ -0,0 +1,117 @@ + + + + + DROP FOREIGN CATALOG + + + + DROP FOREIGN CATALOG + 7 + SQL - Language Statements + + + + DROP FOREIGN CATALOG + remove a foreign catalog + + + + +DROP FOREIGN CATALOG [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP FOREIGN CATALOG removes one or more foreign + catalogs. Foreign catalog names are global within a database. To execute + this command, the current user must own each catalog. + + + + Using CASCADE can also drop dependent Iceberg tables. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if a foreign catalog does not exist. A notice is + issued in this case. + + + + + + name + + + The database-global name of a foreign catalog to drop. + + + + + + CASCADE + + + Automatically drop objects that depend on the catalog, including + dependent Iceberg tables, and in turn all objects that depend on those + objects (see ). + + + + + + RESTRICT + + + Refuse to drop the catalog if any objects depend on it. This is the + default. + + + + + + + + Examples + + + Drop the foreign catalog hive_cat: + +DROP FOREIGN CATALOG hive_cat; + + + + + Compatibility + + + DROP FOREIGN CATALOG is an + Apache Cloudberry extension and is not defined in + the SQL standard. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/drop_foreign_volume.sgml b/doc/src/sgml/ref/drop_foreign_volume.sgml new file mode 100644 index 00000000000..d387708aec2 --- /dev/null +++ b/doc/src/sgml/ref/drop_foreign_volume.sgml @@ -0,0 +1,117 @@ + + + + + DROP FOREIGN VOLUME + + + + DROP FOREIGN VOLUME + 7 + SQL - Language Statements + + + + DROP FOREIGN VOLUME + remove a foreign volume + + + + +DROP FOREIGN VOLUME [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP FOREIGN VOLUME removes one or more foreign volumes. + Foreign volume names are global within a database. To execute this + command, the current user must own each volume. + + + + Using CASCADE can also drop dependent Iceberg tables. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if a foreign volume does not exist. A notice is + issued in this case. + + + + + + name + + + The database-global name of a foreign volume to drop. + + + + + + CASCADE + + + Automatically drop objects that depend on the volume, including + dependent Iceberg tables, and in turn all objects that depend on those + objects (see ). + + + + + + RESTRICT + + + Refuse to drop the volume if any objects depend on it. This is the + default. + + + + + + + + Examples + + + Drop the foreign volume s3_vol: + +DROP FOREIGN VOLUME s3_vol; + + + + + Compatibility + + + DROP FOREIGN VOLUME is an + Apache Cloudberry extension and is not defined in + the SQL standard. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/drop_iceberg_table.sgml b/doc/src/sgml/ref/drop_iceberg_table.sgml new file mode 100644 index 00000000000..caba9cd06c7 --- /dev/null +++ b/doc/src/sgml/ref/drop_iceberg_table.sgml @@ -0,0 +1,119 @@ + + + + + DROP ICEBERG TABLE + + + + DROP ICEBERG TABLE + 7 + SQL - Language Statements + + + + DROP ICEBERG TABLE + remove an Iceberg table + + + + +DROP ICEBERG TABLE [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP ICEBERG TABLE removes one or more Iceberg tables. + Only the owner of an Iceberg table can remove it. + + + + This command only drops Iceberg tables. Plain + DROP TABLE rejects Iceberg tables and is used for + ordinary tables. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the Iceberg table does not exist. A notice is + issued in this case. + + + + + + name + + + The name, optionally schema-qualified, of an Iceberg table to drop. + + + + + + CASCADE + + + Automatically drop objects that depend on the Iceberg table, and in + turn all objects that depend on those objects (see ). + + + + + + RESTRICT + + + Refuse to drop the Iceberg table if any objects depend on it. This is + the default. + + + + + + + + Examples + + + Drop the Iceberg table t: + +DROP ICEBERG TABLE t; + + + + + Compatibility + + + DROP ICEBERG TABLE is an + Apache Cloudberry extension and is not defined in + the SQL standard. + + + + + See Also + + + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index a251363388e..6c374160d73 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -96,10 +96,13 @@ &createDynamicTable; &createEventTrigger; &createExtension; + &createForeignCatalog; &createForeignDataWrapper; &createForeignTable; + &createForeignVolume; &createFunction; &createGroup; + &createIcebergTable; &createIndex; &createLanguage; &createMaterializedView; @@ -144,10 +147,13 @@ &dropDynamicTable; &dropEventTrigger; &dropExtension; + &dropForeignCatalog; &dropForeignDataWrapper; &dropForeignTable; + &dropForeignVolume; &dropFunction; &dropGroup; + &dropIcebergTable; &dropIndex; &dropLanguage; &dropMaterializedView;