1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

feat: DuckDB supports AUTO INCREMENT stanza.

This commit is contained in:
drrtuy
2026-08-04 20:54:28 +01:00
parent 25636531e5
commit 2295f22baf
13 changed files with 767 additions and 23 deletions

View File

@@ -70,7 +70,7 @@ All generated SQL must use **double quotes** for identifiers (DuckDB follows SQL
### DuckDB source and patches
DuckDB source is at `third_parties/duckdb/` (git submodule). No patches are applied — all compatibility is handled at runtime via `duckdb_mysql_compat.cc`. The build produces a static library; `_GLIBCXX_DEBUG` is explicitly undefined in CMakeLists.txt to avoid ABI mismatch with MariaDB's debug build.
DuckDB source is at `third_parties/duckdb/` (git submodule). One local patch is applied on top of the pinned submodule commit — `patches/duckdb-pr24061-setval.diff` (backport of upstream PR #24061 adding `setval()`, needed for AUTO_INCREMENT counter repositioning), applied via `PATCH_COMMAND` in `cmake/duckdb.cmake` and to be dropped when the submodule is bumped to a release containing the PR. Other compatibility is handled at runtime via `duckdb_mysql_compat.cc`. The build produces a static library; `_GLIBCXX_DEBUG` is explicitly undefined in CMakeLists.txt to avoid ABI mismatch with MariaDB's debug build.
### Configuration

View File

@@ -63,8 +63,10 @@ IF(TARGET duckdb)
ADD_SUBDIRECTORY(convertor)
ADD_SUBDIRECTORY(runtime)
# atomic: GCC lowers the 16-byte atomic ops of the AUTO_INCREMENT block
# cache (Duckdb_share::autoinc_range) to libatomic calls.
target_link_libraries(duckdb duckdb_runtime duckdb_convertor duckdb_common
${DUCKDB_LIBRARY}
${DUCKDB_LIBRARY} atomic
)
duckdb_setup_target(duckdb)

View File

@@ -51,6 +51,16 @@ SET(_DUCKDB_STATIC_LIBS
MESSAGE(STATUS "=== Building DuckDB from submodule (${DUCKDB_SUBMODULE_DIR}) ===")
ADD_SUBMODULE(third_parties/duckdb)
# Local patches applied on top of the pinned submodule commit.
#
# duckdb-pr24061-setval.diff: backport of upstream PR #24061 ("Add SetVal",
# merged 2026-07-23, first release after v1.5.2) rewritten against the v1.5.2
# API. Provides setval('seq', value[, is_called]) needed for AUTO_INCREMENT
# counter repositioning (TRUNCATE, ALTER ... AUTO_INCREMENT=N, explicit-id
# reconciliation). Drop when the submodule is bumped to a release containing
# the PR: the apply below will fail loudly on an already-present setval.
SET(_DUCKDB_PATCH "${CMAKE_CURRENT_SOURCE_DIR}/patches/duckdb-pr24061-setval.diff")
# Upstream sets DUCKDB_EXTENSION_JEMALLOC_LINKED via add_extension_definitions(),
# which runs in extension/ but NOT in src/, so allocator.cpp (in duckdb_static)
# compiles the glibc malloc() path even though libjemalloc_extension.a is linked.
@@ -79,6 +89,8 @@ ExternalProject_Add(duckdb_build
PREFIX "${_DUCKDB_BUILD_DIR}"
SOURCE_DIR "${DUCKDB_SUBMODULE_DIR}"
BINARY_DIR "${_DUCKDB_BUILD_DIR}"
# Idempotent: skip when the working tree already carries the patch.
PATCH_COMMAND bash -c "git apply --reverse --check '${_DUCKDB_PATCH}' 2>/dev/null || git apply --verbose '${_DUCKDB_PATCH}'"
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${_DUCKDB_BUILD_TYPE}
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}

View File

@@ -216,6 +216,29 @@ static void note_sequence_default_omitted(const char *column)
column);
}
std::string autoinc_sequence_name(const std::string &table_name)
{
return "mdb_autoinc_" + table_name;
}
Field *find_autoinc_field(const TABLE *table)
{
for (Field **ptr= table->field; *ptr; ptr++)
{
if ((*ptr)->flags & AUTO_INCREMENT_FLAG)
return *ptr;
}
return nullptr;
}
/** Reference to the auto-increment sequence usable as a nextval() argument. */
static std::string autoinc_nextval_expr(const std::string &schema_name,
const std::string &table_name)
{
return "nextval('\"" + schema_name + "\".\"" +
autoinc_sequence_name(table_name) + "\"')";
}
/**
Read the literal default value of a field from the default record and
return it as a string suitable for DuckDB SQL.
@@ -419,11 +442,21 @@ static void append_stmt_table_rename(std::ostringstream &output,
bool FieldConvertor::check()
{
/* not support auto_increment */
if (m_field->flags & AUTO_INCREMENT_FLAG)
return report_duckdb_table_struct_error(
"AUTO_INCREMENT", "removing AUTO_INCREMENT from column",
m_field->field_name.str, m_ctx);
/*
AUTO_INCREMENT is supported by CREATE TABLE only. Adding it to an
existing table would require seeding the sequence from the current
column maximum, which is not implemented yet.
*/
if ((m_field->flags & AUTO_INCREMENT_FLAG) &&
m_ctx != ddl_error_context::CREATE)
{
/*
Same error the server raised while HA_NO_AUTO_INCREMENT was set, so
that the rejection stays indistinguishable from before.
*/
my_error(ER_TABLE_CANT_HANDLE_AUTO_INCREMENT, MYF(0), "DUCKDB");
return true;
}
/* No support for INVISIBLE columns. */
if (m_field->invisible >= INVISIBLE_USER)
@@ -503,8 +536,6 @@ std::string FieldConvertor::translate()
}
}
assert(!(field->flags & AUTO_INCREMENT_FLAG));
return result.str();
}
@@ -633,6 +664,20 @@ bool CreateTableConvertor::check()
return true;
}
/*
An ALTER using ALGORITHM=COPY builds a shadow table through create(), so
it arrives here rather than through the ALTER convertors. Reject
AUTO_INCREMENT on that path: the new sequence would start at 1 while the
copied rows already hold larger ids, silently handing out duplicates.
Seeding from the current column maximum is not implemented.
*/
if (m_thd->lex->sql_command != SQLCOM_CREATE_TABLE &&
find_autoinc_field(m_table))
{
my_error(ER_TABLE_CANT_HANDLE_AUTO_INCREMENT, MYF(0), "DUCKDB");
return true;
}
/* Check PK. */
TABLE_SHARE *share= m_table->s;
@@ -679,6 +724,24 @@ std::string CreateTableConvertor::translate()
result << "USE " << '"' << m_schema_name << '"' << ";";
/*
The sequence must exist before the table, because the AUTO_INCREMENT
column default resolves it at CREATE TABLE bind time.
*/
if (find_autoinc_field(m_table))
{
ulonglong start= m_create_info->auto_increment_value
? m_create_info->auto_increment_value
: 1;
/* A DuckDB sequence counter is int64; never emit an unrepresentable start. */
if (start > (ulonglong) INT64_MAX)
start= (ulonglong) INT64_MAX;
result << "CREATE SEQUENCE IF NOT EXISTS " << '"' << m_schema_name << '"'
<< "." << '"' << autoinc_sequence_name(m_table_name) << '"'
<< " START WITH " << start << ";";
}
result << CREATE_TABLE_STR;
/* MariaDB: IF NOT EXISTS is handled at the SQL layer, not in HA_CREATE_INFO.
Always use IF NOT EXISTS for safety in DuckDB. */
@@ -701,7 +764,16 @@ void CreateTableConvertor::append_column_definition(std::ostringstream &output)
{
if (ptr != first_field)
output << ",";
output << FieldConvertor(field).translate();
output << FieldConvertor(field, ddl_error_context::CREATE).translate();
/*
The AUTO_INCREMENT column draws from a DuckDB sequence. Keeping the
default in DuckDB lets writes that bypass MariaDB still obtain a
non-colliding id from the same counter.
*/
if (field->flags & AUTO_INCREMENT_FLAG)
output << " DEFAULT "
<< autoinc_nextval_expr(m_schema_name, m_table_name);
}
}

View File

@@ -49,6 +49,16 @@ static inline bool is_uuid_field(const Field *field)
field->type_handler()->type_collection() == uuid_coll;
}
/**
Name of the DuckDB sequence backing the AUTO_INCREMENT column of a table.
MariaDB allows at most one AUTO_INCREMENT column per table, so the table
name alone identifies the sequence.
*/
std::string autoinc_sequence_name(const std::string &table_name);
/** The AUTO_INCREMENT field of a table, or nullptr if there is none. */
Field *find_autoinc_field(const TABLE *table);
class BaseConvertor
{
public:

View File

@@ -61,6 +61,7 @@ static duckdb_status_t srv_duckdb_status;
static my_bool copy_ddl_in_batch= TRUE;
static my_bool dml_in_batch= TRUE;
static my_bool update_modified_column_only= TRUE;
static ulonglong autoinc_cache_size= 1000;
static handler *duckdb_create_handler(handlerton *hton, TABLE_SHARE *table,
MEM_ROOT *mem_root);
@@ -262,7 +263,12 @@ static int duckdb_deinit_func(void *p)
/* ----- Share management ----- */
Duckdb_share::Duckdb_share() { thr_lock_init(&lock); }
Duckdb_share::Duckdb_share()
{
thr_lock_init(&lock);
mysql_mutex_init(PSI_INSTRUMENT_ME, &autoinc_refill_mutex,
MY_MUTEX_INIT_FAST);
}
Duckdb_share *ha_duckdb::get_share()
{
@@ -458,7 +464,7 @@ static void build_duckdb_blob_map(Field **field_list, MY_BITMAP *map)
/* ----- DML operations ----- */
int ha_duckdb::write_row(const uchar *)
int ha_duckdb::write_row(const uchar *buf)
{
DBUG_ENTER("ha_duckdb::write_row");
int ret= 0;
@@ -474,6 +480,19 @@ int ha_duckdb::write_row(const uchar *)
DBUG_RETURN(ret);
}
/*
Fill the AUTO_INCREMENT field. Every row reaches DuckDB with all columns
materialized (both the Appender and the SQL insert paths), so the value
must be assigned here; the sequence-backed column default in DuckDB is
never consulted on this path.
*/
if (table->next_number_field && buf == table->record[0] &&
(ret= update_auto_increment()))
{
dbug_tmp_restore_column_map(&table->read_set, org_bitmap);
DBUG_RETURN(ret);
}
myduck::BatchState batch_state= get_batch_state(thd);
if (batch_state == myduck::BatchState::NOT_IN_BATCH)
@@ -510,6 +529,119 @@ int ha_duckdb::write_row(const uchar *)
DBUG_RETURN(ret);
}
/*
Consume `want` ids from the table's AUTO_INCREMENT sequence in one query
and return the first id of the block, or 0 on failure (a sequence value is
never 0: CREATE SEQUENCE is emitted with START WITH >= 1).
nextval() calls of a concurrent DuckDB-side writer (batch ingestion through
run_in_duckdb relying on the column default) may interleave with ours
inside this query. The ids missing from our span belong to that writer, so
such a block must not be handed out; detect it by comparing the span width
with the block size and retry. MariaDB-side callers cannot cause this: they
are serialized on the share's autoinc_refill_mutex.
*/
static ulonglong reserve_autoinc_block(THD *thd, const char *db_name,
const char *table_name, ulonglong want)
{
std::string query=
"SELECT min(x), max(x) FROM (SELECT nextval('\"" +
std::string(db_name) + "\".\"" + autoinc_sequence_name(table_name) +
"\"') AS x FROM range(" + std::to_string(want) + ")) t";
auto *ctx= get_duckdb_context(thd);
for (int attempt= 0; attempt < 3; attempt++)
{
auto result= myduck::duckdb_query(ctx->get_connection(), query);
if (result == nullptr || result->HasError())
return 0;
auto chunk= result->Fetch();
if (chunk == nullptr || chunk->size() == 0)
return 0;
int64_t lo= chunk->GetValue(0, 0).GetValue<int64_t>();
int64_t hi= chunk->GetValue(1, 0).GetValue<int64_t>();
if ((ulonglong) (hi - lo) + 1 == want)
return (ulonglong) lo;
}
return 0;
}
void ha_duckdb::get_auto_increment(ulonglong offset, ulonglong increment,
ulonglong nb_desired_values,
ulonglong *first_value,
ulonglong *nb_reserved_values)
{
DBUG_ENTER("ha_duckdb::get_auto_increment");
/*
The server rounds the first value up to the offset/increment grid
(handler.cc:4389: at most first + increment - 1) and then steps
`increment` at a time, so it uses ids <= first + nb_desired * increment
- 1. Consume exactly that many from the cache to keep the server inside
our block while leaving no unused ids behind.
*/
ulonglong need= nb_desired_values * increment;
if (need / increment != nb_desired_values) /* multiplication overflow */
{
*first_value= ULONGLONG_MAX;
*nb_reserved_values= 0;
DBUG_VOID_RETURN;
}
Duckdb_share::Autoinc_range cur= share->autoinc_range.load();
for (;;)
{
/* Fast path: claim [cur.next, cur.next + need) with one CAS. */
while (cur.end - cur.next >= need)
{
Duckdb_share::Autoinc_range claimed= {cur.next + need, cur.end};
if (share->autoinc_range.compare_exchange_weak(cur, claimed))
{
*first_value= cur.next;
*nb_reserved_values= nb_desired_values;
DBUG_VOID_RETURN;
}
/* CAS failure reloaded cur. */
}
mysql_mutex_lock(&share->autoinc_refill_mutex);
cur= share->autoinc_range.load();
if (cur.end - cur.next >= need)
{
/* Someone refilled while we waited; back to the fast path. */
mysql_mutex_unlock(&share->autoinc_refill_mutex);
continue;
}
ulonglong want= std::max(need, autoinc_cache_size);
DatabaseTableNames dt(table->s->normalized_path.str);
ulonglong first= reserve_autoinc_block(ha_thd(), dt.db_name.c_str(),
dt.table_name.c_str(), want);
if (first == 0)
{
mysql_mutex_unlock(&share->autoinc_refill_mutex);
/* The only failure signal the server understands. */
*first_value= ULONGLONG_MAX;
*nb_reserved_values= 0;
DBUG_VOID_RETURN;
}
/*
Publish the new block minus our own share. A plain store cannot clash
with concurrent fast-path claims: those still come from the old range,
which lies entirely below `first` because the sequence is monotonic.
The remainder of the old block is dropped, not merged: ids between the
two blocks may already belong to a DuckDB-side writer. The unused ids
become gaps, which AUTO_INCREMENT permits.
*/
share->autoinc_range.store({first + need, first + want});
mysql_mutex_unlock(&share->autoinc_refill_mutex);
*first_value= first;
*nb_reserved_values= nb_desired_values;
DBUG_VOID_RETURN;
}
}
int ha_duckdb::update_row(const uchar *old_row, const uchar *new_row)
{
DBUG_ENTER("ha_duckdb::update_row");
@@ -1012,6 +1144,19 @@ int ha_duckdb::delete_table(const char *name)
if (query_result == nullptr || query_result->HasError())
DBUG_RETURN(HA_DUCKDB_DROP_TABLE_ERROR);
/*
Drop the AUTO_INCREMENT sequence, if any. It must go after the table:
while the column default exists the catalog dependency blocks the drop.
The name is derived from the table alone, so no lookup is needed and
the statement is harmless for tables without AUTO_INCREMENT.
*/
std::string seq_query= "DROP SEQUENCE IF EXISTS \"" + dt.db_name + "\".\"" +
autoinc_sequence_name(dt.table_name) + "\"";
auto seq_result= myduck::duckdb_query(ctx->get_connection(), seq_query);
if (seq_result == nullptr || seq_result->HasError())
DBUG_RETURN(HA_DUCKDB_DROP_TABLE_ERROR);
DBUG_RETURN(0);
}
@@ -1229,6 +1374,12 @@ static MYSQL_SYSVAR_BOOL(update_modified_column_only,
"Whether to only update modified columns", NULL, NULL,
TRUE);
static MYSQL_SYSVAR_ULONGLONG(autoinc_cache_size, autoinc_cache_size,
PLUGIN_VAR_RQCMDARG,
"Number of AUTO_INCREMENT ids reserved from the "
"backing DuckDB sequence per query",
NULL, NULL, 1000, 1, 1048576, 0);
/* ---- Global proxy variables (pushed into DuckDB) ---- */
static MYSQL_SYSVAR_ULONGLONG(memory_limit, myduck::global_memory_limit,
@@ -1355,6 +1506,7 @@ static struct st_mysql_sys_var *duckdb_system_variables[]= {
MYSQL_SYSVAR(allow_run_in_duckdb),
MYSQL_SYSVAR(copy_ddl_in_batch), MYSQL_SYSVAR(dml_in_batch),
MYSQL_SYSVAR(update_modified_column_only),
MYSQL_SYSVAR(autoinc_cache_size),
/* Global proxy */
MYSQL_SYSVAR(memory_limit), MYSQL_SYSVAR(temp_directory),
MYSQL_SYSVAR(max_temp_directory_size), MYSQL_SYSVAR(max_threads),

View File

@@ -21,6 +21,7 @@
#define HA_DUCKDB_H
#include <sys/types.h>
#include <atomic>
#include <memory>
#include "my_global.h"
@@ -47,8 +48,30 @@ class Duckdb_share : public Handler_share
{
public:
THR_LOCK lock;
/*
AUTO_INCREMENT block cache. Blocks are drawn from the table's DuckDB
sequence in get_auto_increment(); the cache spreads the cost of that
query over duckdb_autoinc_cache_size ids.
The fast path claims ids with one CAS on the {next, end} pair. Both
fields must move together: with independent atomics a reader could pair
a stale bound with a fresh one and claim ids of a dropped block gap,
which may belong to a DuckDB-side writer. The mutex only serializes
block refills.
*/
struct Autoinc_range
{
ulonglong next; ///< next unused id of the cached block
ulonglong end; ///< one past the last id of the cached block
};
alignas(16) std::atomic<Autoinc_range> autoinc_range{{0, 0}};
mysql_mutex_t autoinc_refill_mutex;
Duckdb_share();
~Duckdb_share() { thr_lock_delete(&lock); }
~Duckdb_share()
{
thr_lock_delete(&lock);
mysql_mutex_destroy(&autoinc_refill_mutex);
}
};
/** @brief
@@ -69,7 +92,7 @@ public:
ulonglong table_flags() const override
{
return (HA_BINLOG_STMT_CAPABLE | HA_BINLOG_ROW_CAPABLE |
HA_NO_AUTO_INCREMENT | HA_NULL_IN_KEY | HA_CAN_INDEX_BLOBS |
HA_NULL_IN_KEY | HA_CAN_INDEX_BLOBS |
HA_CAN_DIRECT_UPDATE_AND_DELETE);
}
@@ -111,6 +134,9 @@ public:
int close(void) override;
int write_row(const uchar *buf) override;
void get_auto_increment(ulonglong offset, ulonglong increment,
ulonglong nb_desired_values, ulonglong *first_value,
ulonglong *nb_reserved_values) override;
int update_row(const uchar *old_data, const uchar *new_data) override;
int delete_row(const uchar *buf) override;

View File

@@ -292,11 +292,14 @@ eval ALTER TABLE t ADD COLUMN d INT INVISIBLE, ALGORITHM = $algorithm;
--error ER_PARSE_ERROR
eval ALTER TABLE t ALTER COLUMN b SET INVISIBLE, ALGORITHM = $algorithm;
# AUTO_INCREMENT
# AUTO_INCREMENT is supported by CREATE TABLE only.
# 'c INT AUTO_INCREMENT KEY' would be rejected by the parser instead (bare KEY
# means PRIMARY KEY and t already has one), and AUTO_INCREMENT without any key
# is rejected by the server, so neither would reach the engine.
--error ER_TABLE_CANT_HANDLE_AUTO_INCREMENT
eval ALTER TABLE t MODIFY COLUMN c INT AUTO_INCREMENT KEY, ALGORITHM = $algorithm;
eval ALTER TABLE t MODIFY COLUMN id INT AUTO_INCREMENT, ALGORITHM = $algorithm;
--error ER_TABLE_CANT_HANDLE_AUTO_INCREMENT
eval ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT, ALGORITHM = $algorithm;
eval ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT UNIQUE, ALGORITHM = $algorithm;
# ENGINE_ATTRIBUTE
--error ER_UNKNOWN_OPTION

View File

@@ -718,9 +718,9 @@ ALTER TABLE t ADD COLUMN d INT INVISIBLE, ALGORITHM = INSTANT;
ERROR HY000: Got error 168 'INVISIBLE column is not supported. Try removing INVISIBLE from column 'd'' from DuckDB
ALTER TABLE t ALTER COLUMN b SET INVISIBLE, ALGORITHM = INSTANT;
ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'INVISIBLE, ALGORITHM = INSTANT' at line 1
ALTER TABLE t MODIFY COLUMN c INT AUTO_INCREMENT KEY, ALGORITHM = INSTANT;
ALTER TABLE t MODIFY COLUMN id INT AUTO_INCREMENT, ALGORITHM = INSTANT;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT, ALGORITHM = INSTANT;
ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT UNIQUE, ALGORITHM = INSTANT;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
CREATE TABLE t1(id INT PRIMARY KEY, a INT) ENGINE = DuckDB ENGINE_ATTRIBUTE='{"KEY":"VALUE"}';
ERROR HY000: Unknown option 'ENGINE_ATTRIBUTE'
@@ -1596,9 +1596,9 @@ ALTER TABLE t ADD COLUMN d INT INVISIBLE, ALGORITHM = INPLACE;
ERROR HY000: Got error 168 'INVISIBLE column is not supported. Try removing INVISIBLE from column 'd'' from DuckDB
ALTER TABLE t ALTER COLUMN b SET INVISIBLE, ALGORITHM = INPLACE;
ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'INVISIBLE, ALGORITHM = INPLACE' at line 1
ALTER TABLE t MODIFY COLUMN c INT AUTO_INCREMENT KEY, ALGORITHM = INPLACE;
ALTER TABLE t MODIFY COLUMN id INT AUTO_INCREMENT, ALGORITHM = INPLACE;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT, ALGORITHM = INPLACE;
ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT UNIQUE, ALGORITHM = INPLACE;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
CREATE TABLE t1(id INT PRIMARY KEY, a INT) ENGINE = DuckDB ENGINE_ATTRIBUTE='{"KEY":"VALUE"}';
ERROR HY000: Unknown option 'ENGINE_ATTRIBUTE'

View File

@@ -718,9 +718,9 @@ ALTER TABLE t ADD COLUMN d INT INVISIBLE, ALGORITHM = COPY;
ERROR HY000: Table storage engine 'DuckDB' does not support the create option 'INVISIBLE column'
ALTER TABLE t ALTER COLUMN b SET INVISIBLE, ALGORITHM = COPY;
ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'INVISIBLE, ALGORITHM = COPY' at line 1
ALTER TABLE t MODIFY COLUMN c INT AUTO_INCREMENT KEY, ALGORITHM = COPY;
ALTER TABLE t MODIFY COLUMN id INT AUTO_INCREMENT, ALGORITHM = COPY;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT, ALGORITHM = COPY;
ALTER TABLE t ADD COLUMN d INT AUTO_INCREMENT UNIQUE, ALGORITHM = COPY;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
CREATE TABLE t1(id INT PRIMARY KEY, a INT) ENGINE = DuckDB ENGINE_ATTRIBUTE='{"KEY":"VALUE"}';
ERROR HY000: Unknown option 'ENGINE_ATTRIBUTE'

View File

@@ -0,0 +1,204 @@
SET NAMES utf8mb4;
#
# MDEV-40266: AUTO_INCREMENT backed by a DuckDB sequence
#
# --- CREATE TABLE creates the sequence -------------------------------
CREATE TABLE t (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, a INT) ENGINE=DuckDB;
SHOW CREATE TABLE t;
Table Create Table
t CREATE TABLE `t` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=DUCKDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci
# The sequence must exist in DuckDB, named after the table.
SELECT run_in_duckdb("SELECT schema_name, sequence_name, start_value FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' ORDER BY sequence_name");
run_in_duckdb("SELECT schema_name, sequence_name, start_value FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' ORDER BY sequence_name")
schema_name sequence_name start_value
VARCHAR VARCHAR BIGINT
[ Rows: 1]
db_autoinc mdb_autoinc_t 1
# The column default must reference that sequence, so that writes which
# bypass MariaDB still draw a non-colliding id from the same counter.
SELECT run_in_duckdb("SELECT column_name, column_default FROM information_schema.columns WHERE table_schema = 'db_autoinc' AND table_name = 't' ORDER BY column_name");
run_in_duckdb("SELECT column_name, column_default FROM information_schema.columns WHERE table_schema = 'db_autoinc' AND table_name = 't' ORDER BY column_name")
column_name column_default
VARCHAR VARCHAR
[ Rows: 2]
a NULL
id nextval('"db_autoinc"."mdb_autoinc_t"')
# --- an insert bypassing MariaDB uses the sequence --------------------
SELECT run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(10)");
run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(10)")
Count
BIGINT
[ Rows: 1]
1
SELECT run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(20)");
run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(20)")
Count
BIGINT
[ Rows: 1]
1
SELECT id, a FROM t ORDER BY id;
id a
1 10
2 20
# --- MariaDB-side inserts draw from the same sequence ------------------
INSERT INTO t(a) VALUES(30);
SELECT LAST_INSERT_ID();
LAST_INSERT_ID()
3
INSERT INTO t(a) VALUES(40);
SELECT LAST_INSERT_ID();
LAST_INSERT_ID()
4
# A multi-row insert reserves its ids in one go.
INSERT INTO t(a) VALUES(50),(60),(70);
SELECT LAST_INSERT_ID();
LAST_INSERT_ID()
5
SELECT id, a FROM t ORDER BY id;
id a
1 10
2 20
3 30
4 40
5 50
6 60
7 70
# A DuckDB-side insert after MariaDB ones must not collide: the block
# reservation advanced the shared counter past the cached ids.
SELECT run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(80)");
run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(80)")
Count
BIGINT
[ Rows: 1]
1
SELECT count(*), count(DISTINCT id) FROM t;
count(*) count(DISTINCT id)
8 8
# --- START WITH honours CREATE TABLE ... AUTO_INCREMENT=N -------------
CREATE TABLE t2 (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, a INT) ENGINE=DuckDB AUTO_INCREMENT=100;
SELECT run_in_duckdb("SELECT sequence_name, start_value FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t2'");
run_in_duckdb("SELECT sequence_name, start_value FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t2'")
sequence_name start_value
VARCHAR BIGINT
[ Rows: 1]
mdb_autoinc_t2 100
SELECT run_in_duckdb("INSERT INTO db_autoinc.t2(a) VALUES(1)");
run_in_duckdb("INSERT INTO db_autoinc.t2(a) VALUES(1)")
Count
BIGINT
[ Rows: 1]
1
INSERT INTO t2(a) VALUES(2);
SELECT id, a FROM t2 ORDER BY id;
id a
100 1
101 2
# --- a table without AUTO_INCREMENT gets no sequence ------------------
CREATE TABLE t3 (id INT PRIMARY KEY, a INT) ENGINE=DuckDB;
SELECT run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t3'");
run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t3'")
count_star()
BIGINT
[ Rows: 1]
0
# AUTO_INCREMENT is accepted by CREATE TABLE only; seeding an existing
# column from its current maximum is not implemented.
ALTER TABLE t3 MODIFY COLUMN id INT AUTO_INCREMENT;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
ALTER TABLE t3 ADD COLUMN b INT AUTO_INCREMENT UNIQUE;
ERROR 42000: Storage engine DUCKDB doesn't support AUTO_INCREMENT columns
# The rejected ALTER must leave the table untouched.
SHOW CREATE TABLE t3;
Table Create Table
t3 CREATE TABLE `t3` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=DUCKDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci
# --- setval() is available (local backport of DuckDB PR #24061) --------
SELECT run_in_duckdb("CREATE SEQUENCE db_autoinc.check_setval");
run_in_duckdb("CREATE SEQUENCE db_autoinc.check_setval")
Count
BIGINT
[ Rows: 0]
SELECT run_in_duckdb("SELECT setval('db_autoinc.check_setval', 42)");
run_in_duckdb("SELECT setval('db_autoinc.check_setval', 42)")
setval('db_autoinc.check_setval', 42)
BIGINT
[ Rows: 1]
42
SELECT run_in_duckdb("SELECT nextval('db_autoinc.check_setval')");
run_in_duckdb("SELECT nextval('db_autoinc.check_setval')")
nextval('db_autoinc.check_setval')
BIGINT
[ Rows: 1]
43
SELECT run_in_duckdb("SELECT setval('db_autoinc.check_setval', 100, false)");
run_in_duckdb("SELECT setval('db_autoinc.check_setval', 100, false)")
setval('db_autoinc.check_setval', 100, CAST('f' AS BOOLEAN))
BIGINT
[ Rows: 1]
100
SELECT run_in_duckdb("SELECT nextval('db_autoinc.check_setval')");
run_in_duckdb("SELECT nextval('db_autoinc.check_setval')")
nextval('db_autoinc.check_setval')
BIGINT
[ Rows: 1]
100
SELECT run_in_duckdb("DROP SEQUENCE db_autoinc.check_setval");
run_in_duckdb("DROP SEQUENCE db_autoinc.check_setval")
Success
BOOLEAN
[ Rows: 0]
# --- DROP TABLE removes the sequence ----------------------------------
DROP TABLE t;
SELECT run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t'");
run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t'")
count_star()
BIGINT
[ Rows: 1]
0
DROP TABLE t2;
DROP TABLE t3;
SELECT run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc'");
run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc'")
count_star()
BIGINT
[ Rows: 1]
0

View File

@@ -0,0 +1,84 @@
--source ../include/have_duckdb.inc
--disable_query_log
CREATE DATABASE IF NOT EXISTS db_autoinc CHARACTER SET utf8mb4;
USE db_autoinc;
--enable_query_log
SET NAMES utf8mb4;
--echo #
--echo # MDEV-40266: AUTO_INCREMENT backed by a DuckDB sequence
--echo #
--echo # --- CREATE TABLE creates the sequence -------------------------------
CREATE TABLE t (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, a INT) ENGINE=DuckDB;
SHOW CREATE TABLE t;
--echo # The sequence must exist in DuckDB, named after the table.
SELECT run_in_duckdb("SELECT schema_name, sequence_name, start_value FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' ORDER BY sequence_name");
--echo # The column default must reference that sequence, so that writes which
--echo # bypass MariaDB still draw a non-colliding id from the same counter.
SELECT run_in_duckdb("SELECT column_name, column_default FROM information_schema.columns WHERE table_schema = 'db_autoinc' AND table_name = 't' ORDER BY column_name");
--echo # --- an insert bypassing MariaDB uses the sequence --------------------
SELECT run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(10)");
SELECT run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(20)");
SELECT id, a FROM t ORDER BY id;
--echo # --- MariaDB-side inserts draw from the same sequence ------------------
INSERT INTO t(a) VALUES(30);
SELECT LAST_INSERT_ID();
INSERT INTO t(a) VALUES(40);
SELECT LAST_INSERT_ID();
--echo # A multi-row insert reserves its ids in one go.
INSERT INTO t(a) VALUES(50),(60),(70);
SELECT LAST_INSERT_ID();
SELECT id, a FROM t ORDER BY id;
--echo # A DuckDB-side insert after MariaDB ones must not collide: the block
--echo # reservation advanced the shared counter past the cached ids.
SELECT run_in_duckdb("INSERT INTO db_autoinc.t(a) VALUES(80)");
SELECT count(*), count(DISTINCT id) FROM t;
--echo # --- START WITH honours CREATE TABLE ... AUTO_INCREMENT=N -------------
CREATE TABLE t2 (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, a INT) ENGINE=DuckDB AUTO_INCREMENT=100;
SELECT run_in_duckdb("SELECT sequence_name, start_value FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t2'");
SELECT run_in_duckdb("INSERT INTO db_autoinc.t2(a) VALUES(1)");
INSERT INTO t2(a) VALUES(2);
SELECT id, a FROM t2 ORDER BY id;
--echo # --- a table without AUTO_INCREMENT gets no sequence ------------------
CREATE TABLE t3 (id INT PRIMARY KEY, a INT) ENGINE=DuckDB;
SELECT run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t3'");
--echo # AUTO_INCREMENT is accepted by CREATE TABLE only; seeding an existing
--echo # column from its current maximum is not implemented.
--error ER_TABLE_CANT_HANDLE_AUTO_INCREMENT
ALTER TABLE t3 MODIFY COLUMN id INT AUTO_INCREMENT;
--error ER_TABLE_CANT_HANDLE_AUTO_INCREMENT
ALTER TABLE t3 ADD COLUMN b INT AUTO_INCREMENT UNIQUE;
--echo # The rejected ALTER must leave the table untouched.
SHOW CREATE TABLE t3;
--echo # --- setval() is available (local backport of DuckDB PR #24061) --------
SELECT run_in_duckdb("CREATE SEQUENCE db_autoinc.check_setval");
SELECT run_in_duckdb("SELECT setval('db_autoinc.check_setval', 42)");
SELECT run_in_duckdb("SELECT nextval('db_autoinc.check_setval')");
SELECT run_in_duckdb("SELECT setval('db_autoinc.check_setval', 100, false)");
SELECT run_in_duckdb("SELECT nextval('db_autoinc.check_setval')");
SELECT run_in_duckdb("DROP SEQUENCE db_autoinc.check_setval");
--echo # --- DROP TABLE removes the sequence ----------------------------------
DROP TABLE t;
SELECT run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc' AND sequence_name = 'mdb_autoinc_t'");
DROP TABLE t2;
DROP TABLE t3;
SELECT run_in_duckdb("SELECT count(*) FROM duckdb_sequences() WHERE schema_name = 'db_autoinc'");
--disable_query_log
DROP DATABASE db_autoinc;
--enable_query_log

View File

@@ -0,0 +1,179 @@
diff --git a/src/catalog/catalog_entry/sequence_catalog_entry.cpp b/src/catalog/catalog_entry/sequence_catalog_entry.cpp
index d6a548a267..dbec7a91ac 100644
--- a/src/catalog/catalog_entry/sequence_catalog_entry.cpp
+++ b/src/catalog/catalog_entry/sequence_catalog_entry.cpp
@@ -81,6 +81,28 @@ int64_t SequenceCatalogEntry::NextValue(DuckTransaction &transaction) {
return result;
}
+int64_t SequenceCatalogEntry::SetValue(DuckTransaction &transaction, int64_t value, bool is_called) {
+ {
+ lock_guard<mutex> seqlock(lock);
+ if (value < data.min_value || value > data.max_value) {
+ throw SequenceException("setval: value %lld is out of bounds for sequence \"%s\" (%lld..%lld)", value, name,
+ data.min_value, data.max_value);
+ }
+
+ data.counter = value;
+ if (!is_called) {
+ data.usage_count++;
+ if (!temporary) {
+ transaction.PushSequenceUsage(*this, data);
+ }
+ return value;
+ }
+ }
+
+ // is_called: behave as if nextval() was just invoked and returned `value`.
+ return NextValue(transaction);
+}
+
void SequenceCatalogEntry::ReplayValue(uint64_t v_usage_count, int64_t v_counter) {
if (v_usage_count > data.usage_count) {
data.usage_count = v_usage_count;
diff --git a/src/function/function_list.cpp b/src/function/function_list.cpp
index 4e269f0604..58bf369e01 100644
--- a/src/function/function_list.cpp
+++ b/src/function/function_list.cpp
@@ -153,6 +153,7 @@ static const StaticFunctionDefinition function[] = {
DUCKDB_SCALAR_FUNCTION_SET_ALIAS(RegexpSplitToArrayFun),
DUCKDB_SCALAR_FUNCTION(RemapStructFun),
DUCKDB_SCALAR_FUNCTION(RowFun),
+ DUCKDB_SCALAR_FUNCTION_SET(SetvalFun),
DUCKDB_SCALAR_FUNCTION_SET(SHA1Fun),
DUCKDB_SCALAR_FUNCTION_SET(SHA256Fun),
DUCKDB_SCALAR_FUNCTION_ALIAS(SplitFun),
diff --git a/src/function/scalar/sequence/nextval.cpp b/src/function/scalar/sequence/nextval.cpp
index 8a53af7bbb..366afcbc3d 100644
--- a/src/function/scalar/sequence/nextval.cpp
+++ b/src/function/scalar/sequence/nextval.cpp
@@ -20,17 +20,24 @@ namespace duckdb {
namespace {
struct CurrentSequenceValueOperator {
- static int64_t Operation(DuckTransaction &, SequenceCatalogEntry &seq) {
+ static int64_t Operation(DuckTransaction &, SequenceCatalogEntry &seq, const int64_t, const bool) {
return seq.CurrentValue();
}
};
struct NextSequenceValueOperator {
- static int64_t Operation(DuckTransaction &transaction, SequenceCatalogEntry &seq) {
+ static int64_t Operation(DuckTransaction &transaction, SequenceCatalogEntry &seq, const int64_t, const bool) {
return seq.NextValue(transaction);
}
};
+struct SetValValueOperator {
+ static int64_t Operation(DuckTransaction &transaction, SequenceCatalogEntry &seq, const int64_t value,
+ const bool is_called) {
+ return seq.SetValue(transaction, value, is_called);
+ }
+};
+
SequenceCatalogEntry &BindSequence(Binder &binder, string &catalog, string &schema, const string &name) {
// fetch the sequence from the catalog
Binder::BindSchemaOrCatalog(binder.context, catalog, schema);
@@ -83,10 +90,44 @@ void NextValFunction(DataChunk &args, ExpressionState &state, Vector &result) {
// sequence to use is hard coded
// increment the sequence
result.SetVectorType(VectorType::FLAT_VECTOR);
+
+ // setval takes the new value (and optionally is_called) as extra arguments
+ UnifiedVectorFormat new_val_data;
+ UnifiedVectorFormat is_called_data;
+ bool has_new_val = false;
+ bool has_is_called = false;
+ if (std::is_same<OP, SetValValueOperator>::value) {
+ args.data[1].ToUnifiedFormat(args.size(), new_val_data);
+ has_new_val = true;
+ if (args.ColumnCount() == 3) {
+ args.data[2].ToUnifiedFormat(args.size(), is_called_data);
+ has_is_called = true;
+ }
+ }
+
auto result_data = FlatVector::GetData<int64_t>(result);
+ auto &result_validity = FlatVector::Validity(result);
for (idx_t i = 0; i < args.size(); i++) {
+ int64_t value = 0;
+ bool is_called = true;
+ if (has_new_val) {
+ auto idx = new_val_data.sel->get_index(i);
+ if (!new_val_data.validity.RowIsValid(idx)) {
+ result_validity.SetInvalid(i);
+ continue;
+ }
+ value = UnifiedVectorFormat::GetData<int64_t>(new_val_data)[idx];
+ }
+ if (has_is_called) {
+ auto idx = is_called_data.sel->get_index(i);
+ if (!is_called_data.validity.RowIsValid(idx)) {
+ result_validity.SetInvalid(i);
+ continue;
+ }
+ is_called = UnifiedVectorFormat::GetData<bool>(is_called_data)[idx];
+ }
// get the next value from the sequence
- result_data[i] = OP::Operation(lstate.transaction, lstate.sequence);
+ result_data[i] = OP::Operation(lstate.transaction, lstate.sequence, value, is_called);
}
}
@@ -162,4 +203,25 @@ ScalarFunction CurrvalFun::GetFunction() {
return curr_val;
}
+ScalarFunctionSet SetvalFun::GetFunctions() {
+ ScalarFunction set_val("setval", {LogicalType::VARCHAR, LogicalType::BIGINT}, LogicalType::BIGINT,
+ NextValFunction<SetValValueOperator>, nullptr, nullptr);
+ set_val.SetBindExtendedCallback(NextValBind);
+ set_val.SetSerializeCallback(Serialize);
+ set_val.SetDeserializeCallback(Deserialize);
+ set_val.SetModifiedDatabasesCallback(NextValModifiedDatabases);
+ set_val.SetInitStateCallback(NextValLocalFunction);
+ set_val.SetVolatile();
+ set_val.SetFallible();
+
+ ScalarFunctionSet set_val_set;
+ set_val_set.AddFunction(set_val);
+
+ // Add an overload that takes an additional boolean parameter
+ set_val.arguments.push_back(LogicalType::BOOLEAN);
+ set_val_set.AddFunction(set_val);
+
+ return set_val_set;
+}
+
} // namespace duckdb
diff --git a/src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp b/src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp
index ed12b77a1c..19d3737fb8 100644
--- a/src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp
+++ b/src/include/duckdb/catalog/catalog_entry/sequence_catalog_entry.hpp
@@ -61,6 +61,7 @@ public:
SequenceData GetData() const;
int64_t CurrentValue();
int64_t NextValue(DuckTransaction &transaction);
+ int64_t SetValue(DuckTransaction &transaction, int64_t value, bool is_called);
void ReplayValue(uint64_t usage_count, int64_t counter);
string ToSQL() const override;
diff --git a/src/include/duckdb/function/scalar/sequence_functions.hpp b/src/include/duckdb/function/scalar/sequence_functions.hpp
index 99766a3326..44b8224006 100644
--- a/src/include/duckdb/function/scalar/sequence_functions.hpp
+++ b/src/include/duckdb/function/scalar/sequence_functions.hpp
@@ -35,4 +35,14 @@ struct NextvalFun {
static ScalarFunction GetFunction();
};
+struct SetvalFun {
+ static constexpr const char *Name = "setval";
+ static constexpr const char *Parameters = "sequence_name,value[,is_called]";
+ static constexpr const char *Description = "Set the value of the sequence to the specified value. If is_called is true, the sequence is called. If is_called is false, the sequence is not called.";
+ static constexpr const char *Example = "setval('my_sequence_name', 100, true)";
+ static constexpr const char *Categories = "";
+
+ static ScalarFunctionSet GetFunctions();
+};
+
} // namespace duckdb