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/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;
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/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/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/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/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/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..46a90b88ef7 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,275 @@ 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)
+ 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;
+ }
+
+ /*
+ * 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);
+
+ /*
+ * 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),
+ 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)
+ 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;
+ }
+
+ /*
+ * 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/laketablecmds.c b/src/backend/commands/laketablecmds.c
new file mode 100644
index 00000000000..d5c9274b049
--- /dev/null
+++ b/src/backend/commands/laketablecmds.c
@@ -0,0 +1,483 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ * laketablecmds.c
+ * lake table creation/manipulation commands
+ *
+ * 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 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.
+ */
+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);
+}
+
+/*
+ * ValidateLakeTableStmt
+ *
+ * QD-side pre-DefineRelation validation wrapper; see ResolveLakeTableOptions.
+ */
+void
+ValidateLakeTableStmt(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/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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b8b69336b89..641bb653a22 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"
@@ -255,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;
@@ -938,6 +940,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.
@@ -1979,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: */
@@ -2200,6 +2221,36 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
DropErrorMsgWrongType(rel->relname, classform->relkind,
state->expected_relkind);
+ /*
+ * 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->expected_relkind == RELKIND_RELATION)
+ {
+ Oid iceberg_amoid = GetIcebergTableAmOid(true);
+ bool is_iceberg = classform->relkind == RELKIND_RELATION &&
+ 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 a table", rel->relname),
+ errhint("Use DROP ICEBERG TABLE to remove an iceberg table.")));
+ }
+ }
+
/* Allow DROP to either table owner or schema owner */
if (!object_ownercheck(RelationRelationId, relOid, GetUserId()) &&
!object_ownercheck(NamespaceRelationId, classform->relnamespace, GetUserId()))
@@ -17059,6 +17110,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 +17118,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 +19829,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/foreign/foreign.c b/src/backend/foreign/foreign.c
index 0fadf562ee4..aff82399f59 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,95 @@ 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);
+ volume->options = (isnull) ? NIL : untransformRelOptions(datum);
+
+ ReleaseSysCache(tp);
+
+ return volume;
+}
+
/*
* Get a copy of an existing local path for a given join relation.
*
diff --git a/src/backend/nodes/copyfuncs.funcs.c b/src/backend/nodes/copyfuncs.funcs.c
index 6f312d17574..d6597996a99 100644
--- a/src/backend/nodes/copyfuncs.funcs.c
+++ b/src/backend/nodes/copyfuncs.funcs.c
@@ -2748,6 +2748,33 @@ _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_STRING_FIELD(catalogtype);
+ 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 +3385,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)
{
@@ -3381,6 +3447,7 @@ _copyDropStmt(const DropStmt *from)
COPY_SCALAR_FIELD(missing_ok);
COPY_SCALAR_FIELD(concurrent);
COPY_SCALAR_FIELD(isdynamic);
+ COPY_SCALAR_FIELD(isiceberg);
return newnode;
}
@@ -3396,6 +3463,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/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..0150a714f65 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;
}
@@ -2163,6 +2164,29 @@ _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_STRING_FIELD(catalogtype);
+ 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 +3510,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 +4285,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 +4646,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..90d72d34559 100644
--- a/src/backend/nodes/outfast.c
+++ b/src/backend/nodes/outfast.c
@@ -673,6 +673,29 @@ _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_STRING_FIELD(catalogtype);
+ 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 +1841,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 +1969,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/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/readfast.c b/src/backend/nodes/readfast.c
index ff3cb5eaddf..b9fbff9226e 100644
--- a/src/backend/nodes/readfast.c
+++ b/src/backend/nodes/readfast.c
@@ -1598,6 +1598,33 @@ _readCreateForeignServerStmt(void)
READ_DONE();
}
+static CreateForeignCatalogStmt *
+_readCreateForeignCatalogStmt(void)
+{
+ READ_LOCALS(CreateForeignCatalogStmt);
+
+ READ_STRING_FIELD(catalogname);
+ READ_STRING_FIELD(servername);
+ READ_STRING_FIELD(catalogtype);
+ 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 +1937,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 +2923,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 +3039,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/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 bc657554219..fc28d5ce5bf 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,151 @@ CreateDirectoryTableStmt:
}
;
+/*****************************************************************************
+ *
+ * QUERY:
+ * CREATE FOREIGN CATALOG name SERVER server_name OPTIONS (...)
+ *
+ *****************************************************************************/
+
+CreateForeignCatalogStmt:
+ CREATE FOREIGN CATALOG_P name SERVER name TYPE_P Sconst create_generic_options
+ {
+ CreateForeignCatalogStmt *n = makeNode(CreateForeignCatalogStmt);
+ n->catalogname = $4;
+ n->servername = $6;
+ n->catalogtype = $8;
+ n->options = $9;
+ n->if_not_exists = false;
+ $$ = (Node *) n;
+ }
+ | 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->catalogtype = $11;
+ n->options = $12;
+ 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:
@@ -10382,6 +10532,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 */
@@ -10429,6 +10604,8 @@ drop_type_name:
| PUBLICATION { $$ = OBJECT_PUBLICATION; }
| SCHEMA { $$ = OBJECT_SCHEMA; }
| SERVER { $$ = OBJECT_FOREIGN_SERVER; }
+ | FOREIGN CATALOG_P { $$ = OBJECT_FOREIGN_CATALOG; }
+ | FOREIGN VOLUME { $$ = OBJECT_FOREIGN_VOLUME; }
| PROTOCOL { $$ = OBJECT_EXTPROTOCOL; }
;
@@ -21175,6 +21352,7 @@ unreserved_keyword:
| HOLD
| HOST
| HOUR_P
+ | ICEBERG
| IDENTITY_P
| IF_P
| IGNORE_P
@@ -21415,6 +21593,7 @@ unreserved_keyword:
| VIEW
| VIEWS
| VOLATILE
+ | VOLUME
| WAREHOUSE
| WAREHOUSE_SIZE
| WEB /* gp */
@@ -22162,6 +22341,7 @@ bare_label_keyword:
| HEADER_P
| HOLD
| HOST
+ | ICEBERG
| IDENTITY_P
| IF_P
| IGNORE_P
@@ -22465,6 +22645,7 @@ bare_label_keyword:
| VIEW
| VIEWS
| VOLATILE
+ | VOLUME
| WAREHOUSE
| WAREHOUSE_SIZE
| WEB
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 021c69ad031..530078e0799 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"
@@ -210,6 +211,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:
@@ -262,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:
@@ -1448,6 +1452,7 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateStmt:
case T_CreateForeignTableStmt:
case T_CreateDirectoryTableStmt:
+ case T_CreateLakeTableStmt:
{
List *stmts;
RangeVar *table_rv = NULL;
@@ -1655,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)
+ ValidateLakeTableStmt(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))
{
/*
@@ -2183,6 +2252,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 +3332,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;
@@ -3311,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;
@@ -3323,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;
@@ -3421,6 +3513,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 +4297,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:
@@ -4211,6 +4311,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/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/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/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..b7c83596f79 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -668,11 +668,35 @@ 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",
+};
+
+/*
+ * 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",
@@ -1077,6 +1101,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 "\
@@ -1274,10 +1308,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", 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", 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_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},
@@ -3042,7 +3079,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))
@@ -3809,6 +3866,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");
@@ -3819,7 +3878,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_iceberg_tables);
else if (Matches("DROP", "DATABASE", MatchAny))
COMPLETE_WITH("WITH (");
else if (HeadMatches("DROP", "DATABASE") && (ends_with(prev_wd, '(')))
@@ -4040,6 +4107,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);
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3de2e549f4c..53258aca56e 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 302607101
#endif
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/catalog/pg_foreign_catalog.h b/src/include/catalog/pg_foreign_catalog.h
new file mode 100644
index 00000000000..547f01c6a9c
--- /dev/null
+++ b/src/include/catalog/pg_foreign_catalog.h
@@ -0,0 +1,70 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ * pg_foreign_catalog.h
+ * definition of the "foreign catalog" system catalog (pg_foreign_catalog)
+ *
+ * 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 fctype BKI_FORCE_NOT_NULL; /* catalog type, e.g. 'hive' */
+ 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..d3e377f1939
--- /dev/null
+++ b/src/include/catalog/pg_foreign_volume.h
@@ -0,0 +1,69 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ * pg_foreign_volume.h
+ * definition of the "foreign volume" system catalog (pg_foreign_volume)
+ *
+ * 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..d22ce5a077f
--- /dev/null
+++ b/src/include/catalog/pg_lake_table.h
@@ -0,0 +1,79 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ * pg_lake_table.h
+ * definition of the "lake table" system catalog (pg_lake_table)
+ *
+ * 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 */
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/commands/laketablecmds.h b/src/include/commands/laketablecmds.h
new file mode 100644
index 00000000000..980d25784d8
--- /dev/null
+++ b/src/include/commands/laketablecmds.h
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ * laketablecmds.h
+ * prototypes for laketablecmds.c.
+ *
+ * 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 ValidateLakeTableStmt(CreateLakeTableStmt *stmt);
+extern void CreateLakeTable(CreateLakeTableStmt *stmt, Oid relId);
+extern void RemoveLakeTableEntry(Oid relid);
+
+#endif /* LAKETABLECMDS_H */
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/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..8f4b9aa5c27 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,
@@ -3271,6 +3273,25 @@ typedef struct CreateForeignServerStmt
List *options; /* generic options to server */
} CreateForeignServerStmt;
+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;
+
+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 +3799,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;
@@ -3803,6 +3833,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/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)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 21db8a4d19d..66684f8d6f9 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -106,10 +106,13 @@ 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_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)
@@ -176,10 +179,13 @@ 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_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/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",
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,
diff --git a/src/test/regress/expected/lake_table.out b/src/test/regress/expected/lake_table.out
new file mode 100644
index 00000000000..b3ca57ea1d2
--- /dev/null
+++ b/src/test/regress/expected/lake_table.out
@@ -0,0 +1,319 @@
+--
+-- 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 | |
+ fctype | text | C | not null | | extended | |
+ 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: 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 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 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 TYPE 'hive'; -- skip with notice
+NOTICE: foreign catalog "lake_test_cat" already exists, skipping
+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, 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
+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 FOREIGN CATALOG lake_test_cat; -- fail, not owner
+ERROR: must be owner of foreign catalog lake_test_cat
+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
+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 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
+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 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
+-------
+ 0
+(1 row)
+
+-- 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.
+-- 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
+-------
+ 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%';
+ count
+-------
+ 0
+(1 row)
+
+-- DROP variants
+DROP FOREIGN CATALOG lake_test_cat; -- fail, already gone
+ERROR: foreign catalog "lake_test_cat" does not exist
+DROP FOREIGN CATALOG IF EXISTS lake_test_cat; -- skip with notice
+NOTICE: foreign catalog "lake_test_cat" does not exist, skipping
+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 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;
+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..3cfc4173a74
--- /dev/null
+++ b/src/test/regress/sql/lake_table.sql
@@ -0,0 +1,155 @@
+--
+-- 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: 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 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');
+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 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 FOREIGN CATALOG lake_test_cat; -- fail, tables depend on it
+
+-- Dropping a lake table removes its pg_lake_table entry
+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 ICEBERG TABLE rejects a non-iceberg table ...
+DROP ICEBERG TABLE lake_test_heap; -- fail, not an iceberg table
+-- 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';
+
+-- 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 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;
+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