From c5ff84be31270baa91036b0202bf944627a79509 Mon Sep 17 00:00:00 2001 From: qsliu Date: Thu, 12 Feb 2026 15:49:07 +0800 Subject: [PATCH 01/14] export GetDuckDBDatabase() C accessor for external extensions Add a C-exported function that returns a void* pointer to the DuckDB database instance managed by pg_duckdb. This allows external PostgreSQL extensions (e.g. pg_ducklake_next) to access the DuckDB instance for loading statically-linked DuckDB extensions via LoadStaticExtension(). Follows the same pattern as RegisterDuckdbTableAm in pgduckdb_table_am.cpp. Co-Authored-By: Claude Opus 4.6 --- src/pgduckdb_duckdb.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pgduckdb_duckdb.cpp b/src/pgduckdb_duckdb.cpp index 68f07fd0..0dd70764 100644 --- a/src/pgduckdb_duckdb.cpp +++ b/src/pgduckdb_duckdb.cpp @@ -231,6 +231,19 @@ DuckDBManager::Reset() { UnclaimBgwSessionHint(); } +/* + * Exported C accessor for the DuckDB database instance. + * Allows external PostgreSQL extensions (e.g. pg_ducklake_next) to access + * the DuckDB instance managed by pg_duckdb, for loading statically-linked + * DuckDB extensions via db->LoadStaticExtension(). + * + * Follows the same pattern as RegisterDuckdbTableAm in pgduckdb_table_am.cpp. + */ +extern "C" __attribute__((visibility("default"))) void * +GetDuckDBDatabase(void) { + return &DuckDBManager::Get().GetDatabase(); +} + int64 GetSeqLastValue(const char *seq_name) { Oid duckdb_namespace = get_namespace_oid("duckdb", false); From 2cf571f8598e5096ed8b2c31182678bbd32fd1f5 Mon Sep 17 00:00:00 2001 From: qsliu Date: Fri, 13 Feb 2026 10:19:46 +0800 Subject: [PATCH 02/14] Add RegisterDuckdbLoadExtension() hook for deferred extension loading External extensions (e.g. pg_ducklake) need to call LoadStaticExtension() but cannot do so during _PG_init() because the DuckDB instance is not yet initialized. This adds a callback registration API so external extensions can register a loader that pg_duckdb invokes during DuckDBManager::Initialize(), after the instance is fully configured. Co-Authored-By: Claude Opus 4.6 --- src/pgduckdb_duckdb.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/pgduckdb_duckdb.cpp b/src/pgduckdb_duckdb.cpp index 0dd70764..2ee86dd6 100644 --- a/src/pgduckdb_duckdb.cpp +++ b/src/pgduckdb_duckdb.cpp @@ -88,6 +88,17 @@ ToString(char *value) { config.options.ddb_option_name = duckdb_##ddb_option_name; \ elog(DEBUG2, "[PGDuckDB] Set DuckDB option: '" #ddb_option_name "'=%s", ToString(duckdb_##ddb_option_name).c_str()); +extern "C" { +typedef void (*DuckDBLoadExtension)(void); +} +static std::vector load_extensions; + +extern "C" __attribute__((visibility("default"))) bool +RegisterDuckdbLoadExtension(DuckDBLoadExtension extension) { + load_extensions.push_back(extension); + return true; +} + void DuckDBManager::Initialize() { elog(DEBUG2, "(PGDuckDB/DuckDBManager) Creating DuckDB instance"); @@ -217,6 +228,10 @@ DuckDBManager::Initialize() { } } + for (auto extension : load_extensions) { + extension(); + } + if (duckdb_autoinstall_known_extensions) { InstallExtensions(context); } From 827be77987c1d29fe5b8d35fd2b26d779fbfa602 Mon Sep 17 00:00:00 2001 From: qsliu Date: Fri, 13 Feb 2026 15:11:18 +0800 Subject: [PATCH 03/14] chore on hook --- src/pgduckdb_duckdb.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/pgduckdb_duckdb.cpp b/src/pgduckdb_duckdb.cpp index 2ee86dd6..f9b3d7d5 100644 --- a/src/pgduckdb_duckdb.cpp +++ b/src/pgduckdb_duckdb.cpp @@ -89,7 +89,7 @@ ToString(char *value) { elog(DEBUG2, "[PGDuckDB] Set DuckDB option: '" #ddb_option_name "'=%s", ToString(duckdb_##ddb_option_name).c_str()); extern "C" { -typedef void (*DuckDBLoadExtension)(void); +typedef void (*DuckDBLoadExtension)(void *db, void *context); } static std::vector load_extensions; @@ -229,7 +229,7 @@ DuckDBManager::Initialize() { } for (auto extension : load_extensions) { - extension(); + extension(database, &context); } if (duckdb_autoinstall_known_extensions) { @@ -246,18 +246,6 @@ DuckDBManager::Reset() { UnclaimBgwSessionHint(); } -/* - * Exported C accessor for the DuckDB database instance. - * Allows external PostgreSQL extensions (e.g. pg_ducklake_next) to access - * the DuckDB instance managed by pg_duckdb, for loading statically-linked - * DuckDB extensions via db->LoadStaticExtension(). - * - * Follows the same pattern as RegisterDuckdbTableAm in pgduckdb_table_am.cpp. - */ -extern "C" __attribute__((visibility("default"))) void * -GetDuckDBDatabase(void) { - return &DuckDBManager::Get().GetDatabase(); -} int64 GetSeqLastValue(const char *seq_name) { From 257090aad99804c857b816068f14690cc401e303 Mon Sep 17 00:00:00 2001 From: qsliu Date: Fri, 13 Feb 2026 16:37:14 +0800 Subject: [PATCH 04/14] export pgduckdb_get_tabledef --- src/pgduckdb_ruleutils.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pgduckdb_ruleutils.cpp b/src/pgduckdb_ruleutils.cpp index 8e4dfe32..fc5927fb 100644 --- a/src/pgduckdb_ruleutils.cpp +++ b/src/pgduckdb_ruleutils.cpp @@ -619,7 +619,7 @@ pgduckdb_get_querydef(Query *query) { * the following patch that I (Jelte) submitted to Postgres in 2023: * https://www.postgresql.org/message-id/CAGECzQSqdDHO_s8=CPTb2+4eCLGUscdh=KjYGTunhvrwcC7ZSQ@mail.gmail.com */ -char * +extern "C" __attribute__((visibility("default"))) char * pgduckdb_get_tabledef(Oid relation_oid) { Relation relation = relation_open(relation_oid, AccessShareLock); const char *relation_name = pgduckdb_relation_name(relation_oid); @@ -649,6 +649,8 @@ pgduckdb_get_tabledef(Oid relation_oid) { // allowed } else if (relation->rd_rel->relpersistence != RELPERSISTENCE_PERMANENT) { elog(ERROR, "Only TEMP and non-UNLOGGED tables are supported in DuckDB"); + } else if (strcmp(duckdb_table_am_name, "duckdb") != 0) { + // not a duckdb table, let them decide } else if (relation->rd_rel->relowner != pgduckdb::MotherDuckPostgresUserOid()) { elog(ERROR, "MotherDuck tables must be owned by the duckb.postgres_role"); } @@ -792,7 +794,7 @@ pgduckdb_get_tabledef(Oid relation_oid) { /* close create table's outer parentheses */ appendStringInfoString(&buffer, ")"); - if (!pgduckdb::IsDuckdbTableAm(relation->rd_tableam)) { + if (duckdb_table_am_name == nullptr) { /* Shouldn't happen but seems good to check anyway */ elog(ERROR, "Only a table with the DuckDB can be stored in DuckDB, %d %d", relation->rd_rel->relam, pgduckdb::DuckdbTableAmOid()); From 2d177982219830771362604600f290caf3d09107 Mon Sep 17 00:00:00 2001 From: qsliu Date: Sat, 14 Feb 2026 11:59:57 +0800 Subject: [PATCH 05/14] export DuckdbUnsafeSetNextExpectedCommandId --- src/pgduckdb_xact.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/pgduckdb_xact.cpp b/src/pgduckdb_xact.cpp index f3261be7..dd7eff2a 100644 --- a/src/pgduckdb_xact.cpp +++ b/src/pgduckdb_xact.cpp @@ -15,6 +15,21 @@ namespace pgduckdb { static CommandId next_expected_command_id = FirstCommandId; static bool top_level_statement = true; +/* + * Unsafe hook for external extensions to set next_expected_command_id. + * + * These allow extensions like pg_ducklake to temporarily suppress mixed-write + * detection for internal metadata operations (e.g., SPI writes to ducklake_* + * tables) that should not count as user-initiated Postgres writes. + * + * WARNING: Misuse can mask genuine mixed-write violations. Only use for + * operations that are logically part of a DuckDB transaction. + */ +extern "C" __attribute__((visibility("default"))) void +DuckdbUnsafeSetNextExpectedCommandId(uint32_t command_id) { + next_expected_command_id = command_id; +} + namespace pg { static bool force_allow_writes; From 442fa520eb6a808c7bb3cd41e9522d43b375c5ba Mon Sep 17 00:00:00 2001 From: qsliu Date: Tue, 24 Feb 2026 16:38:41 +0800 Subject: [PATCH 06/14] Export pgduckdb_get_querydef and pgduckdb_relation_name symbols Add extern "C" with default visibility to these two functions so pg_ducklake can resolve them at runtime for CTAS support. Co-Authored-By: Claude Opus 4.6 --- src/pgduckdb_ruleutils.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pgduckdb_ruleutils.cpp b/src/pgduckdb_ruleutils.cpp index fc5927fb..51ae6e5c 100644 --- a/src/pgduckdb_ruleutils.cpp +++ b/src/pgduckdb_ruleutils.cpp @@ -563,7 +563,7 @@ pgduckdb_db_and_schema_string(const char *postgres_schema_name, const char *duck * DuckDB for the specified Postgres OID. This includes the DuckDB database name * too. */ -char * +extern "C" __attribute__((visibility("default"))) char * pgduckdb_relation_name(Oid relation_oid) { HeapTuple tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relation_oid)); if (!HeapTupleIsValid(tp)) @@ -597,7 +597,7 @@ pgduckdb_relation_name(Oid relation_oid) { * use in get_target_list to determine if we're processing the outermost * targetlist or not. */ -char * +extern "C" __attribute__((visibility("default"))) char * pgduckdb_get_querydef(Query *query) { outermost_query = true; auto save_nestlevel = NewGUCNestLevel(); From cb332d62fed45e6897dcd1e95c076696e46f9d02 Mon Sep 17 00:00:00 2001 From: qsliu Date: Tue, 24 Feb 2026 22:17:44 +0800 Subject: [PATCH 07/14] Allow pg_ducklake extension functions in DuckDB-only function cache Add pg_ducklake's extension OID check to BuildDuckdbOnlyFunctions() so functions registered by pg_ducklake (e.g. ducklake.options()) are recognized as DuckDB-only functions and routed correctly. Co-Authored-By: Claude Sonnet 4.5 --- src/pgduckdb_metadata_cache.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pgduckdb_metadata_cache.cpp b/src/pgduckdb_metadata_cache.cpp index b11543da..76ff4c31 100644 --- a/src/pgduckdb_metadata_cache.cpp +++ b/src/pgduckdb_metadata_cache.cpp @@ -195,7 +195,12 @@ BuildDuckdbOnlyFunctions() { "map_extract_value", "map_from_entries", "map_keys", - "map_values"}; + "map_values", + /* pg_ducklake DuckDB-only functions */ + "options"}; + + /* Also accept functions from pg_ducklake extension */ + Oid ducklake_extension_oid = get_extension_oid("pg_ducklake", true); for (uint32_t i = 0; i < lengthof(function_names); i++) { CatCList *catlist = SearchSysCacheList1(PROCNAMEARGSNSP, CStringGetDatum(function_names[i])); @@ -203,7 +208,9 @@ BuildDuckdbOnlyFunctions() { for (int j = 0; j < catlist->n_members; j++) { HeapTuple tuple = &catlist->members[j]->tuple; Form_pg_proc function = (Form_pg_proc)GETSTRUCT(tuple); - if (getExtensionOfObject(ProcedureRelationId, function->oid) != cache.extension_oid) { + Oid ext_oid = getExtensionOfObject(ProcedureRelationId, function->oid); + if (ext_oid != cache.extension_oid && + (!OidIsValid(ducklake_extension_oid) || ext_oid != ducklake_extension_oid)) { continue; } From 28cb533463380d0cd0ac73c9a04c81769b91aa77 Mon Sep 17 00:00:00 2001 From: qsliu Date: Wed, 25 Feb 2026 10:49:30 +0800 Subject: [PATCH 08/14] Support ALTER TABLE for externally registered table AMs - Set DDLType::ALTER_TABLE for tables using registered external AMs (e.g. ducklake) in utility hook, not just native duckdb tables - Export DuckdbIsAlterTableInProgress() for external extensions - Export pgduckdb_get_alter_tabledef() and pgduckdb_get_rename_relationdef() with extern "C" visibility for use by external extensions - Fix pgduckdb_get_rename_relationdef() to use actual table AM name instead of hardcoded "duckdb" Co-Authored-By: Claude Sonnet 4.5 --- src/pgduckdb_ddl.cpp | 18 ++++++++++++++++-- src/pgduckdb_ruleutils.cpp | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/pgduckdb_ddl.cpp b/src/pgduckdb_ddl.cpp index 9ea89041..18326a64 100644 --- a/src/pgduckdb_ddl.cpp +++ b/src/pgduckdb_ddl.cpp @@ -5,6 +5,7 @@ #include "pgduckdb/pgduckdb_ddl.hpp" #include "pgduckdb/pgduckdb_hooks.hpp" #include "pgduckdb/pgduckdb_planner.hpp" +#include "pgduckdb/pgduckdb_table_am.hpp" #include "pgduckdb/pg/string_utils.hpp" extern "C" { @@ -644,7 +645,8 @@ DuckdbHandleDDLPre(PlannedStmt *pstmt, const char *query_string) { return DuckdbHandleRenameViewPre(stmt); } - if (pgduckdb::IsDuckdbTable(rel)) { + if (pgduckdb::IsDuckdbTable(rel) || + pgduckdb::DuckdbTableAmGetName(rel->rd_tableam) != nullptr) { if (pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::NONE) { ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("Only one DuckDB %s can be renamed in a single statement", @@ -685,7 +687,9 @@ DuckdbHandleDDLPre(PlannedStmt *pstmt, const char *query_string) { * afterwards. We currently only do this to get a better error message, * because we don't support REFERENCES anyway. */ - if (pgduckdb::IsDuckdbTable(relation) && pgduckdb::top_level_duckdb_ddl_type == pgduckdb::DDLType::NONE) { + if ((pgduckdb::IsDuckdbTable(relation) || + pgduckdb::DuckdbTableAmGetName(relation->rd_tableam) != nullptr) && + pgduckdb::top_level_duckdb_ddl_type == pgduckdb::DDLType::NONE) { pgduckdb::top_level_duckdb_ddl_type = pgduckdb::DDLType::ALTER_TABLE; pgduckdb::ClaimCurrentCommandId(); } @@ -1851,3 +1855,13 @@ DECLARE_PG_FUNCTION(duckdb_grant_trigger) { PG_RETURN_NULL(); } } + +/* + * Exported getter for top_level_duckdb_ddl_type, so external extensions + * (like pg_ducklake) can check if an ALTER TABLE is in progress. + * This is needed because pgduckdb uses -fvisibility=hidden for C++ symbols. + */ +extern "C" __attribute__((visibility("default"))) bool +DuckdbIsAlterTableInProgress() { + return pgduckdb::top_level_duckdb_ddl_type == pgduckdb::DDLType::ALTER_TABLE; +} diff --git a/src/pgduckdb_ruleutils.cpp b/src/pgduckdb_ruleutils.cpp index 51ae6e5c..01d4fd67 100644 --- a/src/pgduckdb_ruleutils.cpp +++ b/src/pgduckdb_ruleutils.cpp @@ -2,6 +2,7 @@ #include "pgduckdb/pg/string_utils.hpp" #include "pgduckdb/pgduckdb_types.hpp" #include "pgduckdb/pgduckdb_ddl.hpp" +#include "pgduckdb/pgduckdb_table_am.hpp" #include "pgduckdb/pg/relations.hpp" #include "pgduckdb/pg/locale.hpp" @@ -884,7 +885,7 @@ cookConstraint(ParseState *pstate, Node *raw_constraint, char *relname) { return expr; } -char * +extern "C" __attribute__((visibility("default"))) char * pgduckdb_get_rename_relationdef(Oid relation_oid, RenameStmt *rename_stmt) { if (rename_stmt->renameType != OBJECT_TABLE && rename_stmt->renameType != OBJECT_VIEW && rename_stmt->renameType != OBJECT_COLUMN) { @@ -892,10 +893,18 @@ pgduckdb_get_rename_relationdef(Oid relation_oid, RenameStmt *rename_stmt) { } Relation relation = relation_open(relation_oid, AccessShareLock); - Assert(pgduckdb::IsDuckdbTable(relation) || pgduckdb::IsMotherDuckView(relation)); + Assert(pgduckdb::IsDuckdbTable(relation) || pgduckdb::IsMotherDuckView(relation) || + pgduckdb::DuckdbTableAmGetName(relation->rd_tableam) != nullptr); const char *postgres_schema_name = get_namespace_name_or_temp(relation->rd_rel->relnamespace); - const char *db_and_schema = pgduckdb_db_and_schema_string(postgres_schema_name, "duckdb"); + const char *duckdb_table_am_name = "duckdb"; + if (relation->rd_rel->relkind == RELKIND_RELATION) { + const char *table_am_name = pgduckdb::DuckdbTableAmGetName(relation->rd_tableam); + if (table_am_name != nullptr) { + duckdb_table_am_name = table_am_name; + } + } + const char *db_and_schema = pgduckdb_db_and_schema_string(postgres_schema_name, duckdb_table_am_name); const char *old_table_name = psprintf("%s.%s", db_and_schema, quote_identifier(rename_stmt->relation->relname)); const char *relation_type = "TABLE"; @@ -926,7 +935,7 @@ pgduckdb_get_rename_relationdef(Oid relation_oid, RenameStmt *rename_stmt) { * * TODO: Add support indexes */ -char * +extern "C" __attribute__((visibility("default"))) char * pgduckdb_get_alter_tabledef(Oid relation_oid, AlterTableStmt *alter_stmt) { Relation relation = relation_open(relation_oid, AccessShareLock); const char *relation_name = pgduckdb_relation_name(relation_oid); From a54d2170948b050e69865ccf71f183526a1289b7 Mon Sep 17 00:00:00 2001 From: qsliu Date: Wed, 25 Feb 2026 11:04:40 +0800 Subject: [PATCH 09/14] signal stuck workaround --- src/pgduckdb_duckdb.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pgduckdb_duckdb.cpp b/src/pgduckdb_duckdb.cpp index f9b3d7d5..950986f2 100644 --- a/src/pgduckdb_duckdb.cpp +++ b/src/pgduckdb_duckdb.cpp @@ -103,9 +103,6 @@ void DuckDBManager::Initialize() { elog(DEBUG2, "(PGDuckDB/DuckDBManager) Creating DuckDB instance"); - // Block signals before initializing DuckDB to ensure signal is handled by the Postgres main thread only - pgduckdb::ThreadSignalBlockGuard guard; - // Make sure directories provided in config exists std::filesystem::create_directories(duckdb_temporary_directory); std::filesystem::create_directories(duckdb_extension_directory); From dd759a53a77a6a2e887657ac58b1cab785f31220 Mon Sep 17 00:00:00 2001 From: qsliu Date: Mon, 2 Mar 2026 18:41:08 +0800 Subject: [PATCH 10/14] feat: add DuckdbAllowSubtransaction hook Exports DuckdbAllowSubtransaction(bool) so that extensions like pg_ducklake can temporarily suppress the SAVEPOINT check in the subxact callback while starting an internal PostgreSQL subtransaction as part of a DuckDB-coordinated metadata commit. Declared in pgduckdb_contracts.h for use by downstream extensions. --- src/pgduckdb_xact.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pgduckdb_xact.cpp b/src/pgduckdb_xact.cpp index dd7eff2a..c6ac6c00 100644 --- a/src/pgduckdb_xact.cpp +++ b/src/pgduckdb_xact.cpp @@ -30,6 +30,13 @@ DuckdbUnsafeSetNextExpectedCommandId(uint32_t command_id) { next_expected_command_id = command_id; } +static bool allow_subtransaction = false; + +extern "C" __attribute__((visibility("default"))) void +DuckdbAllowSubtransaction(bool allow) { + allow_subtransaction = allow; +} + namespace pg { static bool force_allow_writes; @@ -328,7 +335,7 @@ DuckdbSubXactCallback_Cpp(SubXactEvent event) { return; } - if (event == SUBXACT_EVENT_START_SUB) { + if (event == SUBXACT_EVENT_START_SUB && !allow_subtransaction) { throw duckdb::NotImplementedException("SAVEPOINT is not supported in DuckDB"); } } From df8109421e2e58805cb0aff92e5e0b00de20bb2f Mon Sep 17 00:00:00 2001 From: qsliu Date: Tue, 3 Mar 2026 10:18:12 +0800 Subject: [PATCH 11/14] chore: export DuckdbIsInitialized() Wraps DuckDBManager::IsInitialized() as an extern "C" visible symbol so downstream extensions can check whether DuckDB has been initialized in the current backend without triggering lazy initialization. Co-Authored-By: Claude Sonnet 4.6 --- src/pgduckdb_duckdb.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pgduckdb_duckdb.cpp b/src/pgduckdb_duckdb.cpp index 950986f2..7fdc9d42 100644 --- a/src/pgduckdb_duckdb.cpp +++ b/src/pgduckdb_duckdb.cpp @@ -99,6 +99,11 @@ RegisterDuckdbLoadExtension(DuckDBLoadExtension extension) { return true; } +extern "C" __attribute__((visibility("default"))) bool +DuckdbIsInitialized() { + return pgduckdb::DuckDBManager::IsInitialized(); +} + void DuckDBManager::Initialize() { elog(DEBUG2, "(PGDuckDB/DuckDBManager) Creating DuckDB instance"); From f66401219bab26d9db642960e18541300cafaad8 Mon Sep 17 00:00:00 2001 From: qsliu Date: Tue, 3 Mar 2026 17:05:24 +0800 Subject: [PATCH 12/14] feat: export GlobalProcessLock via C ABI Add DuckdbLockGlobalProcess/DuckdbUnlockGlobalProcess exports so pg_ducklake can acquire the lock across DSO boundaries without depending on the C++ header. Co-Authored-By: Claude Opus 4.6 --- src/pgduckdb_xact.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pgduckdb_xact.cpp b/src/pgduckdb_xact.cpp index c6ac6c00..01ad1aea 100644 --- a/src/pgduckdb_xact.cpp +++ b/src/pgduckdb_xact.cpp @@ -6,10 +6,21 @@ #include "pgduckdb/pgduckdb_hooks.hpp" #include "pgduckdb/pgduckdb_utils.hpp" #include "pgduckdb/pgduckdb_background_worker.hpp" +#include "pgduckdb/pgduckdb_process_lock.hpp" #include "pgduckdb/pg/transactions.hpp" #include "pgduckdb/utility/cpp_wrapper.hpp" +extern "C" __attribute__((visibility("default"))) void +DuckdbLockGlobalProcess(void) { + pgduckdb::GlobalProcessLock::GetLock().lock(); +} + +extern "C" __attribute__((visibility("default"))) void +DuckdbUnlockGlobalProcess(void) { + pgduckdb::GlobalProcessLock::GetLock().unlock(); +} + namespace pgduckdb { static CommandId next_expected_command_id = FirstCommandId; From cfcf30ed781a0da505229c25cce5b24bc7067f4d Mon Sep 17 00:00:00 2001 From: qsliu Date: Tue, 3 Mar 2026 19:03:38 +0800 Subject: [PATCH 13/14] feat: support time_travel function and expand duckdb.row columns for views Add "time_travel" to DuckDB-only function list. Expand duckdb.row columns to proper PostgreSQL types in CREATE VIEW for queries requiring DuckDB execution, so pg_attribute shows real column names and types. Co-Authored-By: Claude Opus 4.6 --- src/pgduckdb_ddl.cpp | 32 +++++++++++++++++++++++++++++++- src/pgduckdb_metadata_cache.cpp | 3 ++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/pgduckdb_ddl.cpp b/src/pgduckdb_ddl.cpp index 18326a64..623ea0dc 100644 --- a/src/pgduckdb_ddl.cpp +++ b/src/pgduckdb_ddl.cpp @@ -920,7 +920,37 @@ DuckdbHandleViewStmtPre(Node *parsetree, PlannedStmt *pstmt, const char *query_s } if (!pgduckdb::NeedsToBeMotherDuckView(stmt, schema_name)) { - // Let Postgres handle this view + /* + * For views over queries that require DuckDB execution (e.g., + * duckdb_only_functions like read_parquet, time_travel), expand + * duckdb.row columns to proper PostgreSQL types so that + * pg_attribute shows real column names and types. + * + * Skip the expensive parse_analyze when pg_duckdb has no registered + * functions/tables — pure-Postgres views need no rewriting. + */ + if (!pgduckdb::IsExtensionRegistered()) { + return false; + } + RawStmt *rawstmt = makeNode(RawStmt); + rawstmt->stmt = stmt->query; + rawstmt->stmt_location = pstmt->stmt_location; + rawstmt->stmt_len = pstmt->stmt_len; +#if PG_VERSION_NUM >= 150000 + Query *viewParse = parse_analyze_fixedparams(rawstmt, query_string, NULL, 0, NULL); +#else + Query *viewParse = parse_analyze(rawstmt, query_string, NULL, 0, NULL); +#endif + if (IsA(viewParse, Query) && viewParse->commandType == CMD_SELECT && + pgduckdb::NeedsDuckdbExecution(viewParse)) { + char *duckdb_query_string = pgduckdb_get_querydef((Query *)copyObjectImpl(viewParse)); + char *function_call = psprintf("duckdb.query(%s)", quote_literal_cstr(duckdb_query_string)); + RawStmt *wrapped_query = EntrenchColumnsFromCall(viewParse, function_call, &query_string); + MemoryContext query_context = GetMemoryChunkContext(stmt->query); + MemoryContext oldcontext = MemoryContextSwitchTo(query_context); + stmt->query = (Node *)copyObjectImpl(wrapped_query->stmt); + MemoryContextSwitchTo(oldcontext); + } return false; } diff --git a/src/pgduckdb_metadata_cache.cpp b/src/pgduckdb_metadata_cache.cpp index 76ff4c31..0d8e03ef 100644 --- a/src/pgduckdb_metadata_cache.cpp +++ b/src/pgduckdb_metadata_cache.cpp @@ -197,7 +197,8 @@ BuildDuckdbOnlyFunctions() { "map_keys", "map_values", /* pg_ducklake DuckDB-only functions */ - "options"}; + "options", + "time_travel"}; /* Also accept functions from pg_ducklake extension */ Oid ducklake_extension_oid = get_extension_oid("pg_ducklake", true); From 667ea0c6405ccdc83fd402e058e3209cced3d14d Mon Sep 17 00:00:00 2001 From: qsliu Date: Wed, 4 Mar 2026 21:48:41 +0800 Subject: [PATCH 14/14] feat: export hooks and type functions for ducklake_fdw - Register external table check and relation name callbacks so downstream extensions can route foreign tables through DuckDB - Export GetPostgresDuckDBType/Typemod with default visibility for column inference in pg_ducklake's FDW Co-Authored-By: Claude Opus 4.6 --- include/pgduckdb/pgduckdb_types.hpp | 4 ++-- src/pgduckdb_hooks.cpp | 17 ++++++++++++++++- src/pgduckdb_ruleutils.cpp | 14 ++++++++++++++ src/pgduckdb_types.cpp | 4 ++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/include/pgduckdb/pgduckdb_types.hpp b/include/pgduckdb/pgduckdb_types.hpp index d1edd4bc..a4eb9d26 100644 --- a/include/pgduckdb/pgduckdb_types.hpp +++ b/include/pgduckdb/pgduckdb_types.hpp @@ -32,8 +32,8 @@ constexpr int64_t PGDUCKDB_MIN_TIMESTAMP_VALUE = -210866803200000000; void CheckForUnsupportedPostgresType(duckdb::LogicalType type); duckdb::LogicalType ConvertPostgresToDuckColumnType(Form_pg_attribute &attribute); -Oid GetPostgresDuckDBType(const duckdb::LogicalType &type, bool throw_error = false); -int32_t GetPostgresDuckDBTypemod(const duckdb::LogicalType &type); +__attribute__((visibility("default"))) Oid GetPostgresDuckDBType(const duckdb::LogicalType &type, bool throw_error = false); +__attribute__((visibility("default"))) int32_t GetPostgresDuckDBTypemod(const duckdb::LogicalType &type); duckdb::Value ConvertPostgresParameterToDuckValue(Datum value, Oid postgres_type); void ConvertPostgresToDuckValue(Oid attr_type, Datum value, duckdb::Vector &result, uint64_t offset); bool ConvertDuckToPostgresValue(TupleTableSlot *slot, duckdb::Value &value, uint64_t col); diff --git a/src/pgduckdb_hooks.cpp b/src/pgduckdb_hooks.cpp index 8157c3b1..a4a762e6 100644 --- a/src/pgduckdb_hooks.cpp +++ b/src/pgduckdb_hooks.cpp @@ -43,6 +43,15 @@ static ExecutorFinish_hook_type prev_executor_finish_hook = NULL; static ExplainOneQuery_hook_type prev_explain_one_query_hook = NULL; static emit_log_hook_type prev_emit_log_hook = NULL; +typedef bool (*DuckdbExternalTableCheckFn)(Oid relid); +static std::vector external_table_checks; + +extern "C" __attribute__((visibility("default"))) bool +RegisterDuckdbExternalTableCheck(DuckdbExternalTableCheckFn callback) { + external_table_checks.push_back(callback); + return true; +} + static bool ContainsCatalogTable(List *rtes) { foreach_node(RangeTblEntry, rte, rtes) { @@ -67,7 +76,13 @@ ContainsCatalogTable(List *rtes) { static bool IsDuckdbTable(Oid relid) { - return pgduckdb::DuckdbTableAmGetName(relid) != nullptr; + if (pgduckdb::DuckdbTableAmGetName(relid) != nullptr) + return true; + for (auto &check : external_table_checks) { + if (check(relid)) + return true; + } + return false; } static bool diff --git a/src/pgduckdb_ruleutils.cpp b/src/pgduckdb_ruleutils.cpp index 01d4fd67..6b1e7a30 100644 --- a/src/pgduckdb_ruleutils.cpp +++ b/src/pgduckdb_ruleutils.cpp @@ -45,6 +45,14 @@ extern "C" { #include "pgduckdb/pgduckdb_metadata_cache.hpp" #include "pgduckdb/pgduckdb_userdata_cache.hpp" +typedef char *(*DuckdbRelationNameCallbackFn)(Oid relid); +static std::vector relation_name_callbacks; + +extern "C" __attribute__((visibility("default"))) void +RegisterDuckdbRelationNameCallback(DuckdbRelationNameCallbackFn callback) { + relation_name_callbacks.push_back(callback); +} + extern "C" { bool outermost_query = true; @@ -566,6 +574,12 @@ pgduckdb_db_and_schema_string(const char *postgres_schema_name, const char *duck */ extern "C" __attribute__((visibility("default"))) char * pgduckdb_relation_name(Oid relation_oid) { + for (auto &callback : relation_name_callbacks) { + char *name = callback(relation_oid); + if (name) + return name; + } + HeapTuple tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relation_oid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "cache lookup failed for relation %u", relation_oid); diff --git a/src/pgduckdb_types.cpp b/src/pgduckdb_types.cpp index 96bf2f3c..25ba2fbc 100644 --- a/src/pgduckdb_types.cpp +++ b/src/pgduckdb_types.cpp @@ -1543,7 +1543,7 @@ CheckForUnsupportedPostgresType(duckdb::LogicalType type) { } } -Oid +__attribute__((visibility("default"))) Oid GetPostgresDuckDBType(const duckdb::LogicalType &type, bool throw_error) { CheckForUnsupportedPostgresType(type); switch (type.id()) { @@ -1627,7 +1627,7 @@ GetPostgresDuckDBType(const duckdb::LogicalType &type, bool throw_error) { } } -int32 +__attribute__((visibility("default"))) int32 GetPostgresDuckDBTypemod(const duckdb::LogicalType &type) { switch (type.id()) { case duckdb::LogicalTypeId::DECIMAL: {