CUBRID SERIAL — Sequence/Auto-Increment Subsystem With Catalogged State and Cached Values
Contents:
- Theoretical Background
- Common DBMS Design
- CUBRID’s Approach
- Source Walkthrough
- Cross-check Notes
- Open Questions
- Sources
Theoretical Background
Section titled “Theoretical Background”A sequence (or serial) is the database’s gap-permissive surrogate-key
generator: a stateful object that returns a strictly monotonic — but
not necessarily contiguous — number on every call to nextval. The
sequence object is the textbook escape hatch from the relational
model’s prohibition against side effects in expressions: a SELECT that
reads nextval('s') mutates s and is therefore not idempotent.
Database System Concepts (Silberschatz/Korth/Sudarshan, 7th ed.,
Ch. 5 §5.3) introduces sequences in exactly this language: “a special
object whose only purpose is to dispense unique numeric values” and
notes that, by design, “values dispensed by a sequence are not
recovered if the surrounding transaction aborts”.
That last clause is the load-bearing part of the design. Three textbook trade-offs follow from it and frame the rest of this document:
-
Gapless vs. gap-permissive semantics. A gapless sequence would have to be locked for the full duration of the calling transaction, because rolling back a transaction that consumed value
Nmust putNback. This is incompatible with concurrency: everynextvalbecomes a serialization point. Every mainstream engine — Oracle, PostgreSQL, MySQL, CUBRID — chooses gap-permissive semantics: the sequence advances under a short-lived lock, the increment commits independently of the surrounding user transaction, and rolled-back values are simply skipped. -
Cached vs. non-cached allocation. Even gap-permissive sequences cost a heap-page update per
nextvalif implemented naively. The standard optimisation is caching: a singlenextvalreserves a block ofcached_numvalues in memory and only writes the high water mark of the block back to the catalog. Subsequent calls within the block are pure in-memory arithmetic. The price is bigger gaps on crash and on cache eviction (everything between the last consumed value and the high water mark is lost). -
Transactionality of the catalog write. When a sequence advances, its persistent row mutates. If that mutation rode the user transaction, rolling back the user would roll back the sequence too — defeating gap-permissive semantics. The textbook answer is to perform the catalog write under a system top operation (autonomous transaction in Oracle, system transaction in PostgreSQL): a nested transaction that commits when the user is still in flight, decoupling sequence durability from user durability.
CUBRID realises all three trade-offs explicitly. The cached_num
column is a per-serial choice; the catalog write happens inside
log_sysop_start/log_sysop_commit; and nextval returns under a
short X_LOCK on the serial OID rather than a transaction-long lock.
The remainder of this document tracks how these decisions show up in
the source.
Common DBMS Design
Section titled “Common DBMS Design”Every relational engine that wants concurrent surrogate-key generation reaches for a similar set of patterns.
Sequence as a one-row table
Section titled “Sequence as a one-row table”The sequence is not a primitive: every engine implements it as a one-row table in some catalog-like relation, plus a couple of SQL-level functions that read and update that row.
- PostgreSQL dedicates an entire relation kind (
relkind = 'S') to sequences. EachCREATE SEQUENCE sproduces a real heap with exactly one tuple holdinglast_value,log_cnt, andis_called.nextval('s')uses a special access path (SequenceNextvalincommands/sequence.c) that pins the page, bumps the value, WAL-logs, and returns — outside the snapshot of the calling transaction. - Oracle keeps sequence state in
seq$in the data dictionary and exposess.NEXTVALands.CURRVALas pseudo-columns resolvable in any expression. Cache size (CACHE n) trades crash safety for speed;NOCACHEupdates the dictionary on everyNEXTVAL. Recursive SQL, not a custom access path, mutates the row. - MySQL/InnoDB doesn’t expose sequences as first-class objects.
AUTO_INCREMENT counters live as a per-table counter in
dict_table_t(in-memory) plus a derived value reconstructed by scanningMAX(col) + 1at first use after restart (configurable withinnodb_autoinc_lock_mode). The lock-mode parameter is exactly the gapless-vs-gap-permissive knob made user-visible.
CUBRID’s position
Section titled “CUBRID’s position”CUBRID lands closest to Oracle: a single system class _db_serial
holds one row per sequence and one row per AUTO_INCREMENT column,
addressable by the SQL identifier through a unique B-tree on
unique_name. The nextval(s)/currval(s) functions are real SQL
expressions that compile to XASL T_NEXT_VALUE/T_CURRENT_VALUE
operators (see fetch.c), which call into xserial_get_next_value /
xserial_get_current_value on the server side. The crucial twist
relative to Oracle is that CUBRID does not create a private
sequence-cache structure; the in-memory state is a small hash table
of SERIAL_CACHE_ENTRY objects keyed by serial OID, with the
authoritative state always living in the _db_serial heap. This
means every serial — explicit or AUTO_INCREMENT — has the same
storage shape and the same code path.
CUBRID’s Approach
Section titled “CUBRID’s Approach”_db_serial: one catalog row, twelve+ columns
Section titled “_db_serial: one catalog row, twelve+ columns”The _db_serial system class is bootstrapped at database creation
with a fixed list of columns. The column names live in
storage_common.h as macros so both client (DDL) and server
(executor) refer to the same strings:
// SERIAL_ATTR_* — src/storage/storage_common.h#define SERIAL_ATTR_UNIQUE_NAME "unique_name"#define SERIAL_ATTR_NAME "name"#define SERIAL_ATTR_OWNER "owner"#define SERIAL_ATTR_CURRENT_VAL "current_val"#define SERIAL_ATTR_INCREMENT_VAL "increment_val"#define SERIAL_ATTR_MAX_VAL "max_val"#define SERIAL_ATTR_MIN_VAL "min_val"#define SERIAL_ATTR_START_VAL "start_val"#define SERIAL_ATTR_CYCLIC "cyclic"#define SERIAL_ATTR_STARTED "started"#define SERIAL_ATTR_CLASS_NAME "class_name"#define SERIAL_ATTR_ATTR_NAME "attr_name"#define SERIAL_ATTR_CACHED_NUM "cached_num"#define SERIAL_ATTR_COMMENT "comment"A few of these columns deserve immediate explanation because the
algorithm in serial.c keys directly off them:
current_val. The committed high water mark when the serial is cached, or the last returned value when it is not. After everynextvalinvocation that crossed a cache boundary (or whenever the serial is uncached), the newcurrent_valis written back to this column. Critically, when a serial is cached withcached_num = N, the row storescurrent_val = last_handed_out_within_blockis not what is on disk; what is on disk is the last value of the most recent reserved block, i.e. the in-memorylast_cached_valof the cache entry. New cache misses that read the row will therefore start after every value the previous cache had reserved, even if the previous cache only used the first one.started. A 0/1 sentinel that distinguishes “first nextval” from every subsequent call. On the first call CUBRID returnscurrent_valitself and only then advances — this is howSTART WITH 1ends up returning 1 first, not 1 + increment. After the first call,startedis flipped to 1 and persists.cached_num. 0 or 1 means “no cache, every call hits the heap”. Anything ≥ 2 enables the in-memory cache pool with a block of that many values. The DDL refuses to set values that wouldn’t fit inside(max_val - min_val) / |inc_val|, so a fully-exhausted cache always lines up with cycle/overflow boundaries.class_name/attr_name. Non-NULL exactly for the AUTO_INCREMENT serials synthesized fromCREATE TABLE t (id INT AUTO_INCREMENT). They are the back-link used by ALTER/RENAME and the DROP guard that prevents a user from deleting an AUTO_INCREMENT serial directly.
Two singletons connect the SQL world to the row. The class itself
has a fixed OID stored in oid_Serial_class_oid (storage/oid.c,
in the boot-time oid_Reserved_class_table), and the unique B-tree
on unique_name is named pk_db_serial_unique_name and is cached
once at server start in the BTID serial_Cached_btid
(serial_cache_index_btid). DDL paths therefore translate
identifiers to OIDs by db_find_unique, while runtime paths walk
straight from unique_name to a row through the cached BTID.
State machine of a single serial
Section titled “State machine of a single serial”stateDiagram-v2
[*] --> Created: CREATE SERIAL / AUTO_INCREMENT column
Created --> NotStarted: row inserted with started=0\ncurrent_val=start_val
NotStarted --> Started: first nextval()\nreturn current_val\nflip started=1
Started --> Cached: nextval(), cached_num>1\nreserve block of cached_num
Started --> Persisted: nextval(), cached_num<=1\nupdate current_val on every call
Cached --> Cached: nextval() inside block\npure in-memory math
Cached --> CacheMiss: block exhausted\nadvance high-water in heap\nrefill cache
CacheMiss --> Cached
Cached --> Persisted: xserial_decache()\nDDL or eviction
Persisted --> Persisted: nextval()\nupdate current_val each time
Persisted --> Created: ALTER ... RESTART\nor reset on rename
Cached --> [*]: DROP SERIAL\nor table drop for AI
Persisted --> [*]: DROP SERIAL
The transition Cached → CacheMiss → Cached is the optimisation that
keeps nextval cheap: most calls never leave the in-memory path and
never touch the heap.
nextval flow on the server
Section titled “nextval flow on the server”flowchart TD
A["fetch_peek_dbval()<br/>case T_NEXT_VALUE<br/>(query/fetch.c)"] --> B["xserial_get_next_value(thread_p, oid, cached_num, num_alloc, GENERATE_SERIAL)"]
B --> C{cached_num <= 1?}
C -->|yes| D["xserial_get_next_value_internal()<br/>read row, compute Nth value,<br/>update current_val,<br/>flip started"]
C -->|no| E["lock cache_pool_mutex<br/>mht_get(oid)"]
E --> F{cache hit?}
F -->|yes| G["serial_get_next_cached_value()<br/>compare cur_val vs last_cached_val"]
G --> H{block exhausted?}
H -->|no| I["compute next in-memory<br/>copy to result"]
H -->|yes| J["serial_update_cur_val_of_serial():<br/>refill block via heap update<br/>under log_sysop_start"]
J --> I
F -->|no| K["lock_object(oid, X_LOCK, COND)"]
K --> L{granted?}
L -->|no| M["release mutex,<br/>UNCOND lock_object,<br/>retry try_again"]
M --> E
L -->|yes| N["xserial_get_next_value_internal()"]
N --> O["allocate cache entry,<br/>install in mht,<br/>release X_LOCK"]
I --> P["unlock mutex"]
D --> P
O --> P
P --> Q{is_auto_increment?}
Q -->|yes| R["xsession_set_cur_insert_id()"]
Q -->|no| S["return"]
R --> S
Two things in this flow look unusual relative to a Postgres reader’s intuitions:
- The mutex protects the hash table, not the value. The
protected critical section is the lookup-and-update of the
mht_get/mht_putpair plus arithmetic on the entry. The serial row on the heap is protected by the regular CUBRIDX_LOCKon its OID, exactly like any other heap object — sequences ride the same lock manager that rows ride. - The
try_againretry pattern. If the conditionallock_objectfails (someone else is currently advancing this serial), the worker drops the cache mutex before taking the lock unconditionally and then loops back to re-check the cache. This avoids holding the cache-pool mutex while waiting on a row lock, which would otherwise starve every other sequence in the database.
How AUTO_INCREMENT maps onto serials
Section titled “How AUTO_INCREMENT maps onto serials”CUBRID does not have a separate auto-increment subsystem. When DDL
creates a column with AUTO_INCREMENT, the schema layer synthesizes
a serial whose unique_name is constructed by a single macro:
// SET_AUTO_INCREMENT_SERIAL_NAME — src/object/transform.h#define SET_AUTO_INCREMENT_SERIAL_NAME(SR_NAME, CL_NAME, AT_NAME) \ sprintf ((SR_NAME), "%s_ai_%s", (CL_NAME), (AT_NAME))#define AUTO_INCREMENT_SERIAL_NAME_EXTRA_LENGTH (4) /* "_ai_" */do_create_auto_increment_serial (execute_statement.c) then chooses
sensible defaults — inc_val = 1, start_val = 1, cached_num = 0,
cyclic = 0 — and computes max_val from the SQL type domain
(INT32_MAX for INTEGER, INT64_MAX for BIGINT, the
all-nines numeric for NUMERIC(p,0), etc.). The
column descriptor’s auto_increment field is set to the resulting
MOP. From this point on the AUTO_INCREMENT path is mechanically
identical to an explicit CREATE SERIAL: the same _db_serial row,
the same xserial_get_next_value server call. The only divergence
is at the executor: the is_auto_increment flag passed by the
INSERT path causes xsession_set_cur_insert_id to update the
session’s LAST_INSERT_ID()-equivalent state.
The class-name back-link in _db_serial.class_name is what makes
this safe under DDL. do_drop_serial reads class_name; if it is
non-NULL, the explicit DROP SERIAL is rejected with
ER_QPROC_CANNOT_UPDATE_SERIAL — only DROP TABLE is allowed to
free the AI serial. ALTER TABLE … RENAME COLUMN flows through
do_update_auto_increment_serial_on_rename, which rewrites
unique_name, name, class_name, and attr_name on the same
row (after ws_decache to invalidate the workspace MOP cache).
Cache mechanics
Section titled “Cache mechanics”The cache is process-local on the server side. It consists of:
-
A pool (
SERIAL_CACHE_POOL serial_Cache_pool) holding a hash tableht, a free list of entries, and a singly-linked list of pre-allocatedSERIAL_CACHE_AREAblocks. Each area allocatesNCACHE_OBJECTS = 100entries at once and threads them onto the free list. New areas are appended on demand. -
One
SERIAL_CACHE_ENTRYper active serial:// SERIAL_CACHE_ENTRY — src/query/serial.cstruct serial_entry{OID oid; /* serial object identifier */DB_VALUE cur_val; /* last value handed to a caller */DB_VALUE inc_val;DB_VALUE max_val;DB_VALUE min_val;DB_VALUE cyclic;DB_VALUE started;int cached_num;DB_VALUE last_cached_val; /* high water mark of current block */struct serial_entry *next; /* free-list pointer when free */};
Two values do all the work: cur_val advances on every cached
nextval, last_cached_val is the highest value reserved in the
heap. Block exhaustion is exactly cur_val == last_cached_val. On
exhaustion, serial_update_cur_val_of_serial writes
entry->last_cached_val := serial_get_nth_value(..., nturns * cached_num, ...) back to _db_serial.current_val, in effect
reserving the next block of cached_num values inclusive of the
new boundary, all under a heap-page log record that is
top-operation-committed (so it survives even if the user
transaction aborts).
DDL invalidates this cache via xserial_decache. Every code path in
execute_statement.c that mutates a serial’s row (CREATE, ALTER,
DROP, RENAME, AUTO_INCREMENT max-val update) calls serial_decache
afterwards. The client-side wrapper serial_decache in
network_interface_cl.c ships an sserial_decache request to the
server, which calls xserial_decache to remove the entry and also
purges any cached XASL for queries that referenced the serial
(xcache_remove_by_oid).
DDL flow
Section titled “DDL flow”sequenceDiagram
participant C as CSQL Client
participant P as Parser
participant S as do_create_serial<br/>(execute_statement.c)
participant H as Heap (_db_serial)
participant O as Server (xserial_*)
Note over C,O: CREATE SERIAL s START WITH 1 INCREMENT BY 1 CACHE 100
C->>P: SQL text
P->>S: PT_CREATE_SERIAL
S->>H: sm_find_class("db_serial")
S->>S: validate invariants<br/>(SERIAL_INVARIANT[])
S->>S: do_create_serial_internal()<br/>build OBJTMPL with all 14 attrs
S->>H: dbt_finish_object()<br/>insert row, return OID
Note over C,O: SELECT s.NEXTVAL FROM dual
C->>P: SQL text
P->>O: XASL with T_NEXT_VALUE arith node<br/>(serial_oid, cached_num, num_alloc=1)
O->>O: fetch.c → xserial_get_next_value
O->>H: heap_get_visible_version (peek)<br/>under X_LOCK on serial OID
O->>H: spage_update + log_sysop_commit
O->>O: cache entry installed
O-->>C: returned value
Note over C,O: DROP SERIAL s
C->>P: SQL text
P->>S: do_drop_serial
S->>H: db_drop(serial_object)
S->>O: serial_decache(oid)<br/>(client→server message)
O->>O: xserial_decache: remove from cache, purge XASL
Every DDL path repeats the same idiom: locate the serial MOP via
do_get_serial_obj_id (which is just db_find_unique on
unique_name), authorisation-check via
au_check_serial_authorization, mutate via the OBJTMPL API, and
finally serial_decache to invalidate every server’s in-memory
copy. ALTER SERIAL further runs check_serial_invariants against an
8-slot array of SERIAL_INVARIANT predicates — min_val ≤ max_val,
current_val between min and max, cached_num × |inc| ≤ range,
etc. — before letting the change land.
Source Walkthrough
Section titled “Source Walkthrough”Server-side runtime — src/query/serial.c
Section titled “Server-side runtime — src/query/serial.c”This file is the entire server runtime for serials. It exports four
functions through serial.h, plus two SERVER_MODE-only helpers
that cache the unique-name BTID:
-
xserial_get_current_value/xserial_get_current_value_internal. The non-cached path opens a quick scan cache on the_db_serialclass, peeks the row at the serial OID withheap_get_visible_version, reads only thecurrent_valattribute, and shares the value to the result. The cached path short-circuits toentry->cur_valunder the cache mutex, falling back to the non-cached path whenmht_getmisses. No locking is taken on the serial OID —currvalis read-only and tolerates any visible version. -
xserial_get_next_value/xserial_get_next_value_internal. The driver function and the workhorse. The driver enforcesnum_alloc ≥ 1, takes the cache-pool mutex, looks up the entry, and either advances in-memory or escalates. The escalation path is thetry_againloop above: conditionalX_LOCKon the serial OID, drop-and-retry on contention, then call the workhorse, then release the X_LOCK. The workhorse reads every column of the row (current_val,increment_val,max_val,min_val,cyclic,started,cached_num,unique_namefor the replication key), computes the new value withserial_get_nth_value, writes backcurrent_val, and — if the serial is cached — installs a freshSERIAL_CACHE_ENTRYpopulated with bothcur_val = nextandlast_cached_valset(cached_num - 1)increments past the new value. The first-timestarted == 0path is handled by an extranum_alloc--so the first returned value isstart_valitself. -
serial_get_next_cached_value/serial_update_cur_val_of_serial. Pure in-memory advance vs. heap refill. The first comparescur_valtolast_cached_val; if equal (or, fornum_alloc > 1, if the projectednext_valwould land at or beyondlast_cached_val), it calls the second to advance the on-disk high water mark by an integer number of blocks. The block size iscached_numand the number of blocks needed isCEIL_PTVDIV(num_alloc, cached_num)— so evennextval(s, 1000)reserves the smallest aligned super-block that covers it. -
serial_update_serial_object. The inner page-and-log primitive shared by both the cache-refill path and the uncached-update path. It is a textbook example of CUBRID’s top-operation pattern:log_sysop_start, transform viaheap_attrinfo_transform_to_diskinto a stack-allocated record, log the redo withlog_append_redo_recdes, runspage_update, emit a replication record if applicable, and finallylog_sysop_commit. Theif (lock_mode != X_LOCK)branch elides the top operation when the surrounding transaction already holds an X_LOCK on the serial — that case is reached only from CREATE / ALTER paths where the caller explicitly wants the change to be visible only on the user transaction’s own commit, so a separate sysop would be incorrect. -
serial_get_nth_value. The arithmetic core. Computescur_val + nth × inc_val, with branches for positive and negative increment, range checks againstmax_val/min_val, and cyclic wrap-around to the opposite bound. Errors withER_QPROC_SERIAL_RANGE_OVERFLOWwhen non-cyclic and out of range. All math usesnumeric_db_value_*because serials useDB_TYPE_NUMERICprecision-38 internally — the macrosDB_SERIAL_MAX = "99...9"(38 nines) andDB_SERIAL_MIN(negative counterpart) define the implicit upper bound. -
serial_initialize_cache_pool/serial_finalize_cache_pool/serial_alloc_cache_entry/serial_alloc_cache_area. Boot-time bring-up and free-list management. The pool starts with one area of 100 entries; each new area is allocated on demand and chained ontoserial_Cache_pool.area. -
serial_load_attribute_info_of_db_serial/serial_get_attrid. Lazy bootstrap of the attribute-id table. The first time the server needs to read or write a_db_serialrow, it walks the class record once, matches each attribute’s name (theSERIAL_ATTR_*strings) against an enum of indexes (SR_ATTRIBUTES), and cachesattribute_index → ATTR_IDinserial_Attrs_id[]. All subsequent calls go throughserial_get_attridfor O(1) lookup. -
serial_set_cache_entry/serial_clear_value. Boilerplate helpers that clone or release theDB_VALUEs held inside an entry. They are paired withpr_clone_value/pr_clear_valueto honour CUBRID’s deep-copy convention for variable-length values like NUMERIC. -
xserial_decache. Called from the client-server protocol on every DDL completion. Removes the entry from the hash, clears its values, and links it back onto the free list. Also callsxcache_remove_by_oidso any compiled XASL that referenced this serial gets thrown away (a query plan that inlined a stale cached_num is now invalid). -
serial_cache_index_btid/serial_get_index_btid(SERVER_MODE only). Looked up at server start and cached inserial_Cached_btid. The B-tree’s name is hard-coded topk_db_serial_unique_name. This BTID is what the locator uses to translateunique_nameto OID without re-resolving through the catalog every time.
Client-side DDL — src/query/execute_statement.c
Section titled “Client-side DDL — src/query/execute_statement.c”The client path holds all the policy. The executor file’s serial section hosts:
- Identifier resolution.
do_get_serial_obj_idis the standard entry:db_find_uniqueonunique_name. Aloaddbcompatibility shim underDB_CLIENT_TYPE_ADMIN_LOADDB_COMPAT_UNDER_11_2falls back todo_find_serial_by_query(a SELECT against_db_serialby legacy non-qualified name) when the qualified lookup misses — pre-11.2 unloads stored bare names, and the 11.2+ loader rebuilds the user-qualifiedunique_nameon the fly. - CREATE.
do_create_serialparses the AST, fills fourDB_VALUEslots (start,inc,min,max), runs domain coercion toNUMERIC(38,0), builds aSERIAL_INVARIANT[]array of consistency checks, and finally callsdo_create_serial_internalwhich builds an OBJTMPL by setting everySERIAL_ATTR_*attribute and finishing the object. The authorisation check is a simpleAU_DISABLEbracket: serial DDL always touches_db_serialwith auth temporarily disabled, then applies its own owner check (!ws_is_same_object(owner, Au_user) && !au_is_dba_group_member). - ALTER.
do_alter_serialreads the current row, applies the delta from the AST (each ofcurrent_val,inc,min,max,cyclic,cached_numis independently optional), re-runs the invariants against the merged image, writes through OBJTMPL, and ends withserial_decache. The same routine handlesALTER SERIAL ... RESTART WITH n, which is implemented as a CURRENT_VAL update plusSTARTED = 0so the new value is returned by the nextnextvalrather thanmin_val + inc. - DROP.
do_drop_serialenforces the AUTO_INCREMENT guard (class_name IS NOT NULL⇒ refuse), callsdb_dropon the MOP, andserial_decaches. - AUTO_INCREMENT.
do_create_auto_increment_serialis the column-level entrypoint invoked bydo_add_attribute(inexecute_schema.c) whenever a PT_NODE withauto_increment != NULLis added. It composes the serial name with the macro, assembles defaults, and delegates todo_create_serial_internal.do_update_maxvalue_of_auto_increment_serialis the corollary forALTER TABLE ... CHANGE COLUMN type: it widens the serial’smax_valto fit a wider integer type without resetting any state.do_reset_auto_increment_serial(called fromTRUNCATE TABLE) rewinds the serial toMIN_VALand clearsstarted.do_change_auto_increment_serial(ALTER TABLE ... AUTO_INCREMENT = n) reseatscurrent_val,min_val, andstarted.do_update_auto_increment_serial_on_renamerewrites the four back-link columns underws_decacheso the workspace doesn’t serve a stale row. - Helpers.
do_get_serial_cached_numis a one-liner that fetchesSERIAL_ATTR_CACHED_NUMand is what the executor calls when compiling anextvalcall site to embedcached_numinto the XASL — that XASL constant is the samecached_numthat feedsxserial_get_next_valueon the server (and thatxserial_decacheinvalidates by purging the XASL cache).
CREATE TABLE wiring — src/query/execute_schema.c
Section titled “CREATE TABLE wiring — src/query/execute_schema.c”When CREATE TABLE t (id INTEGER AUTO_INCREMENT, …) runs,
do_add_attribute walks each PT_NODE column. If the column carries
auto_increment != NULL, the schema layer pulls
do_create_auto_increment_serial from execute_statement.c to
manufacture the _db_serial row, then stores the resulting MOP into
the in-progress SM_TEMPLATE’s attribute via
smt_set_attribute_auto_increment. The template later flushes a
class object whose attribute carries that MOP, and the
auto_increment field of the SM_ATTRIBUTE becomes the persistent
client-side breadcrumb. At INSERT time the executor reads
auto_increment.serial_obj from the attribute and emits a
T_NEXT_VALUE arith node into the XASL, with cached_num baked in
via do_get_serial_cached_num. RENAME COLUMN routes through
smt_change_attribute_w_dflt_w_order, which in turn calls
do_update_auto_increment_serial_on_rename. RENAME TABLE handles
the same back-link rewrite at the table level. DROP TABLE deletes
every AUTO_INCREMENT serial it owned.
nextval/currval evaluation — src/query/fetch.c
Section titled “nextval/currval evaluation — src/query/fetch.c”The end of the call chain is in fetch_peek_dbval. The XASL
arithmetic operator nodes for serial.next_value() and
serial.current_value() decode to T_NEXT_VALUE and
T_CURRENT_VALUE:
// fetch_peek_dbval — src/query/fetch.c (case T_NEXT_VALUE)serial_oid = db_get_oid (peek_left);cached_num = db_get_int (peek_right);num_alloc = db_get_int (peek_third);
if (xserial_get_next_value (thread_p, arithptr->value, serial_oid, cached_num, num_alloc, GENERATE_SERIAL, false) != NO_ERROR) { goto error; }Two flags are passed by value from the parser through XASL to the call site:
GENERATE_SERIALvsGENERATE_AUTO_INCREMENT. Only the second causesxserial_get_next_valueto update the session’sLAST_INSERT_ID(); an explicits.NEXTVALdoes not.force_set_last_insert_id. Forces the LAST_INSERT_ID update even when there is already one set in the session. The INSERT path passes it asfalsefor the first andtruefor the rest of a multi-row insert that targets two AI columns at once.
The flag also ensures the arithmetic node is stamped
REGU_VARIABLE_FETCH_NOT_CONST, preventing the optimiser from
folding it. Without that flag, the first call would be cached as a
constant and reused — silently turning gap-permissive sequences into
broken sequences.
Position hints (as of 2026-05-01)
Section titled “Position hints (as of 2026-05-01)”| Symbol | File | Line |
|---|---|---|
SR_ATTRIBUTES enum | src/query/serial.c | 56 |
SERIAL_CACHE_ENTRY (struct serial_entry) | src/query/serial.c | 78 |
SERIAL_CACHE_POOL (struct serial_cache_pool) | src/query/serial.c | 106 |
serial_Cache_pool (singleton) | src/query/serial.c | 119 |
serial_Cached_btid | src/query/serial.c | 124 |
xserial_get_current_value | src/query/serial.c | 158 |
xserial_get_current_value_internal | src/query/serial.c | 200 |
xserial_get_next_value | src/query/serial.c | 284 |
serial_get_next_cached_value | src/query/serial.c | 418 |
serial_update_cur_val_of_serial | src/query/serial.c | 507 |
xserial_get_next_value_internal | src/query/serial.c | 635 |
serial_update_serial_object | src/query/serial.c | 917 |
serial_get_nth_value | src/query/serial.c | 1019 |
serial_initialize_cache_pool | src/query/serial.c | 1117 |
serial_finalize_cache_pool | src/query/serial.c | 1155 |
serial_get_attrid | src/query/serial.c | 1189 |
serial_load_attribute_info_of_db_serial | src/query/serial.c | 1216 |
serial_set_cache_entry | src/query/serial.c | 1349 |
xserial_decache | src/query/serial.c | 1414 |
serial_cache_index_btid | src/query/serial.c | 1486 |
SERIAL_ATTR_* macros | src/storage/storage_common.h | 1127 |
oid_Serial_class_oid | src/storage/oid.c | 80 |
oid_get_serial_oid | src/storage/oid.c | 171 |
SET_AUTO_INCREMENT_SERIAL_NAME | src/object/transform.h | 120 |
do_evaluate_default_expr | src/query/execute_statement.c | 430 |
do_create_serial_internal | src/query/execute_statement.c | 672 |
do_update_auto_increment_serial_on_rename | src/query/execute_statement.c | 877 |
do_reset_auto_increment_serial | src/query/execute_statement.c | 1023 |
do_change_auto_increment_serial | src/query/execute_statement.c | 1110 |
do_get_obj_id / do_get_serial_obj_id | src/query/execute_statement.c | 1262 / 1334 |
do_get_serial_cached_num | src/query/execute_statement.c | 1378 |
do_create_serial | src/query/execute_statement.c | 1405 |
do_create_auto_increment_serial | src/query/execute_statement.c | 1872 |
do_update_maxvalue_of_auto_increment_serial | src/query/execute_statement.c | 2128 |
do_alter_serial | src/query/execute_statement.c | 2330 |
do_drop_serial | src/query/execute_statement.c | 2982 |
T_NEXT_VALUE / T_CURRENT_VALUE cases | src/query/fetch.c | 2422 / 2445 |
serial_decache (client wrapper) | src/communication/network_interface_cl.c | 7698 |
sserial_decache (server stub) | src/communication/network_interface_sr.cpp | 6688 |
Cross-check Notes
Section titled “Cross-check Notes”- Ownership of cache state. The cache is a per-server-process
singleton. In HA replication each replica has its own pool, but
values produced on the master are replicated through supplemental
log records emitted by
serial_update_cur_val_of_serial(log_append_supplemental_serial); the replica does not generate values independently. The replica’s cache only matters if it is later promoted, at which pointserial_initialize_cache_poolstarts it cold. This invariant is implicit, not documented. startedis committed before the value is returned. The firstnextvalflipsstarted=0→1and writes bothcurrent_valandstartedin the sameserial_update_serial_objectcall. If the user transaction aborts after consuming the first value, the row keepsstarted=1and the secondnextvalreturnsstart_val + inc, notstart_val. This is the correct gap-permissive semantics, but it is worth flagging because users occasionally read CUBRID’sstartedflag and assume otherwise.current_valafter a cache refill. Reading_db_serial.current_valdirectly from SQL on a cached serial returns the high water mark of the most recent reserved block, not the value last actually returned bynextval. The authoritative “last value returned” lives inserial_Cache_pool.ht[oid].cur_val, which is process-private. Tools that try to reset a sequence byUPDATE _db_serial SET current_val = ...therefore need to also issue aserial_decache(or restart the server) before the change is observed by the nextnextval. Production code uses the ALTER SERIAL DDL precisely to avoid this trap; it ends withserial_decache.- The cache mutex is global.
cache_pool_mutexprotects every serial’s hash entry, not per-serial state. Workloads with many distinct serials see contention on this single mutex even when the underlying serials are independent. The retry-on-X_LOCK-failure pattern drops the mutex while the lock manager is involved, so the worst case is bounded by in-memory math. stack_blockfor record assembly.serial_update_serial_objectusescubmem::stack_block<IO_MAX_PAGE_SIZE>to build the redo-image record without a heap allocation. The function’s redo path is zero-malloc, which matters because it executes inside the cache-pool mutex critical section.- Replication and
lock_mode != X_LOCK. The branch that elideslog_sysop_startwhen the caller already holds an X_LOCK on the serial is reached only on CREATE and ALTER, not onnextval.nextvalalways opens a sysop. The replication-flush mark is also conditional onlock_mode != X_LOCK: an in-progress CREATE/ALTER must not emit a normal RBR record because the row itself is part of the user transaction’s writeset and ships through the regular replication stream on user commit. - AUTO_INCREMENT name collision. Because the serial name format
is
<class>_ai_<attr>, a user-defined serial with that exact name is technically possible. CREATE SERIAL does not forbid underscores; it relies ondo_get_serial_obj_idreturningER_QPROC_SERIAL_ALREADY_EXISTfromdo_create_auto_increment_serialwhen the conflict is detected.
Open Questions
Section titled “Open Questions”- Cache eviction policy. The cache pool has no size limit and
no eviction. Entries accumulate until DDL evicts them via
xserial_decacheor until the server restarts. For workloads that touch many distinct serials briefly (per-tenant AUTO_INCREMENT in a multi-tenant schema), this could grow without bound; the practical upper bound before memory pressure shows is unverified. nextvalunder hot standby. A read replica must rejectnextval.CHECK_MODIFICATION_NO_RETURNinxserial_get_next_valuereturnsER_DB_NO_MODIFICATIONSon read-only replicas, but whether the same check fires on a fully passive HA standby requires readingboot_sr.c’s recovery state machine.- Numeric overflow at the 38-digit boundary.
serial_get_nth_valuechecks the bound and raisesER_QPROC_SERIAL_RANGE_OVERFLOWonly when not cyclic. The intermediateinc_val * nturns * cached_numcomputation could exceed 38 digits before the comparison runs ifcached_numis pathological; saturation behaviour ofnumeric_db_value_mulis not obvious from this file. - Cross-statement isolation of cached values. The cache is
process-global, not session-global.
s.NEXTVALevaluated twice in one statement returns two different values regardless of the SQL standard’s same-expression-same-value rule — matching PostgreSQL, diverging from Oracle. The codebase has no comment confirming intent. - 11.2 unload/load compatibility shim. The fallback in
do_get_serial_obj_idforDB_CLIENT_TYPE_ADMIN_LOADDB_COMPAT_UNDER_11_2suggests that pre-11.2 dumps stored serial names without the user qualifier and that 11.2+ loaders bridge that gap. Whether this is the only place the qualifier migration leaks is unverified.
Sources
Section titled “Sources”src/query/serial.c(39 KB) — full server-side runtime, cache pool, attribute-id bootstrap, BTID caching.src/query/serial.h(1 KB) — exported function set.src/query/execute_statement.c— every DDL entrypoint (do_create_serial,do_alter_serial,do_drop_serial,do_create_auto_increment_serial,do_get_serial_obj_id,do_get_serial_cached_num, and the AUTO_INCREMENT companions for rename / reset / max-val update / change).src/query/execute_schema.c— table-level wiring that creates per-column AUTO_INCREMENT serials and rewrites their back-links on RENAME COLUMN.src/query/fetch.c— XASL evaluation ofT_NEXT_VALUEandT_CURRENT_VALUEarithmetic nodes (callsxserial_get_next_value/xserial_get_current_value).src/storage/storage_common.h—SERIAL_ATTR_*column-name macros,DB_SERIAL_MAX/DB_SERIAL_MINnumeric bounds.src/storage/oid.c/oid.h—oid_Serial_class_oidreserved OID for_db_serial,oid_get_serial_oid.src/object/transform.h—SET_AUTO_INCREMENT_SERIAL_NAMEmacro,AUTO_INCREMENT_SERIAL_NAME_EXTRA_LENGTH.src/communication/network_interface_cl.c/network_interface_sr.cpp— theserial_decacheclient/server stubs that propagate DDL invalidation to every server process.