diff --git a/include/pgduckdb/pgduckdb_ddl.hpp b/include/pgduckdb/pgduckdb_ddl.hpp index 46432079..8691b508 100644 --- a/include/pgduckdb/pgduckdb_ddl.hpp +++ b/include/pgduckdb/pgduckdb_ddl.hpp @@ -2,5 +2,11 @@ #include "pgduckdb/pg/declarations.hpp" +namespace pgduckdb { +enum class DDLType { NONE, CREATE_TABLE, ALTER_TABLE }; +/* Tracks the type of DDL statement that is currently being executed */ +extern DDLType top_level_duckdb_ddl_type; +} // namespace pgduckdb + void DuckdbTruncateTable(Oid relation_oid); void DuckdbInitUtilityHook(); diff --git a/include/pgduckdb/pgduckdb_ruleutils.h b/include/pgduckdb/pgduckdb_ruleutils.h index 4955c424..888f2034 100644 --- a/include/pgduckdb/pgduckdb_ruleutils.h +++ b/include/pgduckdb/pgduckdb_ruleutils.h @@ -12,6 +12,8 @@ char *pgduckdb_relation_name(Oid relid); char *pgduckdb_function_name(Oid function_oid, bool *use_variadic_p); char *pgduckdb_get_querydef(Query *); char *pgduckdb_get_tabledef(Oid relation_id); +char *pgduckdb_get_alter_tabledef(Oid relation_oid, AlterTableStmt *alter_stmt); +char *pgduckdb_get_rename_tabledef(Oid relation_oid, RenameStmt *rename_stmt); bool pgduckdb_is_not_default_expr(Node *node, void *context); List *pgduckdb_db_and_schema(const char *postgres_schema_name, bool is_duckdb_table); const char *pgduckdb_db_and_schema_string(const char *postgres_schema_name, bool is_duckdb_table); diff --git a/src/pgduckdb.cpp b/src/pgduckdb.cpp index 926e3cdc..e236e7d3 100644 --- a/src/pgduckdb.cpp +++ b/src/pgduckdb.cpp @@ -10,6 +10,7 @@ extern "C" { #include "pgduckdb/pgduckdb.h" #include "pgduckdb/pgduckdb_node.hpp" #include "pgduckdb/pgduckdb_background_worker.hpp" +#include "pgduckdb/pgduckdb_metadata_cache.hpp" #include "pgduckdb/pgduckdb_xact.hpp" static void DuckdbInitGUC(void); diff --git a/src/pgduckdb_ddl.cpp b/src/pgduckdb_ddl.cpp index 4208c913..11b81cc6 100644 --- a/src/pgduckdb_ddl.cpp +++ b/src/pgduckdb_ddl.cpp @@ -2,6 +2,7 @@ #include "pgduckdb/pgduckdb_utils.hpp" #include "pgduckdb/pgduckdb_xact.hpp" #include "pgduckdb/pgduckdb_guc.h" +#include "pgduckdb/pgduckdb_ddl.hpp" extern "C" { #include "postgres.h" @@ -32,6 +33,7 @@ extern "C" { #include "pgduckdb/pgduckdb_ruleutils.h" } +#include "pgduckdb/pgduckdb_guc.h" #include "pgduckdb/utility/cpp_wrapper.hpp" #include "pgduckdb/pgduckdb_duckdb.hpp" #include "pgduckdb/pgduckdb_background_worker.hpp" @@ -40,6 +42,10 @@ extern "C" { #include "pgduckdb/vendor/pg_list.hpp" #include +namespace pgduckdb { +DDLType top_level_duckdb_ddl_type = DDLType::NONE; +} // namespace pgduckdb + /* * ctas_skip_data stores the original value of the skipData field of the * CreateTableAsStmt of the query that's currently being executed. For duckdb @@ -181,6 +187,11 @@ DuckdbHandleDDL(PlannedStmt *pstmt, const char *query_string, ParamListInfo para char *access_method = stmt->accessMethod ? stmt->accessMethod : default_table_access_method; bool is_duckdb_table = strcmp(access_method, "duckdb") == 0; if (is_duckdb_table) { + if (pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::NONE) { + ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("Only one DuckDB table can be created in a single statement"))); + } + pgduckdb::top_level_duckdb_ddl_type = pgduckdb::DDLType::CREATE_TABLE; pgduckdb::ClaimCurrentCommandId(); } @@ -202,6 +213,11 @@ DuckdbHandleDDL(PlannedStmt *pstmt, const char *query_string, ParamListInfo para char *access_method = stmt->into->accessMethod ? stmt->into->accessMethod : default_table_access_method; bool is_duckdb_table = strcmp(access_method, "duckdb") == 0; if (is_duckdb_table) { + if (pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::NONE) { + ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("Only one DuckDB table can be created in a single statement"))); + } + pgduckdb::top_level_duckdb_ddl_type = pgduckdb::DDLType::CREATE_TABLE; pgduckdb::ClaimCurrentCommandId(); /* * Force skipData to false for duckdb tables, so that Postgres does @@ -313,6 +329,21 @@ DuckdbHandleDDL(PlannedStmt *pstmt, const char *query_string, ParamListInfo para break; } return; + } else if (IsA(parsetree, AlterTableStmt)) { + auto stmt = castNode(AlterTableStmt, parsetree); + Oid relation_oid = RangeVarGetRelid(stmt->relation, AccessShareLock, false); + Relation relation = RelationIdGetRelation(relation_oid); + /* + * Certain CREATE TABLE commands also trigger an ALTER TABLE command, + * specifically if you use REFERENCES it will alter the table + * 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) { + pgduckdb::top_level_duckdb_ddl_type = pgduckdb::DDLType::ALTER_TABLE; + pgduckdb::ClaimCurrentCommandId(); + } + RelationClose(relation); } } @@ -416,6 +447,15 @@ DECLARE_PG_FUNCTION(duckdb_create_table_trigger) { PG_RETURN_NULL(); } + if (pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::CREATE_TABLE && + pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::NONE) { + ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("Cannot create a DuckDB table this way, use CREATE TABLE or CREATE TABLE ... AS"))); + PG_RETURN_NULL(); + } + /* Reset it back to NONE, for the remainder of the event trigger */ + pgduckdb::top_level_duckdb_ddl_type = pgduckdb::DDLType::NONE; + EventTriggerData *trigger_data = (EventTriggerData *)fcinfo->context; Node *parsetree = trigger_data->parsetree; @@ -859,6 +899,16 @@ DECLARE_PG_FUNCTION(duckdb_alter_table_trigger) { PG_RETURN_NULL(); } + /* Reset since we don't need it anymore */ + if (pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::ALTER_TABLE && + pgduckdb::top_level_duckdb_ddl_type != pgduckdb::DDLType::NONE) { + ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("Cannot ALTER a DuckDB table this way, please use ALTER TABLE"))); + PG_RETURN_NULL(); + } + /* Reset it back to NONE, for the remainder of the event trigger */ + pgduckdb::top_level_duckdb_ddl_type = pgduckdb::DDLType::NONE; + SPI_connect(); /* @@ -889,20 +939,20 @@ DECLARE_PG_FUNCTION(duckdb_alter_table_trigger) { FROM pg_catalog.pg_event_trigger_ddl_commands() cmds JOIN pg_catalog.pg_class ON cmds.objid = pg_class.oid - WHERE cmds.object_type = 'table' + WHERE cmds.object_type in ('table', 'table column') AND pg_class.relam = (SELECT oid FROM pg_am WHERE amname = 'duckdb') UNION ALL SELECT objid as relid, false AS needs_to_check_temporary_set FROM pg_catalog.pg_event_trigger_ddl_commands() cmds JOIN duckdb.tables AS ddbtables ON cmds.objid = ddbtables.relid - WHERE cmds.object_type = 'table' + WHERE cmds.object_type in ('table', 'table column') UNION ALL SELECT objid as relid, true AS needs_to_check_temporary_set FROM pg_catalog.pg_event_trigger_ddl_commands() cmds JOIN pg_catalog.pg_class ON cmds.objid = pg_class.oid - WHERE cmds.object_type = 'table' + WHERE cmds.object_type in ('table', 'table column') AND pg_class.relam != (SELECT oid FROM pg_am WHERE amname = 'duckdb') AND pg_class.relpersistence = 't' )", @@ -946,7 +996,29 @@ DECLARE_PG_FUNCTION(duckdb_alter_table_trigger) { } } - elog(ERROR, "DuckDB does not support ALTER TABLE yet"); + /* Forcibly allow whatever writes Postgres did for this command */ + pgduckdb::ClaimCurrentCommandId(true); + + /* We're going to run multiple queries in DuckDB, so we need to start a + * transaction to ensure ACID guarantees hold. */ + auto connection = pgduckdb::DuckDBManager::GetConnection(true); + + EventTriggerData *trigdata = (EventTriggerData *)fcinfo->context; + char *alter_table_stmt_string; + if (IsA(trigdata->parsetree, AlterTableStmt)) { + AlterTableStmt *alter_table_stmt = (AlterTableStmt *)trigdata->parsetree; + alter_table_stmt_string = pgduckdb_get_alter_tabledef(relid, alter_table_stmt); + } else if (IsA(trigdata->parsetree, RenameStmt)) { + RenameStmt *rename_stmt = (RenameStmt *)trigdata->parsetree; + alter_table_stmt_string = pgduckdb_get_rename_tabledef(relid, rename_stmt); + } else { + elog(ERROR, "Unexpected parsetree type: %d", nodeTag(trigdata->parsetree)); + } + + elog(DEBUG1, "Executing: %s", alter_table_stmt_string); + auto res = pgduckdb::DuckDBQueryOrThrow(*connection, alter_table_stmt_string); + + PG_RETURN_NULL(); } /* diff --git a/src/pgduckdb_ruleutils.cpp b/src/pgduckdb_ruleutils.cpp index d3cf7b7d..5e1799d3 100644 --- a/src/pgduckdb_ruleutils.cpp +++ b/src/pgduckdb_ruleutils.cpp @@ -6,16 +6,24 @@ extern "C" { #include "access/relation.h" #include "access/htup_details.h" #include "catalog/pg_class.h" +#include "catalog/heap.h" #include "catalog/pg_collation.h" #include "commands/dbcommands.h" +#include "commands/tablecmds.h" #include "nodes/nodeFuncs.h" +#include "nodes/parsenodes.h" #include "lib/stringinfo.h" #include "parser/parsetree.h" +#include "parser/parse_coerce.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/relcache.h" #include "utils/rel.h" +#include "nodes/print.h" #include "utils/rls.h" #include "utils/syscache.h" #include "storage/lockdefs.h" @@ -752,7 +760,359 @@ pgduckdb_get_tabledef(Oid relation_oid) { relation_close(relation, AccessShareLock); - return (buffer.data); + return buffer.data; +} +Form_pg_attribute +GetAttributeByName(TupleDesc tupdesc, const char *colname) { + for (int i = 0; i < tupdesc->natts; i++) { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + if (strcmp(NameStr(attr->attname), colname) == 0) { + return attr; + } + } + return NULL; // Return NULL if the column name is not found +} + +/* + * Take a raw CHECK constraint expression and convert it to a cooked format + * ready for storage. + * + * Parse state must be set up to recognize any vars that might appear + * in the expression. + * + * Vendored in from src/backend/catalog/heap.c + */ +static Node * +cookConstraint(ParseState *pstate, Node *raw_constraint, char *relname) { + Node *expr; + + /* + * Transform raw parsetree to executable expression. + */ + expr = transformExpr(pstate, raw_constraint, EXPR_KIND_CHECK_CONSTRAINT); + + /* + * Make sure it yields a boolean result. + */ + expr = coerce_to_boolean(pstate, expr, "CHECK"); + + /* + * Take care of collations. + */ + assign_expr_collations(pstate, expr); + + /* + * Make sure no outside relations are referred to (this is probably dead + * code now that add_missing_from is history). + */ + if (list_length(pstate->p_rtable) != 1) + ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("only table \"%s\" can be referenced in check constraint", relname))); + + return expr; +} + +char * +pgduckdb_get_rename_tabledef(Oid relation_oid, RenameStmt *rename_stmt) { + if (rename_stmt->renameType != OBJECT_TABLE && 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)); + + 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, true); + const char *old_table_name = psprintf("%s.%s", db_and_schema, quote_identifier(rename_stmt->relation->relname)); + + StringInfoData buffer; + initStringInfo(&buffer); + + if (rename_stmt->subname) { + appendStringInfo(&buffer, "ALTER TABLE %s RENAME COLUMN %s TO %s;", old_table_name, + quote_identifier(rename_stmt->subname), quote_identifier(rename_stmt->newname)); + + } else { + appendStringInfo(&buffer, "ALTER TABLE %s RENAME TO %s;", old_table_name, + quote_identifier(rename_stmt->newname)); + } + + relation_close(relation, AccessShareLock); + + return buffer.data; +} + +/* + * pgduckdb_get_alter_tabledef returns the DuckDB version of an ALTER TABLE + * command for the given table. + * + * TODO: Add support indexes + */ +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); + + StringInfoData buffer; + initStringInfo(&buffer); + + if (get_rel_relkind(relation_oid) != RELKIND_RELATION) { + elog(ERROR, "Only regular tables are supported in DuckDB"); + } + + if (list_length(RelationGetFKeyList(relation)) > 0) { + elog(ERROR, "DuckDB tables do not support foreign keys"); + } + + List *relation_context = pgduckdb_deparse_context_for(relation_name, relation_oid); + ParseState *pstate = make_parsestate(NULL); + ParseNamespaceItem *nsitem = addRangeTableEntryForRelation(pstate, relation, AccessShareLock, NULL, false, true); + addNSItemToQuery(pstate, nsitem, true, true, true); + + foreach_node(AlterTableCmd, cmd, alter_stmt->cmds) { + /* + * DuckDB does not support doing multiple ALTER TABLE commands in + * one statement, so we split them up. + */ + appendStringInfo(&buffer, "ALTER TABLE %s ", relation_name); + + switch (cmd->subtype) { + case AT_AddColumn: { + ColumnDef *col = castNode(ColumnDef, cmd->def); + TupleDesc tupdesc = BuildDescForRelation(list_make1(col)); + Form_pg_attribute attribute = TupleDescAttr(tupdesc, 0); + const char *column_fq_type = format_type_with_typemod(attribute->atttypid, attribute->atttypmod); + + appendStringInfo(&buffer, "ADD COLUMN %s %s", quote_identifier(col->colname), column_fq_type); + foreach_node(Constraint, constraint, col->constraints) { + switch (constraint->contype) { + case CONSTR_NULL: { + appendStringInfoString(&buffer, " NULL"); + break; + } + case CONSTR_NOTNULL: { + appendStringInfoString(&buffer, " NOT NULL"); + break; + } + case CONSTR_DEFAULT: { + if (constraint->raw_expr) { + auto expr = cookDefault(pstate, constraint->raw_expr, attribute->atttypid, attribute->atttypmod, + col->colname, attribute->attgenerated); + char *default_string = pgduckdb_deparse_expression(expr, relation_context, false, false); + appendStringInfo(&buffer, " DEFAULT %s", default_string); + } + break; + } + case CONSTR_CHECK: { + appendStringInfo(&buffer, "CHECK "); + + auto expr = cookConstraint(pstate, constraint->raw_expr, RelationGetRelationName(relation)); + + char *check_string = pgduckdb_deparse_expression(expr, relation_context, false, false); + + appendStringInfo(&buffer, "(%s); ", check_string); + break; + } + case CONSTR_PRIMARY: { + appendStringInfoString(&buffer, " PRIMARY KEY"); + break; + } + case CONSTR_UNIQUE: { + appendStringInfoString(&buffer, " UNIQUE"); + break; + } + default: + elog(ERROR, "pg_duckdb does not support this ALTER TABLE yet"); + } + } + + if (col->collClause || col->collOid != InvalidOid) { + elog(ERROR, "Column collations are not supported in DuckDB"); + } + + appendStringInfoString(&buffer, "; "); + break; + } + + case AT_AlterColumnType: { + const char *column_name = cmd->name; + ColumnDef *col = castNode(ColumnDef, cmd->def); + TupleDesc tupdesc = BuildDescForRelation(list_make1(col)); + Form_pg_attribute attribute = TupleDescAttr(tupdesc, 0); + const char *column_fq_type = format_type_with_typemod(attribute->atttypid, attribute->atttypmod); + /* TODO: Disallow after SET DEFAULT/ADD CHECK CONSTRAINTin the same ALTER command */ + + appendStringInfo(&buffer, "ALTER COLUMN %s TYPE %s; ", quote_identifier(column_name), column_fq_type); + break; + } + + case AT_DropColumn: { + appendStringInfo(&buffer, "DROP COLUMN %s", quote_identifier(cmd->name)); + + /* Add CASCADE or RESTRICT if specified */ + if (cmd->behavior == DROP_CASCADE) { + appendStringInfoString(&buffer, " CASCADE"); + } else if (cmd->behavior == DROP_RESTRICT) { + appendStringInfoString(&buffer, " RESTRICT"); + } + + appendStringInfoString(&buffer, "; "); + break; + } + + case AT_ColumnDefault: { + const char *column_name = cmd->name; + TupleDesc tupdesc = RelationGetDescr(relation); + Form_pg_attribute attribute = GetAttributeByName(tupdesc, column_name); + if (!attribute) { + elog(ERROR, "Column %s not found in table %s", column_name, relation_name); + } + + appendStringInfo(&buffer, "ALTER COLUMN %s ", quote_identifier(cmd->name)); + + if (cmd->def) { + auto expr = cookDefault(pstate, cmd->def, attribute->atttypid, attribute->atttypmod, column_name, + attribute->attgenerated); + char *default_string = pgduckdb_deparse_expression(expr, relation_context, false, false); + appendStringInfo(&buffer, "SET DEFAULT %s; ", default_string); + } else { + appendStringInfoString(&buffer, "DROP DEFAULT; "); + } + break; + } + + case AT_DropNotNull: { + appendStringInfo(&buffer, "ALTER COLUMN %s DROP NOT NULL; ", quote_identifier(cmd->name)); + break; + } + + case AT_SetNotNull: { + appendStringInfo(&buffer, "ALTER COLUMN %s SET NOT NULL; ", quote_identifier(cmd->name)); + break; + } + + case AT_AddConstraint: { + pprint(cmd); + Constraint *constraint = castNode(Constraint, cmd->def); + + appendStringInfoString(&buffer, "ADD "); + + switch (constraint->contype) { + case CONSTR_CHECK: { + appendStringInfo(&buffer, "CONSTRAINT %s CHECK ", + quote_identifier(constraint->conname ? constraint->conname : "")); + + auto expr = cookConstraint(pstate, constraint->raw_expr, RelationGetRelationName(relation)); + + char *check_string = pgduckdb_deparse_expression(expr, relation_context, false, false); + + appendStringInfo(&buffer, "(%s); ", check_string); + break; + } + + case CONSTR_PRIMARY: { + appendStringInfoString(&buffer, "PRIMARY KEY ("); + ListCell *cell; + bool first = true; + foreach (cell, constraint->keys) { + char *key = strVal(lfirst(cell)); + if (!first) { + appendStringInfoString(&buffer, ", "); + } + appendStringInfoString(&buffer, quote_identifier(key)); + first = false; + } + appendStringInfoString(&buffer, "); "); + break; + } + + case CONSTR_UNIQUE: { + appendStringInfoString(&buffer, "UNIQUE ("); + ListCell *ucell; + bool ufirst = true; + foreach (ucell, constraint->keys) { + char *key = strVal(lfirst(ucell)); + if (!ufirst) { + appendStringInfoString(&buffer, ", "); + } + appendStringInfoString(&buffer, quote_identifier(key)); + ufirst = false; + } + appendStringInfoString(&buffer, "); "); + break; + } + + default: { + elog(ERROR, "DuckDB does not support this constraint type"); + break; + } + } + break; + } + + case AT_DropConstraint: { + appendStringInfo(&buffer, "DROP CONSTRAINT %s", quote_identifier(cmd->name)); + + /* Add CASCADE or RESTRICT if specified */ + if (cmd->behavior == DROP_CASCADE) { + appendStringInfoString(&buffer, " CASCADE"); + } else if (cmd->behavior == DROP_RESTRICT) { + appendStringInfoString(&buffer, " RESTRICT"); + } + + appendStringInfoString(&buffer, "; "); + break; + } + + case AT_SetRelOptions: + case AT_ResetRelOptions: { + List *options = (List *)cmd->def; + bool is_set = (cmd->subtype == AT_SetRelOptions); + + if (is_set) { + appendStringInfoString(&buffer, "SET ("); + } else { + appendStringInfoString(&buffer, "RESET ("); + } + + ListCell *cell; + bool first = true; + foreach (cell, options) { + DefElem *def = (DefElem *)lfirst(cell); + if (!first) { + appendStringInfoString(&buffer, ", "); + } + + appendStringInfoString(&buffer, quote_identifier(def->defname)); + + if (is_set && def->arg) { + char *val = NULL; + if (IsA(def->arg, String)) { + val = strVal(def->arg); + appendStringInfo(&buffer, " = %s", quote_literal_cstr(val)); + } else if (IsA(def->arg, Integer)) { + val = psprintf("%d", intVal(def->arg)); + appendStringInfo(&buffer, " = %s", val); + } else { + elog(ERROR, "Unsupported option value type"); + } + } + + first = false; + } + + appendStringInfoString(&buffer, "); "); + break; + } + + default: + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("DuckDB does not support this ALTER TABLE command"))); + } + } + relation_close(relation, AccessShareLock); + + return buffer.data; } /* diff --git a/src/pgduckdb_table_am.cpp b/src/pgduckdb_table_am.cpp index e16f8038..2157ba10 100644 --- a/src/pgduckdb_table_am.cpp +++ b/src/pgduckdb_table_am.cpp @@ -18,6 +18,7 @@ extern "C" { #include "access/tableam.h" #include "access/heapam.h" #include "access/amapi.h" +#include "access/xact.h" #include "catalog/index.h" #include "commands/vacuum.h" #include "executor/tuptable.h" @@ -88,7 +89,12 @@ duckdb_scan_rescan(TableScanDesc /*sscan*/, ScanKey /*key*/, bool /*set_params*/ } static bool -duckdb_scan_getnextslot(TableScanDesc /*sscan*/, ScanDirection /*direction*/, TupleTableSlot * /*slot*/) { +duckdb_scan_getnextslot(TableScanDesc /*sscan*/, ScanDirection /*direction*/, TupleTableSlot *slot) { + /* If we are executing ALTER TABLE we return empty tuple */ + if (pgduckdb::top_level_duckdb_ddl_type == pgduckdb::DDLType::ALTER_TABLE) { + ExecClearTuple(slot); + return false; + } NOT_IMPLEMENTED(); } @@ -213,6 +219,9 @@ duckdb_tuple_lock(Relation /*relation*/, ItemPointer /*tid*/, Snapshot /*snapsho static void duckdb_finish_bulk_insert(Relation /*relation*/, int /*options*/) { + if (pgduckdb::top_level_duckdb_ddl_type == pgduckdb::DDLType::ALTER_TABLE) { + return; + } NOT_IMPLEMENTED(); } @@ -314,6 +323,9 @@ duckdb_index_build_range_scan(Relation /*tableRelation*/, Relation /*indexRelati bool /*allow_sync*/, bool /*anyvisible*/, bool /*progress*/, BlockNumber /*start_blockno*/, BlockNumber /*numblocks*/, IndexBuildCallback /*callback*/, void * /*callback_state*/, TableScanDesc /*scan*/) { + if (pgduckdb::top_level_duckdb_ddl_type == pgduckdb::DDLType::ALTER_TABLE) { + return 0; + } NOT_IMPLEMENTED(); } diff --git a/src/pgduckdb_xact.cpp b/src/pgduckdb_xact.cpp index b0ef4cc6..73f078bf 100644 --- a/src/pgduckdb_xact.cpp +++ b/src/pgduckdb_xact.cpp @@ -1,4 +1,5 @@ #include "duckdb/common/exception.hpp" +#include "pgduckdb/pgduckdb_ddl.hpp" #include "pgduckdb/pgduckdb_duckdb.hpp" #include "pgduckdb/pgduckdb_guc.h" #include "pgduckdb/pgduckdb_xact.hpp" @@ -231,6 +232,7 @@ DuckdbXactCallback_Cpp(XactEvent event) { case XACT_EVENT_PARALLEL_PRE_COMMIT: CheckForDisallowedMixedWrites(); + top_level_duckdb_ddl_type = DDLType::NONE; top_level_statement = true; next_expected_command_id = FirstCommandId; pg::force_allow_writes = false; @@ -247,6 +249,7 @@ DuckdbXactCallback_Cpp(XactEvent event) { case XACT_EVENT_ABORT: case XACT_EVENT_PARALLEL_ABORT: + top_level_duckdb_ddl_type = DDLType::NONE; top_level_statement = true; next_expected_command_id = FirstCommandId; pg::force_allow_writes = false; diff --git a/test/pycheck/motherduck_test.py b/test/pycheck/motherduck_test.py index 28910d6e..4b35d448 100644 --- a/test/pycheck/motherduck_test.py +++ b/test/pycheck/motherduck_test.py @@ -6,7 +6,7 @@ library for that purpose. """ -from .utils import Cursor, MOTHERDUCK +from .utils import Cursor, MOTHERDUCK, PG_MAJOR_VERSION import pytest import psycopg.errors @@ -57,3 +57,31 @@ def test_md_ctas(md_cur: Cursor, ddb): match=r"Creating a non-DuckDB table in a ddb\$ schema is not supported", ): md_cur.sql("CREATE TABLE t4(b) USING heap AS SELECT 4 a") + + +def test_md_alter_table(md_cur: Cursor): + md_cur.sql("CREATE TABLE t(a) USING duckdb AS SELECT 1") + # We disallow most ALTER TABLE commands on duckdb tables + with pytest.raises(psycopg.errors.FeatureNotSupported): + md_cur.sql("ALTER TABLE t FORCE ROW LEVEL SECURITY") + + md_cur.sql("ALTER TABLE t ADD COLUMN b int DEFAULT 100") + assert md_cur.sql("SELECT * FROM t") == (1, 100) + + if PG_MAJOR_VERSION >= 15: + # We specifically want to disallow changing the access method + with pytest.raises(psycopg.errors.FeatureNotSupported): + md_cur.sql("ALTER TABLE t SET ACCESS METHOD heap") + + with pytest.raises(psycopg.errors.FeatureNotSupported): + md_cur.sql("CREATE INDEX ON t(a)") + md_cur.sql("DROP TABLE t") + md_cur.sql("CREATE TABLE public.t(a int) USING heap") + # Check that we allow arbitrary ALTER TABLE commands on heap tables + md_cur.sql("ALTER TABLE t FORCE ROW LEVEL SECURITY") + + if PG_MAJOR_VERSION >= 15: + # We also don't want people to change the access method of a table to + # duckdb after the table is created + with pytest.raises(psycopg.errors.FeatureNotSupported): + md_cur.sql("ALTER TABLE t SET ACCESS METHOD duckdb") diff --git a/test/pycheck/temporary_table_test.py b/test/pycheck/temporary_table_test.py index a531a246..3492a6d7 100644 --- a/test/pycheck/temporary_table_test.py +++ b/test/pycheck/temporary_table_test.py @@ -6,10 +6,12 @@ def test_temporary_table_alter_table(cur: Cursor): cur.sql("CREATE TEMP TABLE t(a int) USING duckdb") - with pytest.raises(psycopg.errors.InternalError_): - cur.sql("ALTER TABLE t ADD COLUMN b int") + # We disallow most ALTER TABLE commands on duckdb tables + with pytest.raises(psycopg.errors.FeatureNotSupported): + cur.sql("ALTER TABLE t FORCE ROW LEVEL SECURITY") if PG_MAJOR_VERSION >= 15: + # We specifically want to disallow changing the access method with pytest.raises(psycopg.errors.FeatureNotSupported): cur.sql("ALTER TABLE t SET ACCESS METHOD heap") @@ -17,8 +19,11 @@ def test_temporary_table_alter_table(cur: Cursor): cur.sql("CREATE INDEX ON t(a)") cur.sql("DROP TABLE t") cur.sql("CREATE TEMP TABLE t(a int) USING heap") - cur.sql("ALTER TABLE t ADD COLUMN b int") + # Check that we allow arbitrary ALTER TABLE commands on heap tables + cur.sql("ALTER TABLE t FORCE ROW LEVEL SECURITY") if PG_MAJOR_VERSION >= 15: + # We also don't want people to change the access method of a table to + # duckdb after the table is created with pytest.raises(psycopg.errors.FeatureNotSupported): cur.sql("ALTER TABLE t SET ACCESS METHOD duckdb") diff --git a/test/regression/expected/alter_table_commands.out b/test/regression/expected/alter_table_commands.out new file mode 100644 index 00000000..c8fae065 --- /dev/null +++ b/test/regression/expected/alter_table_commands.out @@ -0,0 +1,217 @@ +-- Test all ALTER TABLE commands supported by pgduckdb_get_alterdef +-- Set up a test table +SET duckdb.force_execution = false; +CREATE TEMP TABLE alter_test( + id INT, + name TEXT, + value DOUBLE PRECISION DEFAULT 0.0, + created_at TIMESTAMP +) USING duckdb; +INSERT INTO alter_test VALUES (1, 'test1', 10.5, '2023-01-01 12:00:00'); +INSERT INTO alter_test VALUES (2, 'test2', 20.5, '2023-01-02 12:00:00'); +-- Verify initial state +SELECT * FROM alter_test ORDER BY id; + id | name | value | created_at +----+-------+-------+-------------------------- + 1 | test1 | 10.5 | Sun Jan 01 12:00:00 2023 + 2 | test2 | 20.5 | Mon Jan 02 12:00:00 2023 +(2 rows) + +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + column_name | column_type | null | key | default | extra +-------------+-------------+------+-----+---------+------- + id | INTEGER | YES | | | + name | VARCHAR | YES | | | + value | DOUBLE | YES | | 0.0 | + created_at | TIMESTAMP | YES | | | +(4 rows) + +-- ADD COLUMN +ALTER TABLE alter_test ADD COLUMN description TEXT; +ALTER TABLE alter_test ADD COLUMN active BOOLEAN DEFAULT true; +ALTER TABLE alter_test ADD COLUMN score INT DEFAULT 100 NULL; +ALTER TABLE alter_test ADD COLUMN score2 INT DEFAULT 100 NOT NULL; +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Parser Error: Adding columns with constraints not yet supported +ALTER TABLE alter_test ADD COLUMN score3 INT DEFAULT 100 CHECK (score3 >= score); +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Parser Error: Adding columns with constraints not yet supported +ALTER TABLE alter_test ADD COLUMN id2 INT PRIMARY KEY; +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Parser Error: Adding columns with constraints not yet supported +ALTER TABLE alter_test ADD COLUMN id3 INT UNIQUE; +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Parser Error: Adding columns with constraints not yet supported +ALTER TABLE alter_test ADD COLUMN german text COLLATE "de-x-icu"; +ERROR: Column collations are not supported in DuckDB +-- Verify columns were added +SELECT * FROM alter_test ORDER BY id; + id | name | value | created_at | description | active | score +----+-------+-------+--------------------------+-------------+--------+------- + 1 | test1 | 10.5 | Sun Jan 01 12:00:00 2023 | | t | 100 + 2 | test2 | 20.5 | Mon Jan 02 12:00:00 2023 | | t | 100 +(2 rows) + +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + column_name | column_type | null | key | default | extra +-------------+-------------+------+-----+----------------------+------- + id | INTEGER | YES | | | + name | VARCHAR | YES | | | + value | DOUBLE | YES | | 0.0 | + created_at | TIMESTAMP | YES | | | + description | VARCHAR | YES | | | + active | BOOLEAN | YES | | CAST('t' AS BOOLEAN) | + score | INTEGER | YES | | 100 | +(7 rows) + +-- ALTER COLUMN TYPE +ALTER TABLE alter_test ALTER COLUMN id TYPE BIGINT; +ALTER TABLE alter_test ALTER COLUMN value TYPE REAL; +-- Verify column types were changed +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + column_name | column_type | null | key | default | extra +-------------+-------------+------+-----+----------------------+------- + id | BIGINT | YES | | | + name | VARCHAR | YES | | | + value | FLOAT | YES | | 0.0 | + created_at | TIMESTAMP | YES | | | + description | VARCHAR | YES | | | + active | BOOLEAN | YES | | CAST('t' AS BOOLEAN) | + score | INTEGER | YES | | 100 | +(7 rows) + +-- DROP COLUMN +ALTER TABLE alter_test DROP COLUMN description; +-- Verify column was dropped +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + column_name | column_type | null | key | default | extra +-------------+-------------+------+-----+----------------------+------- + id | BIGINT | YES | | | + name | VARCHAR | YES | | | + value | FLOAT | YES | | 0.0 | + created_at | TIMESTAMP | YES | | | + active | BOOLEAN | YES | | CAST('t' AS BOOLEAN) | + score | INTEGER | YES | | 100 | +(6 rows) + +-- SET/DROP DEFAULT +ALTER TABLE alter_test ALTER COLUMN name SET DEFAULT 'unnamed'; +INSERT INTO alter_test(id) VALUES (3); +SELECT * FROM alter_test ORDER BY id; + id | name | value | created_at | active | score +----+---------+-------+--------------------------+--------+------- + 1 | test1 | 10.5 | Sun Jan 01 12:00:00 2023 | t | 100 + 2 | test2 | 20.5 | Mon Jan 02 12:00:00 2023 | t | 100 + 3 | unnamed | 0 | | t | 100 +(3 rows) + +ALTER TABLE alter_test ALTER COLUMN name DROP DEFAULT; +INSERT INTO alter_test(id) VALUES (4); +SELECT * FROM alter_test ORDER BY id; + id | name | value | created_at | active | score +----+---------+-------+--------------------------+--------+------- + 1 | test1 | 10.5 | Sun Jan 01 12:00:00 2023 | t | 100 + 2 | test2 | 20.5 | Mon Jan 02 12:00:00 2023 | t | 100 + 3 | unnamed | 0 | | t | 100 + 4 | | 0 | | t | 100 +(4 rows) + +-- Delete this one before adding the NOT NULL constraint +DELETE FROM alter_test WHERE id = 4; +-- SET/DROP NOT NULL +ALTER TABLE alter_test ALTER COLUMN name SET NOT NULL; +-- This should fail +UPDATE alter_test SET name = NULL WHERE id = 1; +ERROR: (PGDuckDB/Duckdb_ExecCustomScan_Cpp) Constraint Error: NOT NULL constraint failed: alter_test.name +ALTER TABLE alter_test ALTER COLUMN name DROP NOT NULL; +-- This should succeed +UPDATE alter_test SET name = NULL WHERE id = 1; +SELECT * FROM alter_test WHERE id = 1; + id | name | value | created_at | active | score +----+------+-------+--------------------------+--------+------- + 1 | | 10.5 | Sun Jan 01 12:00:00 2023 | t | 100 +(1 row) + +-- ADD CONSTRAINT (CHECK) +ALTER TABLE alter_test ADD CONSTRAINT positive_id CHECK (id > 0); +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Not implemented Error: No support for that ALTER TABLE option yet! +-- This should fail +INSERT INTO alter_test(id, name) VALUES (-1, 'negative'); +-- ADD CONSTRAINT (PRIMARY KEY) +ALTER TABLE alter_test ADD PRIMARY KEY (id); +-- This should fail due to duplicate key +INSERT INTO alter_test(id, name) VALUES (1, 'duplicate'); +ERROR: (PGDuckDB/Duckdb_ExecCustomScan_Cpp) Constraint Error: Duplicate key "id: 1" violates primary key constraint. +-- ADD CONSTRAINT (UNIQUE) +ALTER TABLE alter_test ADD CONSTRAINT unique_name UNIQUE (name); +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Not implemented Error: No support for that ALTER TABLE option yet! +-- This should fail due to duplicate name +UPDATE alter_test SET name = 'test2' WHERE id = 3; +-- DROP CONSTRAINT +ALTER TABLE alter_test DROP CONSTRAINT unique_name; +ERROR: constraint "unique_name" of relation "alter_test" does not exist +-- This should now succeed +UPDATE alter_test SET name = 'test2' WHERE id = 3; +SELECT * FROM alter_test WHERE id = 3; + id | name | value | created_at | active | score +----+-------+-------+------------+--------+------- + 3 | test2 | 0 | | t | 100 +(1 row) + +ALTER TABLE alter_test DROP CONSTRAINT positive_id; +ERROR: constraint "positive_id" of relation "alter_test" does not exist +-- This should now succeed +INSERT INTO alter_test(id, name) VALUES (-1, 'negative'); +ERROR: (PGDuckDB/Duckdb_ExecCustomScan_Cpp) Constraint Error: Duplicate key "id: -1" violates primary key constraint. +SELECT * FROM alter_test WHERE id = -1; + id | name | value | created_at | active | score +----+----------+-------+------------+--------+------- + -1 | negative | 0 | | t | 100 +(1 row) + +-- SET/RESET table options +-- Note: DuckDB supports limited table options compared to PostgreSQL +ALTER TABLE alter_test SET (fillfactor = 90); +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Not implemented Error: No support for that ALTER TABLE option yet! +ALTER TABLE alter_test RESET (fillfactor); +ERROR: (PGDuckDB/duckdb_alter_table_trigger_cpp) Not implemented Error: No support for that ALTER TABLE option yet! +ALTER TABLE alter_test RENAME TO alter_test2; +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test2'); + column_name | column_type | null | key | default | extra +-------------+-------------+------+-----+----------------------+------- + id | BIGINT | NO | PRI | | + name | VARCHAR | YES | | | + value | FLOAT | YES | | 0.0 | + created_at | TIMESTAMP | YES | | | + active | BOOLEAN | YES | | CAST('t' AS BOOLEAN) | + score | INTEGER | YES | | 100 | +(6 rows) + +SELECT * from alter_test2 ORDER BY id; + id | name | value | created_at | active | score +----+----------+-------+--------------------------+--------+------- + -1 | negative | 0 | | t | 100 + 1 | | 10.5 | Sun Jan 01 12:00:00 2023 | t | 100 + 2 | test2 | 20.5 | Mon Jan 02 12:00:00 2023 | t | 100 + 3 | test2 | 0 | | t | 100 +(4 rows) + +ALTER TABLE alter_test2 RENAME COLUMN active TO active2; +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test2'); + column_name | column_type | null | key | default | extra +-------------+-------------+------+-----+----------------------+------- + id | BIGINT | NO | PRI | | + name | VARCHAR | YES | | | + value | FLOAT | YES | | 0.0 | + created_at | TIMESTAMP | YES | | | + active2 | BOOLEAN | YES | | CAST('t' AS BOOLEAN) | + score | INTEGER | YES | | 100 | +(6 rows) + +SELECT * from alter_test2 ORDER BY id; + id | name | value | created_at | active2 | score +----+----------+-------+--------------------------+---------+------- + -1 | negative | 0 | | t | 100 + 1 | | 10.5 | Sun Jan 01 12:00:00 2023 | t | 100 + 2 | test2 | 20.5 | Mon Jan 02 12:00:00 2023 | t | 100 + 3 | test2 | 0 | | t | 100 +(4 rows) + +-- Clean up +DROP TABLE alter_test2; diff --git a/test/regression/expected/temporary_tables.out b/test/regression/expected/temporary_tables.out index 63b4e58b..10592c6a 100644 --- a/test/regression/expected/temporary_tables.out +++ b/test/regression/expected/temporary_tables.out @@ -483,4 +483,28 @@ SELECT * FROM ta; 3 | 3 (6 rows) +ALTER TABLE ta + ADD COLUMN xyz int, + ADD COLUMN "column with spaces" text, + ALTER COLUMN b TYPE bigint; +select * from duckdb.query( $$ describe pg_temp.ta $$ ); + column_name | column_type | null | key | default | extra +--------------------+-------------+------+-----+---------+------- + a | INTEGER | YES | | 3 | + b | BIGINT | YES | | | + xyz | INTEGER | YES | | | + column with spaces | VARCHAR | YES | | | +(4 rows) + +SELECT * FROM ta; + a | b | xyz | column with spaces +---+---+-----+-------------------- + 1 | | | + 2 | | | + 3 | | | + 3 | 1 | | + 3 | 2 | | + 3 | 3 | | +(6 rows) + DROP TABLE webpages, t, t_heap, t_heap2, t_heap3, ta, tb, tc, td; diff --git a/test/regression/expected/transactions.out b/test/regression/expected/transactions.out index 951cbb92..13c37f4b 100644 --- a/test/regression/expected/transactions.out +++ b/test/regression/expected/transactions.out @@ -160,8 +160,20 @@ BEGIN; CREATE TEMP TABLE t_ddb2(a int) USING duckdb; CREATE TEMP TABLE t_ddb3(a) USING duckdb AS SELECT 1; DROP TABLE t_ddb3; + INSERT INTO t_ddb2 VALUES (1); + ALTER TABLE t_ddb2 ADD COLUMN b int; + ALTER TABLE t_ddb2 ADD COLUMN c int; + ALTER TABLE t_ddb2 ADD COLUMN d int DEFAULT 100, ADD COLUMN e int DEFAULT 10; + ALTER TABLE t_ddb2 RENAME COLUMN b TO f; + ALTER TABLE t_ddb2 RENAME TO t_ddb4; + SELECT * FROM t_ddb4; + a | f | c | d | e +---+---+---+-----+---- + 1 | | | 100 | 10 +(1 row) + END; -DROP TABLE t_ddb2; +DROP TABLE t_ddb4; -- Similarly is DDL in functions CREATE OR REPLACE FUNCTION f2() RETURNS void LANGUAGE plpgsql @@ -172,6 +184,12 @@ BEGIN CREATE TEMP TABLE t_ddb2(a int) USING duckdb; CREATE TEMP TABLE t_ddb3(a) USING duckdb AS SELECT 1; DROP TABLE t_ddb3; + INSERT INTO t_ddb2 VALUES (1); + ALTER TABLE t_ddb2 ADD COLUMN b int; + ALTER TABLE t_ddb2 ADD COLUMN c int; + ALTER TABLE t_ddb2 ADD COLUMN d int DEFAULT 100, ADD COLUMN e int DEFAULT 10; + ALTER TABLE t_ddb2 RENAME COLUMN b TO f; + ALTER TABLE t_ddb2 RENAME TO t_ddb4; END; $$; SELECT * FROM f2(); @@ -180,7 +198,7 @@ SELECT * FROM f2(); (1 row) -DROP TABLE t_ddb2; +DROP TABLE t_ddb4; -- ...unless there's also PG only changes happing in the same transaction (no matter the order of statements). BEGIN; CREATE TABLE t_x(a int); @@ -247,6 +265,16 @@ BEGIN; DROP TABLE t_ddb; ERROR: (PGDuckDB/DuckdbUtilityHook_Cpp) Not implemented Error: Writing to DuckDB and Postgres tables in the same transaction block is not supported END; +BEGIN; + CREATE TABLE t_x(a int); + ALTER TABLE t_ddb ADD COLUMN b int; +ERROR: (PGDuckDB/DuckdbUtilityHook_Cpp) Not implemented Error: Writing to DuckDB and Postgres tables in the same transaction block is not supported +END; +BEGIN; + ALTER TABLE t_ddb ADD COLUMN b int; + CREATE TABLE t_x(a int); +END; +ERROR: (PGDuckDB/DuckdbXactCallback_Cpp) Not implemented Error: Writing to DuckDB and Postgres tables in the same transaction block is not supported -- Dropping both duckdb tables and postgres tables in the same command is -- disallowed in a transaction. BEGIN; @@ -266,10 +294,11 @@ BEGIN; ERROR: (PGDuckDB/DuckdbUtilityHook_Cpp) Not implemented Error: Writing to DuckDB and Postgres tables in the same transaction block is not supported END; -- ...in any order --- BEGIN; --- DROP TABLE t_ddb; --- DROP TABLE t; --- END; +BEGIN; + DROP TABLE t_ddb; + DROP TABLE t; +END; +ERROR: (PGDuckDB/DuckdbXactCallback_Cpp) Not implemented Error: Writing to DuckDB and Postgres tables in the same transaction block is not supported -- Non-existing tables should be allowed to be dropped in a transaction -- together with DuckDB. BEGIN; diff --git a/test/regression/sql/alter_table_commands.sql b/test/regression/sql/alter_table_commands.sql new file mode 100644 index 00000000..71b27299 --- /dev/null +++ b/test/regression/sql/alter_table_commands.sql @@ -0,0 +1,108 @@ +-- Test all ALTER TABLE commands supported by pgduckdb_get_alterdef +-- Set up a test table +SET duckdb.force_execution = false; +CREATE TEMP TABLE alter_test( + id INT, + name TEXT, + value DOUBLE PRECISION DEFAULT 0.0, + created_at TIMESTAMP +) USING duckdb; + +INSERT INTO alter_test VALUES (1, 'test1', 10.5, '2023-01-01 12:00:00'); +INSERT INTO alter_test VALUES (2, 'test2', 20.5, '2023-01-02 12:00:00'); + +-- Verify initial state +SELECT * FROM alter_test ORDER BY id; +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + +-- ADD COLUMN +ALTER TABLE alter_test ADD COLUMN description TEXT; +ALTER TABLE alter_test ADD COLUMN active BOOLEAN DEFAULT true; +ALTER TABLE alter_test ADD COLUMN score INT DEFAULT 100 NULL; +ALTER TABLE alter_test ADD COLUMN score2 INT DEFAULT 100 NOT NULL; +ALTER TABLE alter_test ADD COLUMN score3 INT DEFAULT 100 CHECK (score3 >= score); +ALTER TABLE alter_test ADD COLUMN id2 INT PRIMARY KEY; +ALTER TABLE alter_test ADD COLUMN id3 INT UNIQUE; +ALTER TABLE alter_test ADD COLUMN german text COLLATE "de-x-icu"; + +-- Verify columns were added +SELECT * FROM alter_test ORDER BY id; +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + +-- ALTER COLUMN TYPE +ALTER TABLE alter_test ALTER COLUMN id TYPE BIGINT; +ALTER TABLE alter_test ALTER COLUMN value TYPE REAL; + +-- Verify column types were changed +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + +-- DROP COLUMN +ALTER TABLE alter_test DROP COLUMN description; + +-- Verify column was dropped +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test'); + +-- SET/DROP DEFAULT +ALTER TABLE alter_test ALTER COLUMN name SET DEFAULT 'unnamed'; +INSERT INTO alter_test(id) VALUES (3); +SELECT * FROM alter_test ORDER BY id; + +ALTER TABLE alter_test ALTER COLUMN name DROP DEFAULT; +INSERT INTO alter_test(id) VALUES (4); +SELECT * FROM alter_test ORDER BY id; + +-- Delete this one before adding the NOT NULL constraint +DELETE FROM alter_test WHERE id = 4; + +-- SET/DROP NOT NULL +ALTER TABLE alter_test ALTER COLUMN name SET NOT NULL; +-- This should fail +UPDATE alter_test SET name = NULL WHERE id = 1; + +ALTER TABLE alter_test ALTER COLUMN name DROP NOT NULL; +-- This should succeed +UPDATE alter_test SET name = NULL WHERE id = 1; +SELECT * FROM alter_test WHERE id = 1; + +-- ADD CONSTRAINT (CHECK) +ALTER TABLE alter_test ADD CONSTRAINT positive_id CHECK (id > 0); +-- This should fail +INSERT INTO alter_test(id, name) VALUES (-1, 'negative'); + +-- ADD CONSTRAINT (PRIMARY KEY) +ALTER TABLE alter_test ADD PRIMARY KEY (id); +-- This should fail due to duplicate key +INSERT INTO alter_test(id, name) VALUES (1, 'duplicate'); + +-- ADD CONSTRAINT (UNIQUE) +ALTER TABLE alter_test ADD CONSTRAINT unique_name UNIQUE (name); +-- This should fail due to duplicate name +UPDATE alter_test SET name = 'test2' WHERE id = 3; + +-- DROP CONSTRAINT +ALTER TABLE alter_test DROP CONSTRAINT unique_name; +-- This should now succeed +UPDATE alter_test SET name = 'test2' WHERE id = 3; +SELECT * FROM alter_test WHERE id = 3; + +ALTER TABLE alter_test DROP CONSTRAINT positive_id; +-- This should now succeed +INSERT INTO alter_test(id, name) VALUES (-1, 'negative'); +SELECT * FROM alter_test WHERE id = -1; + +-- SET/RESET table options +-- Note: DuckDB supports limited table options compared to PostgreSQL +ALTER TABLE alter_test SET (fillfactor = 90); +ALTER TABLE alter_test RESET (fillfactor); + +ALTER TABLE alter_test RENAME TO alter_test2; +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test2'); +SELECT * from alter_test2 ORDER BY id; + +ALTER TABLE alter_test2 RENAME COLUMN active TO active2; +SELECT * FROM duckdb.query('DESCRIBE pg_temp.alter_test2'); +SELECT * from alter_test2 ORDER BY id; + +-- Clean up +DROP TABLE alter_test2; + diff --git a/test/regression/sql/temporary_tables.sql b/test/regression/sql/temporary_tables.sql index 44abb802..f3b967dd 100644 --- a/test/regression/sql/temporary_tables.sql +++ b/test/regression/sql/temporary_tables.sql @@ -261,4 +261,13 @@ INSERT INTO ta (a) SELECT * FROM generate_series(1, 3); -- OK INSERT INTO ta (b) SELECT * FROM generate_series(1, 3); -- OK SELECT * FROM ta; +ALTER TABLE ta + ADD COLUMN xyz int, + ADD COLUMN "column with spaces" text, + ALTER COLUMN b TYPE bigint; + +select * from duckdb.query( $$ describe pg_temp.ta $$ ); + +SELECT * FROM ta; + DROP TABLE webpages, t, t_heap, t_heap2, t_heap3, ta, tb, tc, td; diff --git a/test/regression/sql/transactions.sql b/test/regression/sql/transactions.sql index c549fe46..3849faf6 100644 --- a/test/regression/sql/transactions.sql +++ b/test/regression/sql/transactions.sql @@ -112,9 +112,16 @@ BEGIN; CREATE TEMP TABLE t_ddb2(a int) USING duckdb; CREATE TEMP TABLE t_ddb3(a) USING duckdb AS SELECT 1; DROP TABLE t_ddb3; + INSERT INTO t_ddb2 VALUES (1); + ALTER TABLE t_ddb2 ADD COLUMN b int; + ALTER TABLE t_ddb2 ADD COLUMN c int; + ALTER TABLE t_ddb2 ADD COLUMN d int DEFAULT 100, ADD COLUMN e int DEFAULT 10; + ALTER TABLE t_ddb2 RENAME COLUMN b TO f; + ALTER TABLE t_ddb2 RENAME TO t_ddb4; + SELECT * FROM t_ddb4; END; -DROP TABLE t_ddb2; +DROP TABLE t_ddb4; -- Similarly is DDL in functions @@ -127,12 +134,18 @@ BEGIN CREATE TEMP TABLE t_ddb2(a int) USING duckdb; CREATE TEMP TABLE t_ddb3(a) USING duckdb AS SELECT 1; DROP TABLE t_ddb3; + INSERT INTO t_ddb2 VALUES (1); + ALTER TABLE t_ddb2 ADD COLUMN b int; + ALTER TABLE t_ddb2 ADD COLUMN c int; + ALTER TABLE t_ddb2 ADD COLUMN d int DEFAULT 100, ADD COLUMN e int DEFAULT 10; + ALTER TABLE t_ddb2 RENAME COLUMN b TO f; + ALTER TABLE t_ddb2 RENAME TO t_ddb4; END; $$; SELECT * FROM f2(); -DROP TABLE t_ddb2; +DROP TABLE t_ddb4; -- ...unless there's also PG only changes happing in the same transaction (no matter the order of statements). BEGIN; @@ -199,6 +212,16 @@ BEGIN; DROP TABLE t_ddb; END; +BEGIN; + CREATE TABLE t_x(a int); + ALTER TABLE t_ddb ADD COLUMN b int; +END; + +BEGIN; + ALTER TABLE t_ddb ADD COLUMN b int; + CREATE TABLE t_x(a int); +END; + -- Dropping both duckdb tables and postgres tables in the same command is -- disallowed in a transaction. BEGIN; @@ -218,10 +241,10 @@ BEGIN; END; -- ...in any order --- BEGIN; --- DROP TABLE t_ddb; --- DROP TABLE t; --- END; +BEGIN; + DROP TABLE t_ddb; + DROP TABLE t; +END; -- Non-existing tables should be allowed to be dropped in a transaction -- together with DuckDB.