Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions include/pgduckdb/pgduckdb_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
50 changes: 47 additions & 3 deletions src/pgduckdb_ddl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -916,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;
}

Expand Down Expand Up @@ -1851,3 +1885,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;
}
24 changes: 21 additions & 3 deletions src/pgduckdb_duckdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,26 @@ 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 *db, void *context);
}
static std::vector<DuckDBLoadExtension> load_extensions;

extern "C" __attribute__((visibility("default"))) bool
RegisterDuckdbLoadExtension(DuckDBLoadExtension extension) {
load_extensions.push_back(extension);
return true;
}

extern "C" __attribute__((visibility("default"))) bool
DuckdbIsInitialized() {
return pgduckdb::DuckDBManager::IsInitialized();
}

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);
Expand Down Expand Up @@ -217,6 +230,10 @@ DuckDBManager::Initialize() {
}
}

for (auto extension : load_extensions) {
extension(database, &context);
}

if (duckdb_autoinstall_known_extensions) {
InstallExtensions(context);
}
Expand All @@ -231,6 +248,7 @@ DuckDBManager::Reset() {
UnclaimBgwSessionHint();
}


int64
GetSeqLastValue(const char *seq_name) {
Oid duckdb_namespace = get_namespace_oid("duckdb", false);
Expand Down
17 changes: 16 additions & 1 deletion src/pgduckdb_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<DuckdbExternalTableCheckFn> 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) {
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/pgduckdb_metadata_cache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,23 @@ BuildDuckdbOnlyFunctions() {
"map_extract_value",
"map_from_entries",
"map_keys",
"map_values"};
"map_values",
/* pg_ducklake DuckDB-only functions */
"options",
"time_travel"};

/* 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]));

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;
}

Expand Down
41 changes: 33 additions & 8 deletions src/pgduckdb_ruleutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -44,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<DuckdbRelationNameCallbackFn> relation_name_callbacks;

extern "C" __attribute__((visibility("default"))) void
RegisterDuckdbRelationNameCallback(DuckdbRelationNameCallbackFn callback) {
relation_name_callbacks.push_back(callback);
}

extern "C" {
bool outermost_query = true;

Expand Down Expand Up @@ -563,8 +572,14 @@ 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) {
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);
Expand Down Expand Up @@ -597,7 +612,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();
Expand All @@ -619,7 +634,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);
Expand Down Expand Up @@ -649,6 +664,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");
}
Expand Down Expand Up @@ -792,7 +809,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());
Expand Down Expand Up @@ -882,18 +899,26 @@ 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) {
elog(ERROR, "Only renaming tables and columns is supported in DuckDB");
}

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";
Expand Down Expand Up @@ -924,7 +949,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);
Expand Down
4 changes: 2 additions & 2 deletions src/pgduckdb_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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: {
Expand Down
35 changes: 34 additions & 1 deletion src/pgduckdb_xact.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,48 @@
#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;
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;
}

static bool allow_subtransaction = false;

extern "C" __attribute__((visibility("default"))) void
DuckdbAllowSubtransaction(bool allow) {
allow_subtransaction = allow;
}

namespace pg {

static bool force_allow_writes;
Expand Down Expand Up @@ -313,7 +346,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");
}
}
Expand Down