@@ -113,6 +113,7 @@ struct DuckDBAdbcStatementWrapper {
113113 duckdb_connection connection;
114114 duckdb_prepared_statement statement;
115115 char *ingestion_table_name;
116+ char *target_catalog;
116117 char *db_schema;
117118 ArrowArrayStream ingestion_stream;
118119 IngestionMode ingestion_mode = IngestionMode::CREATE;
@@ -725,16 +726,36 @@ void stream_schema(ArrowArrayStream *stream, ArrowSchema &schema) {
725726}
726727
727728// Helper function to build CREATE TABLE SQL statement
728- static std::string BuildCreateTableSQL (const char *schema, const char *table_name,
729+ static std::string BuildCreateTableSQL (const char *catalog, const char * schema, const char *table_name,
729730 const duckdb::vector<duckdb::LogicalType> &types,
730- const duckdb::vector<std::string> &names, bool if_not_exists = false ) {
731+ const duckdb::vector<std::string> &names, bool if_not_exists = false ,
732+ bool temporary = false , bool replace = false ) {
731733 std::ostringstream create_table;
732- create_table << " CREATE TABLE " ;
734+ if (replace) {
735+ create_table << " CREATE OR REPLACE " ;
736+ } else {
737+ create_table << " CREATE " ;
738+ }
739+ if (temporary) {
740+ create_table << " TEMP " ;
741+ }
742+ create_table << " TABLE " ;
733743 if (if_not_exists) {
734744 create_table << " IF NOT EXISTS " ;
735745 }
736- if (schema) {
737- create_table << duckdb::KeywordHelper::WriteOptionallyQuoted (schema) << " ." ;
746+ // Note: DuckDB resolves two-part names as either catalog.table (default schema)
747+ // or schema.table depending on context. This can become ambiguous if a schema and
748+ // an attached catalog share a name. Callers should prefer passing an explicit
749+ // schema (or defaulting to "main") to produce an unambiguous three-part name.
750+ // For TEMP tables, specifying catalog/schema in the CREATE statement is not allowed;
751+ // the table is automatically placed in the temp catalog.
752+ if (!temporary) {
753+ if (catalog) {
754+ create_table << duckdb::KeywordHelper::WriteOptionallyQuoted (catalog) << " ." ;
755+ }
756+ if (schema) {
757+ create_table << duckdb::KeywordHelper::WriteOptionallyQuoted (schema) << " ." ;
758+ }
738759 }
739760 create_table << duckdb::KeywordHelper::WriteOptionallyQuoted (table_name) << " (" ;
740761 for (idx_t i = 0 ; i < types.size (); i++) {
@@ -748,18 +769,7 @@ static std::string BuildCreateTableSQL(const char *schema, const char *table_nam
748769 return create_table.str ();
749770}
750771
751- // Helper function to build DROP TABLE IF EXISTS SQL statement
752- static std::string BuildDropTableSQL (const char *schema, const char *table_name) {
753- std::ostringstream drop_table;
754- drop_table << " DROP TABLE IF EXISTS " ;
755- if (schema) {
756- drop_table << duckdb::KeywordHelper::WriteOptionallyQuoted (schema) << " ." ;
757- }
758- drop_table << duckdb::KeywordHelper::WriteOptionallyQuoted (table_name);
759- return drop_table.str ();
760- }
761-
762- AdbcStatusCode Ingest (duckdb_connection connection, const char *table_name, const char *schema,
772+ AdbcStatusCode Ingest (duckdb_connection connection, const char *catalog, const char *table_name, const char *schema,
763773 struct ArrowArrayStream *input, struct AdbcError *error, IngestionMode ingestion_mode,
764774 bool temporary, int64_t *rows_affected) {
765775 if (!connection) {
@@ -775,10 +785,31 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *table_name, cons
775785 return ADBC_STATUS_INVALID_ARGUMENT;
776786 }
777787 if (schema && temporary) {
778- // Temporary option is not supported with ADBC_INGEST_OPTION_TARGET_DB_SCHEMA or
779- // ADBC_INGEST_OPTION_TARGET_CATALOG
788+ // Temporary option is not supported with ADBC_INGEST_OPTION_TARGET_DB_SCHEMA
780789 SetError (error, " Temporary option is not supported with schema" );
781- return ADBC_STATUS_INVALID_ARGUMENT;
790+ return ADBC_STATUS_INVALID_STATE;
791+ }
792+ if (catalog && temporary) {
793+ // Temporary option is not supported with ADBC_INGEST_OPTION_TARGET_CATALOG
794+ SetError (error, " Temporary option is not supported with catalog" );
795+ return ADBC_STATUS_INVALID_STATE;
796+ }
797+
798+ // Resolve target name parts.
799+ // Used for both SQL generation (CREATE/DROP) and appender lookup.
800+ // Prefer explicit three-part names; two-part names can be ambiguous.
801+ const char *effective_catalog = catalog;
802+ const char *effective_schema = schema;
803+ if (temporary) {
804+ // Temporary tables live in the special "temp" catalog.
805+ // "CREATE TEMP TABLE" automatically places tables in temp.main.
806+ // For the appender, we need to explicitly target the temp catalog.
807+ effective_catalog = " temp" ;
808+ effective_schema = nullptr ;
809+ } else if (catalog && !schema) {
810+ // Default schema for attached catalogs (DEFAULT_SCHEMA).
811+ // Use catalog.main.table to avoid catalog/schema name ambiguity.
812+ effective_schema = " main" ;
782813 }
783814
784815 duckdb::ArrowSchemaWrapper arrow_schema_wrapper;
@@ -800,7 +831,7 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *table_name, cons
800831 switch (ingestion_mode) {
801832 case IngestionMode::CREATE: {
802833 // CREATE mode: Create table, error if already exists
803- auto sql = BuildCreateTableSQL (schema, table_name, types, names);
834+ auto sql = BuildCreateTableSQL (effective_catalog, effective_schema, table_name, types, names, false , temporary );
804835 duckdb_result result;
805836 if (duckdb_query (connection, sql.c_str (), &result) == DuckDBError) {
806837 const char *error_msg = duckdb_result_error (&result);
@@ -820,16 +851,10 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *table_name, cons
820851 // The appender will naturally fail if the table doesn't exist
821852 break ;
822853 case IngestionMode::REPLACE: {
823- // REPLACE mode: Drop table if exists, then create
824- auto drop_sql = BuildDropTableSQL (schema, table_name);
825- auto create_sql = BuildCreateTableSQL (schema, table_name, types, names);
854+ // REPLACE mode: CREATE OR REPLACE TABLE
855+ auto create_sql =
856+ BuildCreateTableSQL (effective_catalog, effective_schema, table_name, types, names, false , temporary, true );
826857 duckdb_result result;
827- if (duckdb_query (connection, drop_sql.c_str (), &result) == DuckDBError) {
828- SetError (error, duckdb_result_error (&result));
829- duckdb_destroy_result (&result);
830- return ADBC_STATUS_INTERNAL;
831- }
832- duckdb_destroy_result (&result);
833858 if (duckdb_query (connection, create_sql.c_str (), &result) == DuckDBError) {
834859 SetError (error, duckdb_result_error (&result));
835860 duckdb_destroy_result (&result);
@@ -840,7 +865,7 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *table_name, cons
840865 }
841866 case IngestionMode::CREATE_APPEND: {
842867 // CREATE_APPEND mode: Create if not exists, append if exists
843- auto sql = BuildCreateTableSQL (schema, table_name, types, names, true );
868+ auto sql = BuildCreateTableSQL (effective_catalog, effective_schema, table_name, types, names, true , temporary );
844869 duckdb_result result;
845870 if (duckdb_query (connection, sql.c_str (), &result) == DuckDBError) {
846871 SetError (error, duckdb_result_error (&result));
@@ -851,7 +876,7 @@ AdbcStatusCode Ingest(duckdb_connection connection, const char *table_name, cons
851876 break ;
852877 }
853878 }
854- AppenderWrapper appender (connection, schema , table_name);
879+ AppenderWrapper appender (connection, effective_catalog, effective_schema , table_name);
855880 if (!appender.Valid ()) {
856881 return ADBC_STATUS_INTERNAL;
857882 }
@@ -919,6 +944,7 @@ AdbcStatusCode StatementNew(struct AdbcConnection *connection, struct AdbcStatem
919944 statement_wrapper->statement = nullptr ;
920945 statement_wrapper->ingestion_stream .release = nullptr ;
921946 statement_wrapper->ingestion_table_name = nullptr ;
947+ statement_wrapper->target_catalog = nullptr ;
922948 statement_wrapper->db_schema = nullptr ;
923949 statement_wrapper->temporary_table = false ;
924950
@@ -943,6 +969,10 @@ AdbcStatusCode StatementRelease(struct AdbcStatement *statement, struct AdbcErro
943969 free (wrapper->ingestion_table_name );
944970 wrapper->ingestion_table_name = nullptr ;
945971 }
972+ if (wrapper->target_catalog ) {
973+ free (wrapper->target_catalog );
974+ wrapper->target_catalog = nullptr ;
975+ }
946976 if (wrapper->db_schema ) {
947977 free (wrapper->db_schema );
948978 wrapper->db_schema = nullptr ;
@@ -1019,8 +1049,9 @@ static AdbcStatusCode IngestToTableFromBoundStream(DuckDBAdbcStatementWrapper *s
10191049 auto stream = statement->ingestion_stream ;
10201050
10211051 // Ingest into a table from the bound stream
1022- return Ingest (statement->connection , statement->ingestion_table_name , statement->db_schema , &stream, error,
1023- statement->ingestion_mode , statement->temporary_table , rows_affected);
1052+ return Ingest (statement->connection , statement->target_catalog , statement->ingestion_table_name ,
1053+ statement->db_schema , &stream, error, statement->ingestion_mode , statement->temporary_table ,
1054+ rows_affected);
10241055}
10251056
10261057AdbcStatusCode StatementExecuteQuery (struct AdbcStatement *statement, struct ArrowArrayStream *out,
@@ -1354,15 +1385,25 @@ AdbcStatusCode StatementSetOption(struct AdbcStatement *statement, const char *k
13541385 auto wrapper = static_cast <DuckDBAdbcStatementWrapper *>(statement->private_data );
13551386
13561387 if (strcmp (key, ADBC_INGEST_OPTION_TARGET_TABLE) == 0 ) {
1388+ if (wrapper->ingestion_table_name ) {
1389+ free (wrapper->ingestion_table_name );
1390+ }
13571391 wrapper->ingestion_table_name = strdup (value);
1358- wrapper->temporary_table = false ;
13591392 return ADBC_STATUS_OK;
13601393 }
13611394 if (strcmp (key, ADBC_INGEST_OPTION_TEMPORARY) == 0 ) {
13621395 if (strcmp (value, ADBC_OPTION_VALUE_ENABLED) == 0 ) {
1396+ // Align with arrow-adbc PostgreSQL driver behavior: if a schema was set
1397+ // before enabling temporary ingestion, clear it so temporary can proceed.
1398+ // (Some clients set schema by default.)
13631399 if (wrapper->db_schema ) {
1364- SetError (error, " Temporary option is not supported with schema" );
1365- return ADBC_STATUS_INVALID_ARGUMENT;
1400+ free (wrapper->db_schema );
1401+ wrapper->db_schema = nullptr ;
1402+ }
1403+ // Some clients may also set a catalog by default; clear it so temporary can proceed.
1404+ if (wrapper->target_catalog ) {
1405+ free (wrapper->target_catalog );
1406+ wrapper->target_catalog = nullptr ;
13661407 }
13671408 wrapper->temporary_table = true ;
13681409 return ADBC_STATUS_OK;
@@ -1378,14 +1419,21 @@ AdbcStatusCode StatementSetOption(struct AdbcStatement *statement, const char *k
13781419 }
13791420
13801421 if (strcmp (key, ADBC_INGEST_OPTION_TARGET_DB_SCHEMA) == 0 ) {
1381- if (wrapper->temporary_table ) {
1382- SetError (error, " Temporary option is not supported with schema" );
1383- return ADBC_STATUS_INVALID_ARGUMENT;
1422+ if (wrapper->db_schema ) {
1423+ free (wrapper->db_schema );
13841424 }
13851425 wrapper->db_schema = strdup (value);
13861426 return ADBC_STATUS_OK;
13871427 }
13881428
1429+ if (strcmp (key, ADBC_INGEST_OPTION_TARGET_CATALOG) == 0 ) {
1430+ if (wrapper->target_catalog ) {
1431+ free (wrapper->target_catalog );
1432+ }
1433+ wrapper->target_catalog = strdup (value);
1434+ return ADBC_STATUS_OK;
1435+ }
1436+
13891437 if (strcmp (key, ADBC_INGEST_OPTION_MODE) == 0 ) {
13901438 if (strcmp (value, ADBC_INGEST_OPTION_MODE_CREATE) == 0 ) {
13911439 wrapper->ingestion_mode = IngestionMode::CREATE;
0 commit comments