Feature: Apache Iceberg lake tables as an extension (datalake_fdw skeleton) - #1842
Feature: Apache Iceberg lake tables as an extension (datalake_fdw skeleton)#1842MisterRaindrop wants to merge 30 commits into
Conversation
Introduce the three system catalogs backing Iceberg lake-table DDL: - pg_foreign_catalog: named foreign catalog bound to a foreign server - pg_foreign_volume: named foreign volume bound to a foreign server - pg_lake_table: per-relation lake-table metadata (type, catalog, volume, options) Register the headers in the catalog Makefile and bump CATALOG_VERSION_NO. No code references the catalogs yet; DDL/commands land in later commits. OIDs 8549-8558 / 9901-9902 verified free via unused_oids; duplicate_oids clean.
Add three statement parse nodes and their hand-maintained node-support plumbing (copy/equal/out/read + fast serialization), mirroring the existing CreateDirectoryTableStmt and CreateForeignServerStmt patterns: - CreateLakeTableStmt (CREATE ICEBERG TABLE ...; embeds CreateStmt) - CreateForeignCatalogStmt (CREATE FOREIGN CATALOG ...) - CreateForeignVolumeStmt (CREATE FOREIGN VOLUME ...) Nodes are not yet produced by the grammar or dispatched; grammar and command handling land in later commits. ObjectType additions are deferred to the command commit to keep exhaustive switches complete.
Add the ICEBERG and VOLUME unreserved keywords and the CREATE-side
grammar productions that build the parse nodes from the previous commit:
- CREATE ICEBERG TABLE name (cols) [FOREIGN CATALOG c] [FOREIGN VOLUME v]
OPTIONS (...) -> CreateLakeTableStmt (forced DISTRIBUTED RANDOMLY)
- CREATE FOREIGN CATALOG name SERVER s OPTIONS (...) -> CreateForeignCatalogStmt
- CREATE FOREIGN VOLUME name SERVER s OPTIONS (...) -> CreateForeignVolumeStmt
Statements parse but are not yet dispatched; command handling, ObjectType
entries and DROP support land in the next commit. Verified: bison reports
no grammar conflicts; the statements parse and reach ProcessUtility.
Make the foreign catalog and foreign volume DDL commands functional on top of the previously added grammar, parse nodes and system catalogs: * CreateForeignCatalog()/CreateForeignVolume() insert into pg_foreign_catalog/pg_foreign_volume, check server existence and USAGE privilege, record dependencies on the server and owner, and dispatch to segments with preassigned OIDs. * Lookup helpers get_foreign_catalog_oid(), get_foreign_volume_oid() and GetForeignVolumeByName(); both objects are unique on (name, server). * New object infrastructure: OBJECT_FOREIGN_CATALOG/OBJECT_FOREIGN_VOLUME and OCLASS_FOREIGN_CATALOG/OCLASS_FOREIGN_VOLUME with handlers in all exhaustive switches (objectaddress, dependency, aclchk, event trigger, seclabel, dropcmds, alter). Ownership checks go through the generic object_ownercheck() via the new ObjectProperty entries. * Four new syscaches: FOREIGNCATALOGNAME/FOREIGNCATALOGOID and FOREIGNVOLUMENAMESERVER/FOREIGNVOLUMEOID. * DROP CATALOG / DROP VOLUME grammar via drop_type_name, going through the regular RemoveObjects() path with dependency handling, so DROP SERVER ... CASCADE also removes dependent catalogs and volumes. * utility.c dispatch and CREATE/DROP FOREIGN CATALOG|VOLUME command tags.
Make CREATE ICEBERG TABLE functional on top of the existing grammar, parse nodes and pg_lake_table catalog: * laketablecmds.c: CreateLakeTable() inserts the pg_lake_table entry after DefineRelation and records dependencies on the table's foreign catalog and volume; ValidateLakeTableOptions() runs the same resolution on the QD before DefineRelation so validation failures don't surface as QE-annotated errors; RemoveLakeTableEntry() cleans up on drop (hooked into heap_drop_with_catalog). * iceberg_default_catalog / iceberg_default_volume GUCs (synchronized to QEs) provide defaults when CREATE ICEBERG TABLE has no CATALOG or VOLUME clause; their check hooks verify the object exists. * The iceberg table access method is resolved strictly by name (get_table_am_oid) and is expected to be provided by a datalake extension; without it, CREATE ICEBERG TABLE fails up front with a hint. The kernel does not hardcode any extension name or AM OID. * Guard rails: reject the iceberg AM for every creation path other than CreateLakeTableStmt (CREATE TABLE ... USING iceberg, CTAS, matview, default_table_access_method, partition children), reject ALTER TABLE ... SET ACCESS METHOD to or from iceberg, and reject SET DISTRIBUTED BY on lake tables, which must stay DISTRIBUTED RANDOMLY. A relation created with the iceberg AM but without its pg_lake_table metadata would be unusable and undroppable. * utility.c dispatch (transformCreateStmt works on the embedded CreateStmt) and the CREATE LAKE TABLE command tag.
pg_dump cannot reproduce lake tables (their data lives in external object storage managed through the iceberg access method's foreign catalog and volume), so skip them with a warning, matching how other unsupported access methods are handled. The access method is matched by name, not by a hardcoded OID. psql tab completion learns CREATE FOREIGN CATALOG/VOLUME ... SERVER ... OPTIONS, CREATE ICEBERG TABLE, DROP CATALOG/VOLUME with CASCADE/ RESTRICT, and completes foreign catalog and volume names after the CATALOG and VOLUME keywords.
Add a lake_table test to the greenplum_schedule covering the new DDL end to end: CREATE/DROP FOREIGN CATALOG and FOREIGN VOLUME (duplicates, IF NOT EXISTS/IF EXISTS, missing server), segment dispatch, object descriptions, CREATE ICEBERG TABLE with explicit CATALOG/VOLUME clauses and via the iceberg_default_catalog/volume GUCs, the forced random distribution policy, every iceberg-AM misuse guard, ownership checks, and dependency behavior (RESTRICT errors, CASCADE, pg_lake_table cleanup on drop). The iceberg access method is simulated with a heap-backed CREATE ACCESS METHOD, since the kernel resolves it by name and the real AM comes from a datalake extension. Refresh expected output of tests that enumerate system catalogs for the three new ones: misc_sanity (pg_lake_table's toast-less varlena columns), sanity_check (catalog list), and oidjoins (BKI_LOOKUP references, including pg_lake_table.ltforeign_catalog pointing at pg_foreign_catalog).
Address review: new community-authored files should carry the standard Apache-2.0 header instead of the PostgreSQL boilerplate.
Match the community convention (see contrib/pax_storage pax_gbench.cc): the Apache-2.0 license block comes first, followed by the file name and IDENTIFICATION.
Address review: invert the if_not_exists check and error out early so the skip path is no longer nested in an else branch, in both CreateForeignCatalog and CreateForeignVolume.
| GetIcebergTableAmOid(bool missing_ok) | ||
| { | ||
| return get_table_am_oid(ICEBERG_TABLE_AM_NAME, missing_ok); | ||
| } |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
- Does this AM refer to the minimal Iceberg AM implementation that does not support scans or inserts? Is that correct?
- One more question: CREATE ICEBERG TABLE USING
Is this syntax designed to support extensions via other frameworks? From my understanding, there should be no need to specify USING am if we're already using CREATE ICEBERG TABLE, right?
There was a problem hiding this comment.
At this stage, I believe the regression testing coverage is sufficient, test stub is enough. Introducing the AM layer at this point would only add unnecessary complexity with no practical benefit, as we still cannot run any real data workloads with it.
Mirror the CREATE side so DROP matches: - DROP CATALOG / DROP VOLUME -> DROP FOREIGN CATALOG / DROP FOREIGN VOLUME (via drop_type_name, same FOREIGN prefix as FOREIGN DATA WRAPPER) - add DROP ICEBERG TABLE [IF EXISTS] as the pair for CREATE ICEBERG TABLE DROP ICEBERG TABLE carries a new DropStmt.isiceberg flag and validates in RangeVarCallbackForDropRelation that the target actually uses the iceberg access method, erroring '"%s" is not an iceberg table' otherwise (mirrors DROP FOREIGN TABLE). Plain DROP TABLE still removes an iceberg table. Adds CMDTAG_DROP_LAKE_TABLE, psql tab completion for the new forms, and refreshes the lake_table regression with new and negative cases. Addresses review feedback on PR apache#1842.
Promote the catalog type from a free-form OPTION to a required TYPE clause backed by a new pg_foreign_catalog.fctype column, per review on PR apache#1842. Every foreign catalog has a type (hive, hdfs, polaris, ...), so it is a property rather than an option; the value is stored verbatim as an open string and validated by the datalake provider, keeping the kernel provider-agnostic. Bumps CATALOG_VERSION_NO for the new column.
| else if (Matches("DROP", "ICEBERG")) | ||
| COMPLETE_WITH("TABLE"); | ||
| else if (Matches("DROP", "ICEBERG", "TABLE")) | ||
| COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Co-authored-by: Andrey Sokolov <sokolov.andrey.yurevich@gmail.com>
Co-authored-by: Andrey Sokolov <sokolov.andrey.yurevich@gmail.com>
Tab completion for DROP ICEBERG TABLE previously offered every ordinary table. Add Query_for_list_of_iceberg_tables, which filters to relations whose access method is the iceberg AM -- the same rule the backend validates DROP ICEBERG TABLE against -- so completion only suggests tables the command will actually accept. When no provider installed the iceberg AM the scalar subquery is NULL and the list is empty. Addresses review on PR apache#1842.
…name The header prototype was renamed to ValidateLakeTableStmt but the definition and its caller still used the old name, breaking the build (-Werror=missing-prototypes). Rename the definition, caller and comments to match.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
…volume example - gram.y: lower-case the USING format for the access method name (it was only upper-cased for table_type), so a quoted "ICEBERG" / "IceBerg" resolves the iceberg AM instead of failing late with "access method ... does not exist". - regress: add a quoted mixed-case format case to lock this in; rename the volume path option in the example from `path` to `base_path` to match the agreed name (discussion apache#1683). Volume options are free-form (no validator), so this is example-only. - create_foreign_volume.sgml: same base_path example.
|
@andr-sokolov It is ready for review |
…STRIBUTED
Address PR review (andr-sokolov):
- pg_lake_table drops lttable_type and ltoptions. A lake table's format is its
access method (pg_class.relam) and its options are the relation's reloptions
(pg_class.reloptions), validated by the access method, so the catalog now
records only the {relation, foreign catalog, foreign volume} binding -- and no
longer needs a TOAST table. CREATE LAKE TABLE routes its OPTIONS into the base
relation's reloptions; the now-dead CreateLakeTableStmt.options node field and
its node-support handlers are removed.
- CREATE LAKE TABLE ... DISTRIBUTED now raises an error instead of a warning,
since lake tables are always distributed randomly.
- Bump CATALOG_VERSION_NO for the pg_lake_table layout change.
- regress: read pg_class.relam/reloptions instead of the dropped columns; add a
case showing OPTIONS become AM-validated reloptions (accepted vs rejected);
the DISTRIBUTED case is now a failure.
…ure/iceberg-ddl-kernel # Conflicts: # src/include/catalog/catversion.h
Per PR review (andr-sokolov): lake tables are always distributed randomly, so a DISTRIBUTED clause has no place in the syntax. Remove OptDistributedBy from the CreateLakeTableStmt rules, so a DISTRIBUTED clause is now a plain syntax error rather than being parsed and then rejected; the relation is still forced to DISTRIBUTED RANDOMLY internally.
…date format first Per PR review (andr-sokolov): - Rename RelationIsIcebergTable -> RelationIsLakeTable and identify a lake table by its pg_lake_table entry rather than by the iceberg access method: the check answers "is this a lake table", independent of any particular format's AM. - In ResolveLakeTableOptions, validate the USING format before checking access method existence, and resolve the AM by the format name via get_table_am_oid(stmt->table_type), so other formats can be supported later. - validate_table_type compares against ICEBERG_TABLE_AM_NAME rather than a literal. - gram.y: the USING clause names both the format and its like-named access method, so normalize it to lower case; a quoted "ICEBERG" resolves the same AM as an unquoted iceberg. The unsupported-format error now shows that name.
Per PR review (andr-sokolov): the hint hardcoded "ICEBERG"; use stmt->table_type so it stays correct once other lake table formats are supported.
…T AM guard Per PR review (andr-sokolov): - The post-DROP checks joined pg_lake_table against pg_class, so once the relation was gone they returned 0 rows even if the pg_lake_table entry had leaked. Save the table's OID before the drop and probe pg_lake_table by ltrelid directly, so an orphaned row would actually be caught. - ATPrepSetAccessMethod: test OidIsValid(iceberg_amoid) once for the two iceberg checks, and splice ICEBERG_TABLE_AM_NAME into the message at compile time instead of passing the constant as a format argument.
| FROM gp_dist_random('pg_foreign_volume') WHERE fvname = 'lake_test_vol'; | ||
|
|
||
| -- Without a provider extension there is no iceberg table AM | ||
| CREATE LAKE TABLE lake_test_t0 (a int) USING ICEBERG CATALOG lake_test_cat VOLUME lake_test_vol; -- fail with hint |
There was a problem hiding this comment.
We can align syntax with crunchydata.com
Building a Postgres Data Warehouse using Iceberg.pdf
> Iceberg as a Postgres table format Capture queries, writes, & schema changes to provide a transactional table experience for Iceberg in S3.
create table chats (
message_id bigserial not null,
thread_id bigint not null,
…
) using iceberg;There was a problem hiding this comment.
@yjhjstz How does crunchydata specify catalog? I didn't find this syntax in their presentation
There was a problem hiding this comment.
Building a Postgres Data Warehouse using Iceberg.pdf
Yes, looks good. But how we setup catalog and lakehouse format? There should exist some definition ... It's good for pre-configured database instances, but your own database needs a more advanced syntax.
There was a problem hiding this comment.
Em, currently how about change CREATE LAKE TABLE lake_test_t0 -> CREATE TABLE lake_test_t0, no need LAKE ?
There was a problem hiding this comment.
@andr-sokolov @leborchuk @yjhjstz I looked into how Crunchy Data Warehouse (now open-sourced by Snowflake as pg_lake) handles the catalog — hope this helps answer the question above.
TL;DR: Crunchy doesn't specify a catalog at CREATE time, because Postgres itself acts as the catalog.
- Managed Iceberg tables are created with plain
CREATE TABLE ... USING iceberg. The Iceberg metadata lives inside Postgres and is exposed through aniceberg_tablesview compatible with the Iceberg JDBC/SQL catalog protocol, so Spark / pyiceberg / iceberg-rust connect to Postgres as the catalog (JDBC with Postgres credentials;catalog_name= the database name). (Crunchy docs) - I couldn't find any syntax for attaching an external catalog (Glue / HMS / REST) — the product doesn't seem to have that concept, which would explain why it doesn't appear in the presentation.
- Storage location is a GUC default plus an optional per-table reloption:
SET crunchy_iceberg.default_location_prefix TO 's3://...', orCREATE TABLE ... USING iceberg WITH (location = 's3://...'). With neither set, the cluster's bundled managed storage is used — so in the demo nothing needs to be specified at all. @leborchuk's reading ("good for pre-configured database instances") matches what I found.
One more data point that may be relevant: pg_lake is currently adding external Iceberg REST catalog support as a client (pg_lake#94), and that roadmap mentions handling multiple endpoints/credentials "likely using a Postgres SERVER-like concept" — quite close to what CREATE FOREIGN CATALOG ... SERVER does in this PR. So the two designs may be converging from different starting points.
For reference, the two syntax shapes side by side:
- Plain
CREATE TABLE ... USING iceberg: the catalog/volume bindings move into reloptions, e.g.WITH (catalog 'c', volume 'v'), with GUC defaults; no new grammar, and plainDROP TABLEapplies (the Spark / pg_lake convention). CREATE LAKE TABLE ... USING <format> CATALOG c VOLUME v: catalog/volume are first-class clauses with dedicatedCREATE/DROP LAKE TABLEcommands (the current PR).
There was a problem hiding this comment.
if we use CREATE TABLE ... USING iceberg WITH (catalog 'c', volume 'v'), we need not change kernel code, iceberg will be totally an extension .
There was a problem hiding this comment.
@yjhjstz Agreed — this looks technically feasible, so I sketched what the fully-extension shape could look like.
Since Cloudberry's table AM API already supports per-AM reloptions validation (TableAmRoutine->amoptions, unlike vanilla PG16), WITH (catalog ..., volume ...) can be validated by the iceberg AM itself with no kernel changes. The catalog/volume objects could reuse the existing FDW infrastructure (SERVER + USER MAPPING), which gives ACLs, pg_dump support, QD/QE dispatch and dependency tracking for free:
CREATE EXTENSION pg_iceberg; -- provides the table AM, two FDWs, and hooks
CREATE SERVER hive_cat FOREIGN DATA WRAPPER iceberg_catalog
OPTIONS (type 'hive', uri 'thrift://metastore:9083');
CREATE USER MAPPING FOR alice SERVER hive_cat
OPTIONS (user '...', password '...');
CREATE SERVER s3_vol FOREIGN DATA WRAPPER iceberg_volume
OPTIONS (base_path 's3://bucket/prefix');
CREATE TABLE t (a int, b text) USING iceberg
WITH (catalog 'hive_cat', volume 's3_vol');
DROP TABLE t; -- plain DROP TABLE appliesA few things the extension would need to own, instead of kernel guards:
- registering table metadata and
pg_dependentries (table → server) viaobject_access_hook, soDROP SERVERstays protected and CASCADE works; - intercepting paths that don't make sense for iceberg (CTAS, partition children,
ALTER TABLE ... SET ACCESS METHOD) viaProcessUtility_hook, and forcing random distribution; - the library would need to be in
shared_preload_librariesso the hooks are always installed.
This is essentially the pg_lake architecture, and USER MAPPING would also give a cleaner credentials story than embedding keys in options. If we go this way, the kernel scaffolding in this PR (the grammar, the three catalogs, the guards) would no longer be needed — the work would move into the datalake extension instead. WDYT?
@leborchuk @andr-sokolov
There was a problem hiding this comment.
@MisterRaindrop If possible, let's not change the kernel to reduce conflicts when updating to new versions of PostgreSQL. I think it's better to use an extension.
|
@andr-sokolov @yjhjstz |
Hi, is there any way to introduce your new commits to this place, where we have a context for review? FYI. @MisterRaindrop |
Review of this PR converged on doing lake tables as an extension instead of as kernel syntax: no new grammar, no new catalogs, no new node types, and nothing in src/backend that has to be maintained forever for one storage format. The replacement is a contrib module that reaches the same place through mechanisms PostgreSQL already has -- a table access method for "CREATE TABLE ... USING iceberg", and foreign servers plus user mappings for catalog endpoints and credentials. This reverts every kernel change made on this branch in one commit rather than rewriting the branch, so that the 64 inline review comments stay anchored to the code they were written about. It is also the cheapest way back: if the extension approach is later judged wrong, reverting this single commit restores the whole kernel scaffolding. The pre-revert tip is also kept as archive/iceberg-ddl-kernel-pr1842 on the author's fork. The resulting tree is identical to the merge base (9c3d48e), verified by comparing tree object ids rather than by reading the diff. The extension implementation follows in the next commits.
Add contrib/datalake_fdw, a skeleton for Iceberg lake-table support that needs
no kernel changes: a lake table is an ordinary CREATE TABLE ... USING iceberg,
and it names a catalog server and a volume server in its reloptions, both
created through foreign-data wrappers this extension registers.
Mapping a table to a pair of foreign servers is what keeps the kernel out of it.
Server options, ownership, privileges and dump/restore already exist for foreign
servers, reloptions already reach every segment with pg_class, and recording the
two servers in pg_depend makes DROP SERVER refuse to strand a table -- none of
which needs new catalogs or grammar.
Where that mapping is kept is one function's business. Every operation reads it
through pg_iceberg_get_table_info(), whose signature and result types match the
existing datalake_fdw implementation this work is the upstream half of -- that
one keeps the same mapping in a system catalog of its own, which an extension
cannot add. Holding the interface still means the layers above it are the same
code on both sides, and that the storage can be reconsidered later without
touching a caller.
The DDL path is complete against a stub metadata engine, so CREATE TABLE and
DROP TABLE work end to end with no catalog service, object store, Arrow or JVM
in the picture. Everything that would touch data reports a clean
"iceberg: <operation> is not supported yet". The interfaces the later work
plugs into ship whole so they can be reviewed before there is an implementation
behind them: the IcebergMetaEngine vtable with a capability bitmap the registry
validates and dispatches through, the FormatReader/FormatWriter instance
interfaces, and the storage facade over open/read/write/list.
Details worth a reviewer's attention:
* Table metadata always goes through one engine, the Java agent, and nothing
selects between implementations -- no option, no setting. The vtable stays
because the implementation is expected to change; that is a property of the
build, never of a table or a session, so an existing table can never be
reinterpreted by a configuration change.
* The table access method fills every callback GetTableAmRoutine() asserts.
ANALYZE succeeds as a zero-sample no-op through relation_acquire_sample_rows,
which keeps it off the scan path that reports not-supported, and VACUUM is a
no-op, so database-wide maintenance never dies on a lake table.
* The object-access hook records the server dependencies on the coordinator and
on every segment, while only GP_ROLE_DISPATCH calls the metadata engine, so
each node can protect its own catalog and the remote side sees one call.
Utility-mode DDL is refused rather than creating local state without dispatch.
* VACUUM FULL is refused in the utility hook, not in the access method: relation
rewriting creates a transient relation first, which reaches OAT_POST_CREATE
and has the engine create a table remotely before the rewrite reports its
error, leaving an orphan behind.
* Credentials are refused in server options and belong in user mappings, which
stay optional so ambient object-store credentials remain usable. Binding
resolution never reads them, so DDL and DROP work with none configured.
* Volume URIs are parsed once, in the options layer, into a versioned
DatalakeLocation; backends receive only that canonical form.
* Option names are macros in per-wrapper option modules, next to the typed
struct each one parses into and the per-catalog-type parse function that fills
it, so that support for a further catalog or storage protocol is an addition
rather than a rewrite. Option lookup itself is one shared set of accessors.
The keys users write are Apache Iceberg's -- uri, warehouse, and rest for a
catalog reached over the REST protocol -- because the specification defines one
protocol that several implementations answer, and an SQL surface tied to one of
them would make every other one need a second spelling. polaris is accepted as
an alias of rest, since that is what the existing implementation calls it. The
macro names, struct names and field names stay that implementation's, so the
divergence is one string per key rather than a different shape.
* A DlErrCode says which kind of failure occurred and nothing else, which is not
enough to diagnose one -- a remote catalog's message, its own error class, and
a stack from wherever it threw have to arrive somewhere. Implementations
record that alongside the code they return, and the entry points facing
PostgreSQL turn both into one report: the message as DETAIL, a stack only for a
session that asked for log-level detail. Recording allocates nothing and
raises nothing, so a cleanup path crossing back from C++ can use it. The
SQLSTATE follows the code rather than being internal_error throughout, which
also keeps a source location out of user-visible output.
* C++ translation units reach the server headers through common/dl_pg_api.h,
which applies extern "C" -- without it the module builds and then fails to
dlopen on a mangled errmsg. The C/C++ boundary macros follow the PAX pattern,
including deferring ereport() until after the catch handler is left, since
longjmp() out of a handler is undefined. Exported symbols are limited to the
PG entry points listed in exports.txt, ELF and Mach-O each getting the right
linker mechanism, so a future static Arrow cannot leak into other extensions.
* A schema-level dump round-trips. pg_dump writes DISTRIBUTED RANDOMLY and
ALTER TABLE ... OWNER TO for a table like this, so both are accepted -- a
guard that refuses what this module's own dump emits refuses to restore it.
Neither can desynchronise anything: the distribution clause asks for the
policy that would have been injected anyway, and ownership is local catalog
state. Every other ALTER form, and a distribution clause naming columns,
stay refused. Dumping the *contents* of a lake table still fails, because
scanning does; a full pg_dump of a database containing one therefore does not
work yet, and what a dump of externally owned table data should even mean is
the open question behind that.
Test material lives under test/automation, one directory per category, with the
module's Makefile pointing pg_regress at the category that needs no external
service; make installcheck from the module and make test from the harness run
the same cases. Testing against a real catalog or object store cannot be done by
comparing against a recorded transcript, so the harness is what those categories
will be added to, and it already reports a category whose services are absent as
skipped rather than passed.
The suite covers the DDL path including per-segment catalog state, the rejection
matrices and the privilege model; installcheck is green on a three-segment
cluster and does not depend on the order the cases run in. Per-segment
assertions compare against gp_segment_configuration rather than naming segments,
so they hold on a cluster of any size, and each guard has a case showing what it
does *not* refuse -- renaming a schema that holds no lake table, for instance --
because a guard wider than its problem passes its own tests just as well.
CI runs the suite as its own matrix entry, ic-datalake-fdw, whose demo cluster is
created with shared_preload_libraries='datalake_fdw' -- the module installs
process-wide hooks, so _PG_init refuses to load any other way, and a generic
cluster could not run these cases at all. That is the same mechanism two
existing entries already use. ("make check" would need the temp-config this
module also ships; it exists in-tree only, since PGXS refuses the target.)
The error channel has no coverage yet: no statement can make the stub engine
fail, so the first implementation that can fail is what brings a case for it.
…covery The test sets shared_preload_libraries and restarts with "gpstop -raiq". An immediate shutdown skips the shutdown checkpoint, so the control file is left in a state other than DB_SHUTDOWNED and the next startup performs crash recovery: xlogrecovery.c sets InRecovery, xlog.c calls PerformWalRecovery(), which signals PMSIGNAL_RECOVERY_STARTED, and the postmaster moves to PM_RECOVERY. In that state canAcceptConnections() answers CAC_NOTCONSISTENT, reported as "the database system is not accepting connections" with detail "Hot standby mode is disabled". gpstart makes exactly such a connection right after pg_ctl returns, to read the segment configuration, so gpstop -r exits CRITICAL and the restart is reported as failed. The damage does not stop there. psql gives up at the \c that follows, so every statement in the file is skipped and the test fails as a whole; the cleanup at the end of the file never runs; and gpstart never got past starting the coordinator in admin mode, so the cluster is left with no segments up. Suites that run after this one in the same job then lose their Gather Motion nodes and fail as well. Shut down fast instead. A fast shutdown writes the shutdown checkpoint, the control file says DB_SHUTDOWNED, no recovery runs, PM_RECOVERY is never entered, and CAC_NOTCONSISTENT cannot be returned -- the failure becomes unreachable rather than merely less likely. Fast is also what the rest of the tree already uses: gpstop -raf/-arf appear in dozens of places, and this file was the only user of -raiq. Measured on a three-segment demo cluster, dirtying 1.5M coordinator rows before each restart so that recovery is slow enough to lose the race reliably: -raiq failed 2/2 with the message above, -rafq passed 3/3 with all three segments still up afterwards. pg_controldata confirms the mechanism at the other end -- "in production" after an immediate shutdown, "shut down" after a fast one. The test still passes under pg_regress with the change.
|
@tuhaihe as you asked, the extension work now lives in this PR instead of a separate one, so the review context stays in one place. @andr-sokolov @yjhjstz please note what changed below before looking at the diff again. How it was moved, and why this way. The branch was not force-pushed. It now has, in order:
Rewriting the branch would have been shorter, but it would have orphaned the review history, and it would have made going back expensive. This way the kernel scaffolding is one Please re-review from scratch. The diff is now 100% different from what was reviewed and approved earlier in this thread, so the earlier approval does not apply to what is here now. I would rather say that explicitly than have it merged on the strength of it. One thing that will look out of place: the second commit changes two lines in The original description is kept at the bottom of the PR body, struck through. #1881 is superseded by this PR. |
| btree_gin \ | ||
| btree_gist \ | ||
| citext \ | ||
| datalake_fdw \ |
There was a problem hiding this comment.
Is datalake_fdw enabled by default? We can disable it by default and enable it using the --enable-datalake_fdw configure option, the way like PAX.
There was a problem hiding this comment.
Good call -- done in 6f40dd0, following the PAX mechanism: PGAC_ARG_BOOL defaulting to no, substituted into Makefile.global, and a conditional in contrib/Makefile that moves the directory to ALWAYS_SUBDIRS when disabled so the clean targets still reach it.
Three things I decided while doing it, all cheap to reverse if you disagree:
- The option is spelled
--enable-datalake-fdw, with dashes. Every option in this tree uses dashes (--enable-tap-tests,--enable-ic-udp2,--enable-catalog-ext), and autoconf maps the dashes toenable_datalake_fdwfor the Makefile variable, so the directory name still matches. Happy to use the underscore form if you would rather it read exactly like the directory. - No
AC_DEFINE. PAX and ic-udp2 define a macro because C code tests it; nothing here does, and adding an unread macro would mean touchingsrc/include/pg_config.h.infor no reader.--enable-pxfand--enable-orafcealready use this shorter four-argument form. --enable-datalake-fdwis also added todevops/build/automation/cloudberry/scripts/configure-cloudberry.sh, next to--enable-pax. Without it theic-datalake-fdwCI job would install nothing and silently test nothing. I deliberately did not add it tocoverity.ymlandsonarqube.yml, which also pass--enable-pax-- say the word if you want the module in the weekly scans too.
One thing you should know about configure. I regenerated it by hand rather than running autoconf over the whole file, because the committed configure and configure.ac are currently out of sync in both directions:
configure.achas a restructured PAX liburing check (thecase $host_os in linux*)block with the macOS fallback comment) that was never regenerated intoconfigure;configurehas a Darwin python shared-library lookup (trying.dylibin addition toDLSUFFIX) that does not exist inconfigure.ac.
A full autoconf run therefore produced twelve hunks, only four of which were mine; taking all of them would have swept those two unrelated changes into this PR and, in the python case, reverted working behaviour. So I applied only the four hunks for this option and left the drift alone. It is worth someone reconciling those two separately -- I did not want to do it inside this PR.
Verified by configuring a real tree both ways, not by inspection:
| configure says | Makefile.global |
SUBDIRS |
ALWAYS_SUBDIRS |
|
|---|---|---|---|---|
--enable-datalake-fdw |
... yes |
enable_datalake_fdw = yes |
has datalake_fdw |
-- |
| default | ... no |
enable_datalake_fdw = no |
-- | has datalake_fdw |
Review asked for the module to be off by default and enabled explicitly, the way PAX is, rather than being added to the unconditional contrib SUBDIRS list. The mechanism is the same one PAX and ic-udp2 use: a PGAC_ARG_BOOL defaulting to no, substituted into Makefile.global, and a conditional in contrib/Makefile that puts the directory in ALWAYS_SUBDIRS when disabled so the clean targets still reach it. The option carries no AC_DEFINE. PAX and ic-udp2 define one because C code tests it; nothing here does, and adding an unread macro would mean touching src/include/pg_config.h.in for no reader. --enable-pxf and --enable-orafce already use this shorter form. configure is regenerated by hand rather than wholesale. The committed configure and configure.ac are currently out of sync in both directions -- the PAX liburing block has changes in configure.ac that were never regenerated, and the Darwin python shared-library lookup exists in configure but not in configure.ac -- so a full autoconf run would have swept eight unrelated hunks into this commit. Only the four hunks belonging to this option were applied. --enable-datalake-fdw is added to the CI configure line, without which the ic-datalake-fdw job would install nothing and test nothing. Verified by configuring both ways on a real tree: with the option, "checking whether to build with datalake_fdw support ... yes", Makefile.global gets enable_datalake_fdw = yes, and contrib puts datalake_fdw in SUBDIRS; without it, no / no / and the directory appears in ALWAYS_SUBDIRS instead.
Important
This PR has been repurposed, in place, from kernel syntax to an extension.
Review here converged on doing lake tables without kernel changes, and
@tuhaihe asked
for the new commits to land in this PR so the review context stays in one
place. So rather than opening a third PR, the branch now carries:
comments stay anchored to the code they were written about;
the merge base, verified by comparing tree object ids);
Nothing was force-pushed. The kernel work is therefore still recoverable two
ways: revert the single revert commit, or check out
archive/iceberg-ddl-kernel-pr1842on the author's fork.The diff is now 100% different from what was reviewed and approved earlier
in this thread, so the existing approval no longer applies to it. Please
re-review from scratch. The original description is kept, struck through, at
the bottom.
What does this PR do?
Adds
contrib/datalake_fdw: Apache Iceberg lake tables as a table accessmethod, with no kernel changes. A lake table is an ordinary
CREATE TABLE ... USING iceberg WITH (catalog = '...', volume = '...'), wherecatalogandvolumename foreign servers created through two foreign-datawrappers this extension registers.
This is deliberately a skeleton, published to get the shape reviewed before
the implementation follows.
CREATE TABLEandDROP TABLEwork end to endagainst a stub metadata engine -- no catalog service, object store, Arrow or JVM
involved -- and everything that would touch data reports a clean
iceberg: <operation> is not supported yet. The interfaces the later work plugsinto ship whole so they can be argued about now: the metadata-engine vtable with
a capability bitmap the registry validates and dispatches through, the
reader/writer format interfaces, and the storage facade.
The question this change is really asking is whether the seam is in the
right place -- see Additional Context.
Type of Change
Test Plan
make installcheckmake -C src/test installcheck-cbdb-parallelImpact
Performance: none. Nothing outside the module changes behaviour, and no
statement gains work: the utility and object-access hooks match on the access
method and return immediately for every other relation.
User-facing changes: a new extension, plus two lines of registration
(
contrib/Makefile, and a CI matrix entry so the suite actually runs). Theextension installs process-wide hooks, so it must be in
shared_preload_libraries;_PG_initrefuses to load any other way rather thanhalf-initialising. One consequence worth documenting when this ships: because
gpconfig -c shared_preload_librariesreplaces the value rather thanappending to it, overwriting that GUC on a cluster where the extension is in use
will break the databases that have it installed.
Dependencies: none. The module builds with no new external dependency -- the
C++ translation units are stubs today, and the object-store SDK and Arrow arrive
with the changes that need them.
Checklist
Additional Context
This is the upstream half of an implementation that already exists. A
datalake_fdw with the same shape -- the same two hooks, the same table access
method, largely the same file names -- runs in production elsewhere, and the
intent is that this becomes the single source for it rather than a second
lookalike. Two consequences a reviewer should see explicitly:
The mapping accessor is deliberately shaped to that implementation. A lake
table's mapping to its catalog and volume servers is read only through
pg_iceberg_get_table_info(), whose signature and result types(
IcebergTableInfo,IcebergTableOptions) match it field for field. Thatimplementation keeps the same mapping in a system catalog of its own, which an
extension cannot add, so here the function reads reloptions instead -- but the
difference stops inside the function body, and the layers above it can be the
same code on both sides.
The SQL vocabulary follows the Iceberg specification, not the released
implementation. Catalog servers take
uriandwarehouse, and the REST catalogtype is
rest. An earlier revision of this change used the other tree'surl/warehouse_location_prefix/polarison the reasoning that one vocabularyacross both trees was worth more than either name; review pointed out that the
REST catalog specification exists precisely so that these keys are the same
across implementations, and that argument wins.
polarisis kept as an acceptedalias for
restso existing SQL keeps working. Convergence with the other treeis unaffected in substance: the macro names, struct names, field names and
parse-function split are unchanged, so only the string a user types differs.
hadoopands3are in the vocabulary but refused until something implementsthem.
Semantics that are decided (and why they are in the SQL surface)
DROP TABLEdoes not delete lake data unless the table was created withpurge_on_drop = true. It is a table option rather than a GUC or a separatefunction so that it travels with the table, cannot turn one
DROPinto adestructive one through session state, and is carried out by a dump.
(catalog, namespace, table)name triple, matchingthe existing implementation.
the external catalog owning the metadata. Nothing here takes distributed locks.
adapt to silently. The external table is the truth.
What was and was not verified
Stated plainly, because a skeleton is worth less if you have to guess:
Verified on a three-segment cluster:
make installcheck3/3, andorder-independent (each file alone and in reverse order, on a fresh database);
zero build warnings; exactly 8 exported symbols; a schema-level
pg_dump/restoreround trip; Apache RAT clean. Separately, an 84-statement smoke pass over
ordinary objects -- heap/AO/partitioned/temp/unlogged tables, CTAS, matviews,
inherited and typed tables, views, sequences, indexes, functions, another
wrapper's servers and user mappings, every ALTER form, COPY, TRUNCATE, VACUUM
FULL, CLUSTER, REINDEX -- with the library preloaded, both with and without the
extension installed, plus two concurrency probes. Zero errors: the hooks are
inert for everything that is not a lake table.
Verified by CI (this was listed as unverified when the change was first
published, and has since run): the
ic-datalake-fdwentry added here passes onRocky 8, 9 and 10, and
ic-cbdb-parallelpasses on every platform.Still not verified: the error-detail channel has no regression coverage, for
the reason given below.
Why this branch also touches
contrib/interconnectThe second commit changes two lines in another module's test, which would
otherwise look like unrelated scope. It is here because that test made this PR's
CI red and the failure is worth fixing rather than retrying:
contrib/interconnect/sql/interconnect.sqlrestarts the cluster withgpstop -raiq. An immediate shutdown skips the shutdown checkpoint, so the nextstartup runs crash recovery; while the postmaster is in
PM_RECOVERYit rejectsthe connection
gpstartmakes to read the segment configuration ("the databasesystem is not accepting connections"),
gpstop -rexits CRITICAL, psql gives upat the following
\c, and every statement in the file is skipped -- leaving thecluster with only the coordinator running, so the suites that run after it in the
same job fail too. A fast shutdown checkpoints on the way out, so no recovery
runs and the state that produces that error is never entered. Measured before
and after with recovery deliberately slowed:
-raiqfailed 2/2,-rafqpassed3/3. Fast is also what the rest of the tree already uses; this file was the only
-raiq. Happy to split it into its own PR if that is preferred.Known limits, stated rather than left to be found
ALTER SERVER ... RENAMEon a referencedserver is refused. Storing OIDs instead would allow it, at the cost of owning
dump/restore translation; the accessor above is the only thing that would
change.
pg_dumpof a database containing a lake table fails, because dumpingtable contents means scanning. A schema-level dump round-trips. The data
belongs to the lake, so it arguably should not be in the dump at all -- but
pg_dumpdecides from relkind, which an extension cannot influence, so thatneeds a separate conversation.
ALTER TABLEform exceptOWNER TOis refused while the access methodis unfinished.
purge_on_dropis therefore fixed at CREATE time.make the stub engine fail.
Naming
datalake_fdwis inherited from the existing implementation and is a poor fit:what this installs is a table access method plus two wrappers, not one FDW. A
better name is welcome while it is still cheap to change.
Discussion: #1683 · Superseded PR: #1881
Original description — kernel-side DDL scaffolding (superseded; kept for the review history)
SummaryKernel-side DDL scaffolding for Iceberg lake tables, per the design proposal in #1683 (this PR is the "core syntax" milestone of the roadmap posted there). It adds the system catalogs, parse nodes, grammar, commands, and client-tool awareness that a datalake provider extension builds on. No table AM implementation or provider is included — those are later milestones (agent / FDW phases in the roadmap).What's included (structured for commit-by-commit review)catalog:pg_foreign_catalog,pg_foreign_volume(metadata service / storage location handles, hanging offpg_foreign_server),pg_lake_table(per-relation lake metadata keyed by relid). All three have TOAST tables; name indexes are single-column unique (see "Design decisions").nodes:CreateLakeTableStmt(embedsCreateStmt),CreateForeignCatalogStmt,CreateForeignVolumeStmt+ hand-maintained node-support functions.parser:CREATE ICEBERG TABLE ... CATALOG ... VOLUME ... OPTIONS (...),CREATE FOREIGN CATALOG|VOLUME ... SERVER ...,DROP CATALOG|VOLUMEviadrop_type_name.commands (foreign catalog/volume): create/drop with IF [NOT] EXISTS, dependencies, ownership, object addressing / COMMENT / event-trigger integration, syscaches, QD→QE dispatch with synchronized OIDs.commands (CREATE ICEBERG TABLE): resolves catalog/volume (explicit clause oriceberg_default_catalog/iceberg_default_volumeGUCs), validates the statement uses theicebergAM, creates the relation + TOAST +pg_lake_tablerow + dependencies; guardsALTER TABLE SET ACCESS METHOD/SET DISTRIBUTED BYboth directions; lake tables are forcedDISTRIBUTED RANDOMLY.bin:pg_dumpskips iceberg tables (by AM name, with a warning — same pattern as PAX); psql tab completion.tests:lake_tableregression ingreenplum_schedule(DDL lifecycle, duplicate/USING rejection, TOAST wide rows, ownership, dependency/CASCADE, QD/QE dispatch checks) + catalog-expected refresh acrossregress,singlenode_regress, and pax copies.Design decisions to reviewProvider decoupling: the kernel resolves the AM strictly by name (get_table_am_oid("iceberg")); no hardcoded AM OID and no extension-name checks. Everything degrades gracefully (with a hint) when no provider is installed.Provider integration contract:CreateLakeTable()runs afterDefineRelation()and ends withCommandCounterIncrement()+InvokeObjectPostCreateHook(LakeTableRelationId, ...)— a provider performs remote Iceberg creation from that object-access hook (on QD and QEs), where catalog/volume/options metadata is already visible.relation_set_new_filelocatorremains local-storage-only.Catalog/volume names are database-global (single-column unique index), likepg_foreign_server— every reference syntax (DROP CATALOG x, theCATALOG xclause, the GUCs) identifies them by bare name, so the uniqueness scope matches what the syntax can express. (The alternative — per-server names like user mappings — would require server-qualified reference syntax everywhere.)CREATE ICEBERG TABLErejects aUSINGclause naming any other AM; without it the statement impliesUSING iceberg.Known limitations / open questions (deliberately out of scope here)pg_dump / pg_upgrade: FOREIGN CATALOG/VOLUME objects and pg_lake_table metadata are not dumped yet; iceberg tables are skipped with a warning (PAX precedent). Restore/upgrade support is planned as a follow-up.Permission model: creating a catalog/volume requiresUSAGEon its server. Referencing one fromCREATE ICEBERG TABLEcurrently checks existence only — whether that should requireUSAGEon the underlying server, or dedicated ACLs (GRANT USAGE ON CATALOG), is an open question we'd like reviewer input on.The GUC defaults are synced to QEs and behave likedefault_tablespacew.r.t. objects dropped afterSET.Discussion: #1683