Skip to content

CUBRID DDL Execution — Schema-Change Pipeline From Parse Tree to Catalog and Class-Object Cache Invalidation

Contents:

The Data-Definition Language is the part of SQL that does not move rows; it moves the shape under which rows are interpreted. SQL’s DDL statements — CREATE TABLE, ALTER TABLE, DROP TABLE, CREATE INDEX, RENAME, TRUNCATE and their object-relational and partitioning extensions — together build the schema graph from which every later SELECT/INSERT/UPDATE plan is compiled. Database System Concepts (Silberschatz et al., 7th ed., §5.2 and §15.5) captures the textbook structure of any DDL implementation in five movements: parse the DDL into a definition tree, validate it against the existing catalog, materialise the on-disk artefacts (heap, B-trees, foreign-key descriptors), insert/update/delete the matching catalog rows, and broadcast invalidation so caches that referenced the old shape are flushed before the next plan compile.

Several design decisions are forced by what kind of DBMS the engine is.

  1. Transactional vs. non-transactional DDL. SQL Server, DB2, Postgres and MySQL 8.0 wrap DDL in the same WAL/recovery framework as DML — CREATE TABLE + ROLLBACK is identical to no CREATE TABLE. Older MySQL releases and Oracle treat DDL as an implicit commit. A transactional engine must journal every catalog mutation through the same redo/undo log as user data; an implicit-commit engine only needs the catalog row to land eventually.

  2. Schema-change vs. data movement. Pure-metadata DDL (CREATE TABLE, DROP INDEX) takes a short exclusive lock and exits. Data-touching DDL (ALTER … ADD COLUMN c NOT NULL DEFAULT 0, partition reorg, TRUNCATE) needs a cursor-style scan that can run for hours under the same long DDL.

  3. DDL fence and cache invalidation. Every cached plan referencing a changed class must be invalidated before a new plan compiles against the new shape. The standard discipline is a monotonically increasing schema version: Oracle’s Library Cache Lock, Postgres’ CommandCounterIncrement + relcache invalidation messages, CUBRID’s sm_bump_local_schema_version plus a per-class chn on each MOP.

  4. Staging. Mutating the live class would expose partial states to concurrent readers. The standard answer is an in-memory template the engine mutates freely and swaps in atomically at end-of-statement under exclusive lock. Postgres rewrites a Relation in place under AccessExclusiveLock; InnoDB has online DDL with a ghost copy + row-log; Oracle uses DBMS_REDEFINITION. CUBRID’s answer is the SM_TEMPLATE — a working copy of SM_CLASS inside a DB_CTMPL handle, installed by sm_finish_class → install_new_representation.

  5. Catalog representation. Tuple-style engines (Postgres pg_class / pg_attribute, MySQL 8.0 mysql.tables, Oracle dba_objects) hold one row per object and treat DDL as an INSERT/UPDATE/DELETE over privileged system tables. Object-style engines treat each class as an instance of a meta-class (_db_class, _db_attribute); CUBRID is the second style — the catalog is the set of _db_-prefixed classes and catcls_* in src/storage/catalog_class.c translates between SM_CLASS and the heap-row representation.

The book chapters that motivate this layering are Database System Concepts §5.2 (DDL syntax), §15.5 (System Catalog), and §17 (Recovery and the role of WAL for DDL). Petrov’s Database Internals (Ch. 4) frames the same machinery from the perspective of a single-node engine: schema state, when written to disk through the same B-tree that holds tuples, automatically inherits durability, locking, and crash recovery.

Postgres’ DDL pipeline is a useful baseline. A CREATE TABLE parses to a CreateStmt node; the ProcessUtility switch in src/backend/tcop/utility.c dispatches to DefineRelation. DefineRelation builds a TupleDesc (equivalent in spirit to an SM_TEMPLATE), calls heap_create_with_catalog to allocate the relfilenode, the pg_class row, and the pg_attribute rows, then registers relevant constraints. Two further moves wire up the cache: every catalog INSERT goes through CatalogTupleInsert, which records an invalidation message for the relfilenode and the relid; CommandCounterIncrement at end-of-statement makes that invalidation visible to subsequent statements in the same transaction. The relcache rebuilds lazily on next access. DDL is fully transactional: a ROLLBACK undoes both the catalog rows and the file-system operations through pendingDeletes.

MySQL 8.0’s atomic DDL drove a complete rewrite of the data dictionary: every DDL is a single InnoDB transaction over the new mysql.tables / mysql.columns system tables, with the file-system operations queued through a DDL log so that crash recovery either commits the entire DDL or rolls it back. Earlier MySQL versions famously did not — CREATE TABLE could leave the .frm file behind even on rollback. The lesson is that storage engine and dictionary must share a transactional substrate; if they do not, atomic DDL is unreachable.

Oracle takes the opposite route: every DDL is bracketed by an implicit commit. CREATE TABLE first commits any pending user transaction, then runs in its own transaction over the data dictionary, then commits again. The shared cursor cache uses the library cache lock (a hash partition latch) to fence cached plans: the DDL takes the lock in exclusive mode, marks dependent cursors invalid, and releases the lock; subsequent compiles take the lock in shared mode and re-parse if any dependency was marked.

SQL Server and DB2 are similar in spirit to Postgres: catalog rows live in regular system tables, DDL is fully transactional, and a per-database “schema version” plus per-statement recompilation handles cache invalidation.

CUBRID combines several traits from this menu. Like Postgres and MySQL 8.0 it is fully transactional: every DDL acquires a system savepoint at entry and rolls back to that savepoint on any error path, so a partial ALTER TABLE cannot leak. Like Oracle it stages the new shape in an in-memory template before committing; CUBRID’s SM_TEMPLATE is the analogue of Oracle’s work area inside the data-dictionary cache, but it lives client- side in the workspace rather than inside a shared library cache. Like Postgres, it bumps a monotonically increasing version number to invalidate plan caches; sm_bump_local_schema_version is the direct analogue of CommandCounterIncrement. The catalog is written through catcls_* rather than through plain heap_insert because CUBRID’s catalog is a set of system classes rather than ordinary tables; an SM_CLASS becomes a heap row in _db_class only after the template has been finalised.

The walkthrough that follows will show how these pieces are arranged into a single pipeline, where each CUBRID-specific choice — workspace MOPs, locator-mediated heap creation, two-phase partitioning — fits in.

flowchart TB
  SQL[SQL text]
  SQL --> Parse[csql_grammar.y<br/>PT_CREATE_ENTITY / PT_ALTER / PT_DROP]
  Parse --> Sem[name_resolution<br/>· semantic_check]
  Sem --> DoStmt[do_statement / do_execute_statement<br/>switch on PT_NODE_TYPE]
  DoStmt --> CE[do_create_entity]
  DoStmt --> AL[do_alter]
  DoStmt --> DR[do_drop]
  DoStmt --> CI[do_create_index]
  DoStmt --> TR[do_create_trigger]
  DoStmt --> SE[do_create_serial]
  DoStmt --> US[do_create_user / do_grant]
  CE --> Tmpl[dbt_create_class<br/>= smt_def_class]
  AL --> Tmpl2[dbt_edit_class<br/>= smt_edit_class_mop]
  Tmpl --> Local[do_create_local<br/>do_add_attributes / do_add_constraints]
  Tmpl2 --> Local
  Local --> Finish[dbt_finish_class<br/>= sm_finish_class -> update_class]
  Finish --> Install[install_new_representation<br/>· allocate_disk_structures]
  Install --> Locator[locator_add_class<br/>locator_create_heap_if_needed]
  Locator --> Catcls[catcls_insert_catalog_classes<br/>via locator flush]
  Catcls --> Bump[sm_bump_local_schema_version<br/>· XASL cache invalidation on next access]
  DR --> DropPath[db_drop_class_ex<br/>-> sm_delete_class_mop]
  DropPath --> Locator

The pipeline is the spine of the DDL implementation: every concrete DDL statement is a specialisation of the same path. The remainder of this section walks each phase in detail, with code excerpts preserved verbatim so the reader does not have to chase the source tree.

Top-level dispatch — do_statement and the DDL switch

Section titled “Top-level dispatch — do_statement and the DDL switch”

The single-statement entry point is do_statement in execute_statement.c. It is called once per parsed PT_NODE. The function does three things in order: assess the read-fetch instance version (DDL needs LC_FETCH_DIRTY_VERSION because we are about to write the catalog), then a giant switch (statement->node_type) that routes each parse-tree kind to its handler, then a tail that triggers replication and supplemental log capture for HA.

// do_statement — execute_statement.c (DDL slice of the switch)
case PT_CREATE_ENTITY: error = do_check_internal_statements (parser, statement, do_create_entity); break;
case PT_ALTER: error = do_check_internal_statements (parser, statement, do_alter); break;
case PT_DROP: (void) do_reserve_classinfo (parser, statement, cls_info);
error = do_check_internal_statements (parser, statement, do_drop); break;
case PT_CREATE_INDEX: error = do_create_index (parser, statement); break;
case PT_DROP_INDEX: error = do_drop_index (parser, statement); break;
case PT_ALTER_INDEX: error = do_alter_index (parser, statement); break;
case PT_RENAME: error = do_rename (parser, statement); break;
case PT_TRUNCATE: error = do_truncate (parser, statement); break;
case PT_CREATE_TRIGGER: error = do_create_trigger (parser, statement); break;
case PT_DROP_TRIGGER: error = do_drop_trigger (parser, statement); break;
case PT_ALTER_TRIGGER: error = do_alter_trigger (parser, statement); break;
case PT_CREATE_SERIAL: error = do_create_serial (parser, statement); break;
case PT_ALTER_SERIAL: error = do_alter_serial (parser, statement); break;
case PT_DROP_SERIAL: error = do_drop_serial (parser, statement); break;
case PT_CREATE_USER: error = do_create_user (parser, statement); break;
case PT_DROP_USER: error = do_drop_user (parser, statement); break;
case PT_GRANT: error = do_grant (parser, statement); break;
case PT_REVOKE: error = do_revoke (parser, statement); break;
case PT_CREATE_STORED_PROCEDURE: error = jsp_create_stored_procedure (parser, statement); break;

The wrapper do_check_internal_statements is currently a thin pass-through to do_func (parser, statement) — it exists to host text-domain internal statements that are presently disabled behind #if 0. The mental model is “this dispatch line is the DDL switch”, with the wrapper a vestigial hook for auto-generated companion statements.

After the handler returns, the same do_statement body wires the DDL into HA replication:

// do_statement — execute_statement.c (post-handler tail)
if (need_stmt_replication)
{
int repl_error = NO_ERROR;
if (error >= 0)
{
repl_error = locator_all_flush ();
}
suppress_repl_error = db_set_suppress_repl_on_transaction (false);
if (error >= 0 && repl_error == NO_ERROR && suppress_repl_error == NO_ERROR)
{
repl_error = do_replicate_statement (parser, statement);
}
/* ... */
}
if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) > 0)
{
(void) do_supplemental_statement (parser, statement, cls_info, reserved_oid);
}

locator_all_flush () is the critical call: any transient workspace MOPs created during the DDL get flushed to disk before the schema-replication record is written, otherwise the log applier on the slave would see a catalog row pointing at a class with no heap file.

CREATE TABLE — building the SM_TEMPLATE end-to-end

Section titled “CREATE TABLE — building the SM_TEMPLATE end-to-end”

A CREATE TABLE parse-tree node carries entity_type = PT_CLASS, the name, the column list, the constraint list, optional LIKE source class, optional AS SELECT query, optional partition info, and table options (charset, collation, comment, REUSE_OID, ENCRYPT). The handler do_create_entity (execute_schema.c) follows a fixed sequence: take a system savepoint, build a template, populate it via do_create_local, finish the template, then run the post-create steps.

sequenceDiagram
  participant Parser
  participant DoCE as do_create_entity
  participant Smt as smt_def_class<br/>(schema_template.c)
  participant Loc as locator_reserve_class_name<br/>(locator_cl.c)
  participant DCL as do_create_local
  participant SmF as sm_finish_class<br/>(schema_manager.c)
  participant SmU as update_class<br/>install_new_representation
  participant LAdd as locator_add_class
  participant Heap as locator_create_heap_if_needed<br/>· heap_create
  participant Cat as catcls_insert_catalog_classes<br/>(catalog_class.c)

  Parser->>DoCE: PT_NODE { create_entity }
  DoCE->>DoCE: tran_system_savepoint("cREATEeNTITY")
  DoCE->>Smt: dbt_create_class(name)
  Smt->>Loc: locator_reserve_class_name(name)
  Loc-->>Smt: pseudo-OID, SCH_M_LOCK
  Smt-->>DoCE: SM_TEMPLATE
  DoCE->>DCL: do_create_local(parser, ctemplate, node)
  DCL->>DCL: do_add_attributes
  DCL->>DCL: do_add_constraints
  DCL->>DCL: do_check_fk_constraints
  DCL-->>DoCE: NO_ERROR
  DoCE->>SmF: dbt_finish_class(ctemplate)
  SmF->>SmU: update_class(template, &classmop, ...)
  SmU->>SmU: lockhint_subclasses<br/>flatten_template
  SmU->>LAdd: locator_add_class(SM_CLASS, name)
  LAdd-->>SmU: classmop (cached in workspace)
  SmU->>SmU: install_new_representation
  SmU->>SmU: allocate_disk_structures (B-trees)
  SmU-->>DoCE: classmop
  DoCE->>Heap: locator_create_heap_if_needed(class_obj, reuse_oid)
  Heap->>Heap: heap_create(hfid, oid, reuse_oid)
  Heap-->>DoCE: hfid set on SM_CLASS
  DoCE->>DoCE: do_create_partition (if partitioned)
  DoCE->>DoCE: do_create_index (per CREATE INDEX clause)
  DoCE->>DoCE: locator_flush_class -> Cat
  Cat-->>DoCE: row in _db_class
  DoCE-->>Parser: NO_ERROR

The savepoint at do_create_entity head is named UNIQUE_SAVEPOINT_CREATE_ENTITY = "cREATEeNTITY" and is the only thing that makes a CREATE TABLE failure recoverable: if any later step fails, tran_abort_upto_system_savepoint rolls the entire DDL back to before the catalog mutation.

// do_create_entity — execute_schema.c (skeleton)
class_name = node->info.create_entity.entity_name->info.name.original;
/* ... super-class partitioning checks, table option parsing ... */
error = tran_system_savepoint (UNIQUE_SAVEPOINT_CREATE_ENTITY);
do_rollback_on_error = true;
ctemplate = create_like ? dbt_copy_class (class_name, create_like, &source_class)
: dbt_create_class (class_name);
if (!create_like) error = do_create_local (parser, ctemplate, node, query_columns);
class_obj = dbt_finish_class (ctemplate);
if (locator_create_heap_if_needed (class_obj, reuse_oid) == NULL) { error = er_errid (); break; }
if (node->info.create_entity.partition_info != NULL)
error = do_create_partition (parser, node, &info);
for (create_index = node->info.create_entity.create_index;
create_index; create_index = create_index->next)
error = do_create_index (parser, create_index);

dbt_create_class is in src/compat/db_temp.c. It is a thin wrapper around smt_def_class that also reserves the class name with the server through locator_reserve_class_name:

// dbt_create_class + dbt_reserve_name — db_temp.c (condensed)
def = smt_def_class (name); /* allocate SM_TEMPLATE */
if (def != NULL) def = dbt_reserve_name (def, name);
return def;
// dbt_reserve_name:
reserved = locator_reserve_class_name (def->name, &class_oid); /* server hash insert */
if (reserved != LC_CLASSNAME_RESERVED)
{ /* LC_CLASSNAME_EXIST -> ER_LC_CLASSNAME_EXIST */ smt_quit (def); return NULL; }
return def;

Reserving the name before the template is populated lets two concurrent CREATE TABLE statements race for the name and see a deterministic loser — the second one gets LC_CLASSNAME_EXIST and aborts, regardless of which one actually finishes the template first. This is CUBRID’s analogue of Postgres’ namespace-level lock taken in RangeVarGetAndCheckCreationNamespace.

do_create_local is the column/constraint plumbing; it is a straight sequence of do_add_* helpers, each of which mutates the template:

// do_create_local — execute_schema.c (template-population sequence)
error = do_add_attributes (parser, ctemplate, attr_def_list, constraint_list, query_columns);
error = do_add_attributes (parser, ctemplate, class_attr_def_list, NULL, NULL);
error = do_add_constraints (ctemplate, constraint_list);
error = do_check_fk_constraints (ctemplate, constraint_list);
error = do_add_methods (parser, ctemplate, method_def_list);
error = do_add_method_files (parser, ctemplate, method_file_list);
error = do_add_resolutions (parser, ctemplate, resolution_list);
error = do_add_supers (parser, ctemplate, supclass_list);
error = do_add_queries (parser, ctemplate, as_query_list);
error = do_set_object_id (parser, ctemplate, object_id_list);

The result is a fully populated SM_TEMPLATE that only lives in the executor’s workspace. The catalog has not been touched yet; the only thing on the server side is the reserved name entry from locator_reserve_class_name.

dbt_finish_class calls sm_finish_class, which in turn calls update_class — the central template-installation routine.

// sm_finish_class — schema_manager.c
int
sm_finish_class (SM_TEMPLATE * template_, MOP * classmop)
{
return update_class (template_, classmop, 0, AU_ALTER, true);
}

update_class is the heart of the DDL pipeline. It does, in order: bump the local schema version, fetch the existing SM_CLASS if this is an ALTER (NULL if CREATE), pre-lock the subclass lattice and the super-classes, flatten the template (merging inherited components), lock the subclasses for write, flatten subclasses, allocate the persistent class object via locator_add_class if new, install the new representation, allocate the disk B-trees, and finally update the super- and sub-class lists.

// update_class — schema_manager.c (condensed)
sm_bump_local_schema_version ();
error = tran_system_savepoint (SM_ADD_UNIQUE_CONSTRAINT_SAVEPOINT_NAME);
if ((error == NO_ERROR) && (template_->op != NULL))
error = au_fetch_class (template_->op, &class_, AU_FETCH_UPDATE, auth);
if (needs_hierarchy_lock)
{
error = lockhint_subclasses (template_, class_);
error = lock_supers (template_, class_ ? class_->inheritance : NULL, &oldsupers, &newsupers);
}
if (class_ != NULL) class_->new_ = template_;
error = flatten_template (template_, NULL, &flat, auto_res);
if (needs_hierarchy_lock)
error = lock_subclasses (template_, newsupers, class_ ? class_->users : NULL, &newsubs);
class_->new_ = flat;
error = flatten_subclasses (newsubs, NULL);
if (class_ == NULL) /* fresh class -> new MOP */
{
class_ = classobj_make_class (template_->name);
/* ... owner assignment ... */
template_->op = locator_add_class ((MOBJ) class_, sm_ch_name ((MOBJ) class_));
}
flat->partition_parent_atts = template_->partition_parent_atts;
error = install_new_representation (template_->op, class_, flat);
num_indexes = allocate_disk_structures (template_->op, class_, newsubs, template_);
error = update_supers (template_->op, oldsupers, newsupers);
error = update_subclasses (newsubs);

The single line template_->op = locator_add_class (...) is when a fresh SM_CLASS becomes a workspace MOP — the client-side handle for the class object — bound to the temporary OID returned by locator_reserve_class_name. The permanent OID is assigned at flush time by the server.

install_new_representation is the structural mutation step: it fixes self-referential domains, runs build_storage_order to assign attribute IDs, and decides whether the change forces a new on-disk representation (a CUBRID-specific concept: an SM_CLASS may carry many historical representations so old heap rows can still be parsed). If it does, every cached instance of the class is flushed and decached.

// install_new_representation — schema_manager.c (condensed)
fixup_component_classes (classop, flat);
fixup_self_reference_domains (classop, flat);
check_inherited_attributes (classop, class_, flat);
needrep = build_storage_order (class_, flat);
for (a = flat->shared_attributes; a != NULL; a = a->header.next) assign_attribute_id (class_, a, 0);
for (a = flat->class_attributes; a != NULL; a = a->header.next) assign_attribute_id (class_, a, 1);
if (needrep && !classop->no_objects)
{
locator_flush_all_instances (classop, DECACHE);
locator_update_class (classop);
newrep = 1; WS_SET_NO_OBJECTS (classop);
}
error = transfer_disk_structures (classop, class_, flat);
invalidate_unused_triggers (classop, class_, flat);
sm_reset_descriptors (classop);
error = classobj_install_template (class_, flat, newrep);
locator_update_class (classop);

The two pieces worth noting here are transfer_disk_structures, which reconciles the old SM_CLASS_CONSTRAINT list with the new flat one and either preserves or deallocate_index-es each B-tree, and classobj_install_template, which does the actual swap: it drops the old SM_CLASS field-by-field and copies the flattened template into the live class object. The class MOP is now authoritative; the next DML compile that fetches it will see the new shape.

Back in do_create_entity, after dbt_finish_class returns, the heap is created lazily — heap allocation is deferred to locator_create_heap_if_needed so a vclass (view) can finish without ever paying a heap, and a regular class only pays once.

// locator_create_heap_if_needed — locator_cl.c (condensed)
class_obj = locator_fetch_class (class_mop, DB_FETCH_CLREAD_INSTWRITE);
hfid = sm_ch_heap (class_obj);
if (HFID_IS_NULL (hfid))
{
class_obj = locator_fetch_class (class_mop, DB_FETCH_WRITE);
oid = ws_oid (class_mop);
if (OID_ISTEMP (oid)) { locator_flush_class (class_mop); oid = ws_oid (class_mop); }
heap_create (hfid, oid, reuse_oid);
/* lob processing, dirty, flush */
}

If the OID is still temporary (the class has never been flushed to the server), locator_flush_class is invoked first; this is what reifies the temporary OID into a permanent one and triggers catcls_insert_catalog_classes server-side. A CREATE TABLE therefore typically performs the catalog write here, not at the end of the statement.

Finally, partitioning and same-statement CREATE INDEX clauses are dispatched. Inline CREATE TABLE … AS SELECT is desugared into an INSERT INTO target SELECT … at the bottom of do_create_entity and re-entered through do_statement — a small but elegant trick that lets the engine reuse all of DML.

ALTER TABLE — multi-clause and single-clause paths

Section titled “ALTER TABLE — multi-clause and single-clause paths”

do_alter walks the chain (ALTER may carry several clauses) and executes each under a single UNIQUE_SAVEPOINT_MULTIPLE_ALTER savepoint so an error in clause N rolls back clauses 1..N-1.

stateDiagram-v2
  [*] --> Savepoint: tran_system_savepoint("mULTIPLEaLTER")
  Savepoint --> Loop: for each crt_clause
  Loop --> SemCheck: pt_compile (re-check 2nd+ clause)
  SemCheck --> Code: switch(alter_code)
  Code --> Rename: PT_RENAME_ENTITY
  Code --> AddIdx: PT_ADD_INDEX_CLAUSE
  Code --> DropIdx: PT_DROP_INDEX_CLAUSE
  Code --> ChgAI: PT_CHANGE_AUTO_INCREMENT
  Code --> ChgAttr: PT_CHANGE_ATTR
  Code --> Owner: PT_CHANGE_OWNER
  Code --> Coll: PT_CHANGE_COLLATION
  Code --> TblComm: PT_CHANGE_TABLE_COMMENT
  Code --> ColComm: PT_CHANGE_COLUMN_COMMENT
  Code --> WithTmpl: default -> do_alter_one_clause_with_template
  Rename --> Loop
  AddIdx --> Loop
  DropIdx --> Loop
  ChgAI --> Loop
  ChgAttr --> Loop
  Owner --> Loop
  Coll --> Loop
  TblComm --> Loop
  ColComm --> Loop
  WithTmpl --> Loop
  Loop --> [*]: NO_ERROR
  Loop --> Rollback: error -> tran_abort_upto_system_savepoint
  Rollback --> [*]
// do_alter — execute_schema.c (condensed)
error_code = tran_system_savepoint (UNIQUE_SAVEPOINT_MULTIPLE_ALTER);
for (crt_clause = alter; crt_clause != NULL; crt_clause = crt_clause->next)
{
if (do_semantic_checks) { /* re-pt_compile 2nd+ clauses against the freshly mutated class */ }
switch (crt_clause->info.alter.code)
{
case PT_RENAME_ENTITY: error_code = do_alter_clause_rename_entity (parser, crt_clause); break;
case PT_ADD_INDEX_CLAUSE: error_code = do_alter_clause_add_index (parser, crt_clause); break;
case PT_DROP_INDEX_CLAUSE: error_code = do_alter_clause_drop_index (parser, crt_clause); break;
case PT_CHANGE_AUTO_INCREMENT: error_code = do_alter_change_auto_increment (parser, crt_clause); break;
case PT_CHANGE_ATTR: error_code = do_alter_clause_change_attribute (parser, crt_clause); break;
case PT_CHANGE_OWNER: error_code = do_alter_change_owner (parser, crt_clause); break;
case PT_CHANGE_COLLATION: error_code = do_alter_change_default_cs_coll (parser, crt_clause); break;
case PT_CHANGE_TABLE_COMMENT: error_code = do_alter_change_tbl_comment (parser, crt_clause); break;
case PT_CHANGE_COLUMN_COMMENT: error_code = do_alter_change_col_comment (parser, crt_clause); break;
default: error_code = do_alter_one_clause_with_template (parser, crt_clause); /* template-mutating clauses */
}
if (error_code != NO_ERROR) goto error_exit;
do_semantic_checks = true;
}

The catch-all do_alter_one_clause_with_template handles the clauses that mutate the SM_TEMPLATE rather than poke at the live SM_CLASS: PT_ADD_QUERY, PT_DROP_QUERY, PT_MODIFY_QUERY, PT_ADD_ATTR_MTHD, PT_DROP_ATTR_MTHD, PT_MODIFY_ATTR_MTHD, PT_RESET_QUERY, plus all partition-altering codes (PT_APPLY_PARTITION, PT_REMOVE_PARTITION, PT_ADD_PARTITION, PT_ADD_HASHPARTITION, PT_REORG_PARTITION, PT_COALESCE_PARTITION, PT_ANALYZE_PARTITION, PT_PROMOTE_PARTITION).

The skeleton of every clause that touches the template is the same: edit, mutate, finish, edit-again to add constraints if needed, finish-again. The PT_ADD_ATTR_MTHD case is the most illustrative because it shows the typical two-finish pattern that arises whenever you add a column and a unique constraint in one statement:

// do_alter_one_clause_with_template / PT_ADD_ATTR_MTHD — execute_schema.c (condensed)
error = tran_system_savepoint (UNIQUE_SAVEPOINT_ADD_ATTR_MTHD);
error = do_add_attributes (parser, ctemplate, attr_def_list, constraint_list, NULL);
vclass = dbt_finish_class (ctemplate); /* first finish: column added */
ctemplate = dbt_edit_class (vclass); /* re-edit so constraints can reference new column */
error = do_add_constraints (ctemplate, constraint_list);
error = do_check_fk_constraints (ctemplate, constraint_list);
if (mthd_def_list != NULL)
error = do_add_methods (parser, ctemplate, mthd_def_list);
/* second dbt_finish_class happens in the common tail */

The reason for the split is that constraints can reference the new column, but the column does not exist on the server until the template is finished and the catalog row is written. So the column is committed, the class is re-edited with a fresh template that includes the new column, and the constraint is attached.

The PT_CHANGE_ATTR path (do_alter_clause_change_attribute) is even more elaborate because changing an attribute may require an instance-level update (do_run_update_query_for_class, do_run_upgrade_instances_domain) when the new domain is not a trivial superset of the old.

DROP TABLE — sm_delete_class_mop and the cascade

Section titled “DROP TABLE — sm_delete_class_mop and the cascade”

do_drop is shorter: per entity in the drop list, partition sanity check, then drop_class_name → db_drop_class_ex → sm_delete_class_mop.

// do_drop / drop_class_name — execute_schema.c
error = tran_system_savepoint (UNIQUE_SAVEPOINT_DROP_ENTITY);
for (entity_spec = entity_spec_list; entity_spec; entity_spec = entity_spec->next)
for (entity = entity_spec->info.spec.flat_entity_list; entity; entity = entity->next)
{
error = drop_class_name (entity->info.name.original,
statement->info.drop.is_cascade_constraints);
if (error != NO_ERROR) goto error_exit;
}
/* drop_class_name */
class_mop = db_find_class (name);
if (class_mop) return db_drop_class_ex (class_mop, is_cascade_constraints);

sm_delete_class_mop is the inverse of the CREATE pipeline. A partitioned class branches out to do_drop_partitioned_class which recurs into each partition; the steady-state path runs tran_system_savepoint (SM_DROP_CLASS_MOP_SAVEPOINT_NAME), sm_bump_local_schema_version, au_fetch_class (..., AU_FETCH_WRITE, AU_ALTER), lockhint_subclasses, an FK referrers check (cascade-drop or ER_FK_CANT_DROP_PK_REFERRED), removal of any auto-increment SERIAL objects, then classobj_make_template (NULL, op, class_) to produce a null template that represents the disappearance of the class. Sub/super-class locks are taken via lock_supers_drop / lock_subclasses / flatten_subclasses; every cached instance is ws_mark_instances_deleted-ed and flushed with DECACHE before the heap is destroyed (otherwise the workspace would dereference freed memory); update_supers_drop and update_subclasses reshape the schema graph; transfer_disk_structures (op, class_, NULL) deallocates every B-tree (the flat = NULL is what tells it to drop, not migrate); remove_class_triggers performs physical trigger deletion. The catalog row is deleted server-side at flush time by catcls_delete_catalog_classes under the same MVCC transaction.

A successful DROP TABLE therefore looks like a series of client-side mutations that converge at flush time on a catalog delete plus a heap destroy.

do_create_index (PT_CREATE_INDEX) and do_drop_index (PT_DROP_INDEX) both delegate to a shared helper:

// create_or_drop_index_helper — execute_schema.c (signature)
static int
create_or_drop_index_helper (PARSER_CONTEXT *parser,
const char *const constraint_name,
const bool is_reverse,
const bool is_unique,
const PT_INDEX_INFO *idx_info,
DB_OBJECT *const obj,
DO_INDEX do_index);

The helper rejects index DDL on a partition; computes DB_CONSTRAINT_TYPE from (is_reverse, is_unique); materialises filter predicates (pt_to_pred_with_context, xts_map_filter_pred_to_stream) and function-index expressions (pt_node_to_function_index) into the streamed SM_PREDICATE_INFO and SM_FUNCTION_INFO; derives the canonical name through sm_produce_constraint_name; and finally calls sm_add_constraint (which allocates the B-tree via allocate_disk_structures) or sm_drop_constraint (which frees it).

PT_ALTER_INDEX splits through do_alter_index into do_alter_index_rebuild (drop+create), do_alter_index_rename, do_alter_index_comment, and the VISIBLE/INVISIBLE toggle do_alter_index_status.

Partition DDL — pre/post split and child-class fanout

Section titled “Partition DDL — pre/post split and child-class fanout”

CUBRID’s partition DDL is implemented as a root-template mutation on the parent class plus a fanout of do_create_local calls for each child partition. The driver do_alter_partitioning_pre runs before the parent template is finished, and do_alter_partitioning_post runs after, so that the partition columns and constraints can be in place before the children are materialised.

// do_alter_partitioning_pre — execute_schema.c
switch (alter_op)
{
case PT_APPLY_PARTITION: /* set SM_ATTFLAG_PARTITION_KEY on parent template */
case PT_ADD_PARTITION:
case PT_ADD_HASHPARTITION: error = do_create_partition (parser, alter, pinfo); break;
case PT_REMOVE_PARTITION: error = do_remove_partition_pre (parser, alter, pinfo); break;
case PT_COALESCE_PARTITION: error = do_coalesce_partition_pre (parser, alter, pinfo); break;
case PT_REORG_PARTITION: error = do_reorganize_partition_pre (parser, alter, pinfo); break;
/* ... */
}

do_create_partition itself synthesises a PT_CREATE_ENTITY parse tree per child partition, naming them with the PARTITIONED_SUB_CLASS_TAG (__p__) suffix, and recurses through do_create_local for each. The child template inherits the parent (supclass_list) and carries a partition field populated by pt_node_to_partition_info. Hash partitions create hashsize children; range and list partitions create one child per PT_PARTS element.

Hash partitions appear in the schema as base_class__p__p0, base_class__p__p1, …, all sub-classes of the base. Each child has its own heap and B-trees; the parent’s heap is empty. Routing of DML to the correct child happens later, in pt_resolve_partition_spec (parser) and btree_find_root_with_key (executor).

Cross-reference: see the dedicated cubrid-partition analysis for the routing-side details.

do_create_trigger, do_drop_trigger, do_alter_trigger, do_rename_trigger and do_remove_trigger are all in execute_statement.c. They are thin wrappers around tr_create_trigger, tr_drop_trigger, etc., from src/object/trigger_manager.c.

// do_create_trigger — execute_statement.c
class_ = db_find_class (PT_TR_TARGET_CLASS (target));
/* ... extract event/condition/action, time, priority, comment ... */
trigger = tr_create_trigger (name, status, priority, event, class_, attribute,
cond_time, cond_source, action_time, action_type,
action_source, comment);
if (smclass != NULL && smclass->users != NULL && TM_TRAN_ISOLATION () < TRAN_REP_READ)
error = locator_all_flush ();

The flush of the entire workspace is a CUBRID quirk: trigger metadata travels through the same heap row as the class (SM_CLASS::triggers), so installing a trigger on a class whose sub-classes are temporary objects requires a full flush to make the trigger visible across the hierarchy. The repeatable-read guard short-circuits the flush when isolation already provides consistency.

Cross-reference: the cubrid-trigger analysis in this folder covers tr_* internals.

Locator integration — DDL routes through the same MOP path as DML

Section titled “Locator integration — DDL routes through the same MOP path as DML”

DDL never bypasses the locator. Every catalog mutation reaches the server through one of:

  • locator_reserve_class_name — server-side hash insert into the classname-to-OID table; returns a temporary OID.
  • locator_add_class — workspace cache install with the temporary OID; sets SCH_M_LOCK on the class MOP.
  • locator_flush_class — pushes the class instance to the server, where xlocator_force translates it into catcls_insert_catalog_classes / catcls_update_catalog_classes.
  • locator_remove_class — decache instances, destroy the heap, delete the classname.
  • locator_create_heap_if_needed — ensure HFID is set; calls heap_create on the server.
  • locator_all_flush — drain every dirty MOP before replication records the DDL.

The crucial property is that none of these are special DDL opcodes. A class is just an instance of _db_class; flushing it to the server is the same operation as flushing any other dirty MOP. The catalog write happens because the destination heap of the class MOP is the catalog heap.

// locator_add_class — locator_cl.c (condensed)
class_mop = ws_find_class (classname); /* deleted-and-resurrected case is also handled */
locator_get_reserved_class_name_oid (classname, &class_temp_oid);
/* Convert root-class lock to IX_LOCK so the new class can be IX-locked under it */
lock = ws_get_lock (sm_Root_class_mop);
if (lock != NULL_LOCK)
ws_set_lock (sm_Root_class_mop, lock_conv (lock, IX_LOCK));
else
locator_lock (sm_Root_class_mop, LC_CLASS, IX_LOCK, LC_FETCH_CURRENT_VERSION);
class_mop = ws_cache_with_oid (class_obj, &class_temp_oid, sm_Root_class_mop);
if (class_mop != NULL) { ws_dirty (class_mop); ws_set_lock (class_mop, SCH_M_LOCK); }
return class_mop;

Two design points are worth highlighting:

  • The class MOP is installed under the root class MOP (sm_Root_class_mop), which holds an IX_LOCK for the duration of the DDL — this is how concurrent DDLs serialise against each other through the locator without needing a global schema lock.
  • The new class is cached under its temporary OID until flushed, at which point the server assigns a permanent OID and ws_update_oid rewrites the workspace handle.

Cross-reference: the cubrid-locator analysis covers the classname hash table, xlocator_assign_oid_batch, and the temporary-to-permanent-OID rewrite in detail.

Catalog write — catcls_insert_catalog_classes

Section titled “Catalog write — catcls_insert_catalog_classes”

The server side of a CREATE TABLE flush lands in catcls_insert_catalog_classes (src/storage/catalog_class.c).

// catcls_insert_catalog_classes — catalog_class.c (condensed)
value_p = catcls_get_or_value_from_class_record (thread_p, record_p);
class_oid_p = &ct_Class.cc_classoid; /* OID of _db_class */
cls_info_p = catalog_get_class_info (thread_p, class_oid_p, NULL);
hfid_p = &cls_info_p->ci_hfid;
heap_scancache_start_modify (thread_p, &scan, hfid_p, class_oid_p, SINGLE_ROW_UPDATE, NULL);
catcls_insert_instance (thread_p, value_p, &oid, &root_oid, class_oid_p, hfid_p, &scan);
heap_scancache_end_modify (thread_p, &scan);

The route is: deserialise the class record into an OR_VALUE tree, find the catalog _db_class by its well-known OID (ct_Class.cc_classoid), open a heap scancache in modify mode, and call catcls_insert_instance which performs the actual heap insert plus B-tree updates for every catalog index. The write is a single MVCC transaction — the same MVCC machinery that protects user tables protects the catalog.

catcls_update_catalog_classes is the same, but for ALTER. Note the convenient fall-through: if the class name is not found, the update is upgraded to an insert.

// catcls_update_catalog_classes — catalog_class.c (condensed)
catcls_find_oid_by_class_name (thread_p, name_p, &oid);
if (OID_ISNULL (&oid)) /* upgrade UPDATE to INSERT */
return catcls_insert_catalog_classes (thread_p, record_p);
value_p = catcls_get_or_value_from_class_record (thread_p, record_p);
/* open scancache; heap_update_logical inside catcls_update_instance */

catcls_delete_catalog_classes is symmetric: find the OID by name, open the heap scancache for SINGLE_ROW_DELETE, call catcls_delete_instance, then catcls_remove_entry to evict from the in-memory class cache. In MVCC, the row is not physically removed — the comment in the source is explicit: /* in MVCC, do not physically remove the row */ — instead it is logically deleted and reclaimed by VACUUM.

Cache invalidation — schema version, class_object, XASL

Section titled “Cache invalidation — schema version, class_object, XASL”

Three caches need to know when a class changes:

flowchart LR
  Bump[sm_bump_local_schema_version<br/>schema_manager.c]
  Bump --> Inst[install_new_representation<br/>classobj_install_template]
  Inst --> WS[Workspace MOP cache<br/>SM_CLASS swap]
  Bump --> XASL[XASL cache miss on next compile<br/>see cubrid-xasl-cache]
  Inst --> Repr[representation chain<br/>old reps preserved for legacy heap rows]
  WS --> Reset[sm_reset_descriptors<br/>per-MOP attribute cache]
  • Workspace class-object cache. The MOP itself owns the SM_CLASS pointer. classobj_install_template walks every field and copies from the flattened template into the live class. After this point, every au_fetch_class (mop, ..., AU_FETCH_READ, ...) returns the new shape. There is no per-statement cache invalidation message — the swap is the invalidation, and any executor that holds a stale pointer would have lost its lock at the same time.
  • Attribute and method descriptor caches. sm_reset_descriptors clears the per-class attribute descriptor cache (used by db_get_attribute_descriptor and the fast-path attribute getters). Old descriptor handles will fail on next use rather than dereference freed memory.
  • XASL cache. CUBRID’s plan cache keys each entry on a (SHA1(SQL), schema_version) pair. sm_bump_local_schema_version increments the local counter and the next plan compile that references the changed class sees a miss. The XASL cache is not purged eagerly; it ages out naturally.
  • Trigger cache. invalidate_unused_triggers is called from install_new_representation to mark TR_TRIGGER entries associated with attributes that no longer exist as deleted. The authoritative trigger list is rebuilt from _db_trigger on next access.
  • Representation chain. build_storage_order decides whether the change forces a new representation of the class. The old representations are preserved in the catalog so that historical heap rows whose schema-version stamp predates the change can still be read; the executor consults the right representation via or_class_rep_id. This is CUBRID’s analogue of Postgres’ attnum plus drop-column-marker scheme.

Cross-references: cubrid-class-object covers the SM_CLASS / SM_TEMPLATE data structures; cubrid-xasl-cache covers the plan cache invalidation policy; cubrid-catalog-manager covers the disk catalog layout that catcls_* writes into.

Transactional semantics — savepoints everywhere

Section titled “Transactional semantics — savepoints everywhere”

CUBRID’s DDL is fully transactional, and the discipline that makes it so is system savepoints. Every DDL handler establishes a unique savepoint at entry and rolls back to it on any errpath:

// savepoint identifiers — execute_schema.c (selected; full list lives at the top of the file)
#define UNIQUE_SAVEPOINT_CREATE_ENTITY "cREATEeNTITY"
#define UNIQUE_SAVEPOINT_DROP_ENTITY "dROPeNTITY"
#define UNIQUE_SAVEPOINT_MULTIPLE_ALTER "mULTIPLEaLTER"
#define UNIQUE_SAVEPOINT_ADD_ATTR_MTHD "aDDaTTRmTHD"
#define UNIQUE_SAVEPOINT_CHANGE_ATTR "cHANGEaTTR"
#define UNIQUE_SAVEPOINT_RENAME "rENAME"
#define UNIQUE_SAVEPOINT_TRUNCATE "tRUnCATE"
#define UNIQUE_SAVEPOINT_REPLACE_VIEW "rEPlACE"
#define UNIQUE_SAVEPOINT_ALTER_INDEX "aLTERiNDEX"
/* plus the user/grant/revoke variants and partition savepoints */

The strange casing ("cREATEeNTITY") is intentional: it makes collisions with user-named savepoints astronomically unlikely. The savepoint is taken with tran_system_savepoint, which writes a CLR (compensation log record) into the WAL — on rollback, recovery walks the WAL backwards from the abort record to the savepoint LSN and undoes everything in between.

A CREATE TABLE … AS SELECT that fails halfway through the INSERT is rolled back to cREATEeNTITY; the catalog row, the heap, every B-tree, and any partition children are all undone. A ROLLBACK after a successful CREATE TABLE undoes the create just as cleanly. This is the contract that lets CUBRID claim atomic DDL in the MySQL 8.0 sense.

The replication tail records the schema change as a LOG_REPLICATION_DDL entry by calling do_replicate_statement; the supplemental log captures the affected class OIDs and the parsed statement text for CDC. Both are deferred until after the local DDL has succeeded and locator_all_flush () has reified every dirty MOP.

Top-level dispatch — execute_statement.c

Section titled “Top-level dispatch — execute_statement.c”
  • do_statement (parser, statement) — single-statement entry point; the giant switch (statement->node_type) is the DDL router.
  • do_execute_statement — re-prepared variant; walks the same switch but dispatches via the prepared XASL when applicable.
  • do_check_internal_statements (parser, stmt, do_func) — currently a pass-through to do_func; the legacy text-domain internal-statements hook is #if 0-guarded.
  • do_reserve_classinfo, do_reserve_oidinfo — record the affected class OID before DROP/DROP_SERIAL so the supplemental log can name it after the catalog row is gone.
  • do_supplemental_statement — emits the supplemental log record for CDC.
  • do_replicate_statement — emits the schema-replication record for HA.
  • do_create_serial, do_alter_serial, do_drop_serial — serial DDL; calls into _db_serial.
  • do_create_trigger, do_drop_trigger, do_alter_trigger, do_rename_trigger, do_remove_trigger, do_set_trigger, do_get_trigger, do_execute_trigger — trigger DDL; delegate to tr_* in trigger_manager.c.

CREATE TABLE / DROP / RENAME — execute_schema.c

Section titled “CREATE TABLE / DROP / RENAME — execute_schema.c”
  • do_create_entityCREATE TABLE/CREATE VIEW driver.
  • do_create_local — column/constraint/method/super-class population of the template.
  • execute_create_select_query — the CREATE TABLE … AS SELECT desugaring: synthesises a PT_INSERT and re-enters do_statement.
  • create_select_to_insert_into — synthesises the PT_INSERT parse tree.
  • do_dropDROP TABLE driver.
  • drop_class_name — name → MOP → db_drop_class_ex.
  • truncate_class_name — name → MOP → db_truncate_class.
  • do_truncateTRUNCATE driver.
  • do_renameRENAME driver; uses acquire_locks_for_multiple_rename to atomically lock and reserve names for chained renames.
  • do_rename_internal — performs the actual sm_rename_class.
  • update_locksets_for_multiple_rename, acquire_locks_for_multiple_rename — the lock-and-reserve dance for RENAME a TO b, b TO c, c TO a.
  • do_recreate_renamed_class_indexes, do_copy_indexes — index recreation after RENAME and CREATE LIKE.
  • do_alter, do_alter_one_clause_with_template — multi-clause driver and the single-clause template path used by ADD/DROP column, query ops, and partition ops.
  • do_alter_clause_rename_entity, do_alter_clause_add_index, do_alter_clause_drop_index, do_alter_change_auto_increment, do_alter_clause_change_attribute, do_alter_change_owner, do_alter_change_default_cs_coll, do_alter_change_tbl_comment, do_alter_change_col_comment — one per PT_ALTER_CODE that bypasses the catch-all template path.
  • do_change_att_schema_only — schema-only column redefinition.
  • do_run_update_query_for_new_notnull_fields, do_run_update_query_for_new_default_expression_fields, do_update_new_notnull_cols_without_default, do_update_new_cols_with_default_expression, do_run_upgrade_instances_domain — instance-level fixups for ALTER ADD COLUMN with NOT NULL / DEFAULT / domain widening.
  • do_drop_att_constraints, do_recreate_att_constraints, do_save_all_indexes, do_drop_saved_indexes, do_recreate_saved_indexes — constraint and index preservation around CHANGE COLUMN.
  • do_alter_index_status — ALTER INDEX … VISIBLE/INVISIBLE.
  • do_create_index — PT_CREATE_INDEX driver.
  • do_drop_index — PT_DROP_INDEX driver.
  • do_alter_index — dispatch to rebuild/rename/comment/status.
  • do_alter_index_rebuild, do_alter_index_rename, do_alter_index_comment.
  • create_or_drop_index_helper — the shared body.
  • get_reverse_unique_index_type, get_index_type_qualifiers — PT-flag → DB_CONSTRAINT_TYPE adapters.
  • do_create_partition synthesises a PT_CREATE_ENTITY per child and recurses into do_create_local.
  • do_alter_partitioning_pre / do_alter_partitioning_post — two-phase ALTER … PARTITION dispatcher; per-op pairs are do_remove_partition_pre/_post, do_coalesce_partition_pre/_post, do_reorganize_partition_pre/_post, plus do_promote_partition_list, do_promote_partition_by_name, do_promote_partition, do_analyze_partition.
  • Partition-aware helpers: do_check_partitioned_class, do_get_partition_parent, do_is_partitioned_subclass, do_drop_partitioned_class, do_rename_partition, do_get_partition_size, do_get_partition_keycol, do_drop_partition_list, do_create_partition_constraints, do_create_partition_constraint, do_redistribute_partitions_data.

User and authorisation DDL — execute_schema.c

Section titled “User and authorisation DDL — execute_schema.c”
  • do_grant, do_revoke — privilege management.
  • do_create_user, do_drop_user, do_alter_user — user DDL (member sets, password, comment).

Trigger and serial DDL — execute_statement.c

Section titled “Trigger and serial DDL — execute_statement.c”
  • do_create_trigger — wraps tr_create_trigger.
  • do_drop_trigger — wraps tr_drop_trigger per spec list.
  • do_alter_trigger — wraps tr_set_priority / tr_set_status.
  • do_rename_trigger — wraps tr_rename_trigger.
  • do_create_serial, do_alter_serial, do_drop_serial — CRUD on the _db_serial system class, plus do_get_serial_obj_id lookup helper.

Template machinery — db_temp.c, schema_template.c, schema_manager.c

Section titled “Template machinery — db_temp.c, schema_template.c, schema_manager.c”
  • Public dbt_* API: dbt_create_class, dbt_create_vclass, dbt_edit_class, dbt_copy_class, dbt_finish_class, dbt_abort_class, plus the per-component dbt_add_attribute / dbt_add_constraint / dbt_drop_* / dbt_*_query_spec mutators.
  • Internal factory: smt_def_class, smt_def_typed_class, smt_edit_class_mop, smt_copy_class_mop, smt_copy_class, smt_quit, def_class_internal.
  • SM_TEMPLATE lifecycle (class_object.c): classobj_make_template, classobj_make_template_like, classobj_install_template, classobj_free_template.
  • Inheritance merge and physical layout: flatten_template, flatten_subclasses, build_storage_order, assign_attribute_id, assign_method_id.
  • Schema-graph primitives: update_class, update_supers, update_supers_drop, update_subclasses, lock_supers, lock_supers_drop, lock_subclasses, lockhint_subclasses.
  • Physical mutation: install_new_representation, transfer_disk_structures, allocate_disk_structures, deallocate_index, rem_class_from_index.
  • Public class-update endpoints: sm_finish_class, sm_update_class, sm_update_class_auto, sm_update_class_with_auth. Drop machinery: sm_delete_class_mop, remove_class_triggers, sm_drop_cascade_foreign_key, do_drop_partitioned_class.
  • DDL fence counter: sm_bump_local_schema_version, sm_local_schema_version.

Locator and catalog — locator_cl.c, locator_sr.c, catalog_class.c

Section titled “Locator and catalog — locator_cl.c, locator_sr.c, catalog_class.c”
  • Classname hash: locator_reserve_class_name, locator_delete_class_name, locator_get_reserved_class_name_oid, locator_force_drop_class_name_entry.
  • Client-side class MOP lifecycle: locator_add_class, locator_remove_class, locator_update_class, locator_flush_class, locator_create_heap_if_needed, locator_prepare_rename_class. Workspace drains: locator_all_flush, locator_flush_all_instances.
  • Server-side: xlocator_force dispatches the flush and routes catalog-class instances to catcls_*; xlocator_remove_class_from_index and xlocator_assign_oid_batch handle multi-class index removal and temporary→permanent OID rewriting.
  • Catalog write API: catcls_insert_catalog_classes, catcls_update_catalog_classes, catcls_delete_catalog_classes, catcls_update_class_stats, catcls_remove_entry, catcls_compile_catalog_classes.
  • OR_VALUE ↔ heap-row marshalling: catcls_get_or_value_from_class, catcls_get_or_value_from_class_record, catcls_put_or_value_into_record, catcls_insert_instance, catcls_delete_instance, catcls_update_instance. Name resolution at ALTER/DROP: catcls_find_oid_by_class_name.
SymbolFileLine
do_statement (DDL switch)src/query/execute_statement.c3244
do_check_internal_statementssrc/query/execute_statement.c4270
do_create_triggersrc/query/execute_statement.c6661
do_drop_triggersrc/query/execute_statement.c6801
do_alter_triggersrc/query/execute_statement.c6860
do_create_serialsrc/query/execute_statement.c1406
do_replicate_statementsrc/query/execute_statement.c16136
do_supplemental_statementsrc/query/execute_statement.c15515
do_altersrc/query/execute_schema.c1770
do_alter_one_clause_with_templatesrc/query/execute_schema.c409
do_alter_clause_rename_entitysrc/query/execute_schema.c1558
do_alter_clause_change_attributesrc/query/execute_schema.c9942
drop_class_namesrc/query/execute_schema.c2582
do_dropsrc/query/execute_schema.c2607
acquire_locks_for_multiple_renamesrc/query/execute_schema.c2737
do_renamesrc/query/execute_schema.c2890
create_or_drop_index_helpersrc/query/execute_schema.c3027
do_create_indexsrc/query/execute_schema.c3330
do_drop_indexsrc/query/execute_schema.c3370
do_alter_indexsrc/query/execute_schema.c3995
do_create_partitionsrc/query/execute_schema.c4037
do_alter_partitioning_presrc/query/execute_schema.c6017
do_alter_partitioning_postsrc/query/execute_schema.c6165
do_add_attributessrc/query/execute_schema.c7735
do_add_constraintssrc/query/execute_schema.c7950
do_check_fk_constraintssrc/query/execute_schema.c8303
do_create_localsrc/query/execute_schema.c8766
execute_create_select_querysrc/query/execute_schema.c8946
do_create_entitysrc/query/execute_schema.c9025
truncate_class_namesrc/query/execute_schema.c9855
do_truncatesrc/query/execute_schema.c9880
do_grantsrc/query/execute_schema.c1888
do_create_usersrc/query/execute_schema.c2114
dbt_create_classsrc/compat/db_temp.c76
dbt_edit_classsrc/compat/db_temp.c132
dbt_finish_classsrc/compat/db_temp.c225
smt_def_classsrc/object/schema_template.c735
smt_edit_class_mopsrc/object/schema_template.c753
update_classsrc/object/schema_manager.c13148
sm_finish_classsrc/object/schema_manager.c13431
sm_delete_class_mopsrc/object/schema_manager.c13584
install_new_representationsrc/object/schema_manager.c12366
transfer_disk_structuressrc/object/schema_manager.c11924
sm_bump_local_schema_versionsrc/object/schema_manager.c6717
locator_add_classsrc/transaction/locator_cl.c5378
locator_create_heap_if_neededsrc/transaction/locator_cl.c5472
locator_remove_classsrc/transaction/locator_cl.c5652
locator_flush_classsrc/transaction/locator_cl.c4890
xlocator_forcesrc/transaction/locator_sr.c7129
catcls_insert_catalog_classessrc/storage/catalog_class.c4310
catcls_update_catalog_classessrc/storage/catalog_class.c4573
catcls_delete_catalog_classessrc/storage/catalog_class.c4379
catcls_get_or_value_from_class_recordsrc/storage/catalog_class.c3552
  • cubrid-class-object describes SM_CLASS and SM_TEMPLATE as data structures; this doc describes the workflow by which a template becomes a class. classobj_install_template is the structural contract; the representation chain (new_, old_, representations) is populated by build_storage_order here and consumed by or_class_rep_id there.
  • cubrid-catalog-manager covers the on-disk layout of _db_class / _db_attribute / _db_index. This doc covers the call paths into catcls_insert_catalog_classes / catcls_update_catalog_classes / catcls_delete_catalog_classes; the marshalling is one-to-one with OR_VALUE trees described there.
  • cubrid-locator owns the classname hash table, lock conversion under sm_Root_class_mop, and the temporary→permanent OID rewrite at flush time; this doc names the call sites (locator_add_class, locator_remove_class, locator_create_heap_if_needed, locator_flush_class, xlocator_force).
  • cubrid-trigger owns tr_create_trigger / tr_drop_trigger and the _db_trigger catalog row. The cross-cutting concerns are invalidate_unused_triggers inside install_new_representation and remove_class_triggers inside sm_delete_class_mop.
  • cubrid-btree and cubrid-partition: index DDL delegates to sm_add_constraint / sm_drop_constraint and ultimately to btree_create_index / btree_delete_index; partition DDL fans into per-child class creation, with routing in the partition doc.
  • cubrid-xasl-cache covers how the bumped sm_local_schema_version interacts with the plan-cache key and prepared-statement state.
  • Drift watch. Line numbers in the position-hint table are stable as of late-2025 worktree state; edits to execute_statement.c for new PT_NODE_TYPE codes tend to push the DDL switch downward. Anchor on symbol names — they are stable across releases since 11.0.
  • Online schema change. ALTER TABLE takes SCH_M_LOCK for its full duration; a row-log / ghost-copy à la InnoDB online DDL would let concurrent DML proceed during long ALTERs (ADD COLUMN with non-trivial DEFAULT, domain widening). The natural integration points are do_run_update_query_for_new_notnull_fields and do_run_upgrade_instances_domain.
  • Concurrent DDL on disjoint classes. Two CREATE TABLE statements on different classes serialise through the IX_LOCK on sm_Root_class_mop plus the classname-hash insert. Whether a finer-grained schema lock would help in practice depends on contention measurements not in the source.
  • Per-partition online add column. do_add_attribute runs once on the parent template and propagates through flatten_subclasses. The representation chain carries a per-class representation ID, so per-partition parallel migration is conceivable but not implemented.
  • Atomicity of trigger DDL across HA. locator_all_flush () inside do_create_trigger covers local consistency, but the schema-replication record is written by the outer do_replicate_statement. Behaviour under slave-side crash recovery for hierarchical triggers is not exhaustively documented.
  • do_check_internal_statements reactivation. The wrapper hosts a substantial #if 0 block for TEXT-domain auxiliary statements. If TEXT returns, the savepoint discipline must be reconciled with the per-DDL savepoints already taken by the dispatched handlers.
  • MVCC delete of catalog rows. catcls_delete_catalog_classes does not physically remove the row; whether VACUUM on the catalog heap is treated specially (e.g. forced after every DROP) is not obvious from execute_schema.c alone.

Synthesised from the CUBRID source tree at the revision the position-hint table records.

Code paths consumed:

  • src/query/execute_schema.cdo_create_entity, do_alter, do_drop, do_truncate, do_rename, do_create_index / do_drop_index / do_alter_index*, do_create_partition, do_alter_partitioning_pre/_post, do_grant / do_revoke, do_create_user / do_drop_user / do_alter_user, plus the do_add_* template-mutation helpers.
  • src/query/execute_statement.cdo_statement switch, replication and supplemental-log tail, trigger / serial / stored-procedure DDL handlers.
  • src/object/schema_manager.cupdate_class, sm_finish_class, sm_delete_class_mop, install_new_representation, transfer_disk_structures, sm_bump_local_schema_version.
  • src/object/schema_template.csmt_def_class, smt_edit_class_mop, smt_copy_class_mop, smt_quit.
  • src/object/class_object.cclassobj_make_template, classobj_install_template, classobj_make_class, classobj_free_template.
  • src/object/object_template.cobt_quit, obt_assign, obt_apply_assignments, obt_update.
  • src/compat/db_temp.c — public dbt_* template API.
  • src/transaction/locator_cl.clocator_reserve_class_name, locator_add_class, locator_create_heap_if_needed, locator_remove_class, locator_flush_class, locator_update_class, locator_all_flush.
  • src/transaction/locator_sr.cxlocator_force, xlocator_remove_class_from_index, xlocator_assign_oid_batch, locator_force_drop_class_name_entry.
  • src/storage/catalog_class.ccatcls_insert_catalog_classes, catcls_update_catalog_classes, catcls_delete_catalog_classes, catcls_get_or_value_from_class_record, catcls_compile_catalog_classes.
  • src/parser/csql_grammar.y — DDL grammar that produces PT_CREATE_ENTITY, PT_ALTER, PT_DROP, PT_CREATE_INDEX, PT_ALTER_INDEX, PT_RENAME, PT_TRUNCATE, PT_CREATE_TRIGGER, PT_CREATE_SERIAL, PT_CREATE_USER.

Theoretical references:

  • Silberschatz et al., Database System Concepts, 7th ed., §5.2 (DDL syntax), §15.5 (System Catalog), §17 (Recovery and the atomicity of DDL).
  • Petrov, Database Internals, Ch. 4 (Schema management as a special case of B-tree updates).
  • Postgres source: src/backend/commands/tablecmds.c (DefineRelation, ATExecAddColumn), src/backend/utils/cache/relcache.c (relcache invalidation), src/backend/utils/cache/inval.c (CommandCounterIncrement).
  • MySQL 8.0 Atomic DDL design notes (mysql.tables, dd::cache, the DDL log).
  • Oracle Concepts Guide (Library Cache Locks and Pins; DDL implicit commit).