Guide for using Alembic database migrations with the CUBRID dialect.
- Installation
- Configuration
- Running Migrations
- CUBRID-Specific Behavior
- Limitations & Workarounds
- Examples
- Troubleshooting
Install sqlalchemy-cubrid with the alembic extra:
pip install sqlalchemy-cubrid[alembic]This pulls in Alembic ≥ 1.7 as a dependency. The CUBRID Alembic implementation
(CubridImpl) is registered automatically via the alembic.ddl entry point —
no manual configuration is needed.
Note: If you install Alembic separately (
pip install alembic), it will still auto-discover the CUBRID implementation as long assqlalchemy-cubridis installed in the same environment.
alembic init alembicThis creates an alembic/ directory and an alembic.ini configuration file.
Edit alembic.ini:
[alembic]
sqlalchemy.url = cubrid://dba:password@localhost:33000/demodbOr set it dynamically in alembic/env.py:
from sqlalchemy import create_engine
def run_migrations_online():
connectable = create_engine("cubrid://dba:password@localhost:33000/demodb")
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()The standard Alembic env.py works without modification. The CubridImpl
class is auto-discovered when the connection URL uses the cubrid:// scheme.
A minimal env.py for online migrations:
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# Import your models' metadata
from myapp.models import Base
config = context.config
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()
run_migrations_online()# Auto-generate from model changes
alembic revision --autogenerate -m "add users table"
# Create an empty migration
alembic revision -m "custom migration"# Upgrade to the latest version
alembic upgrade head
# Upgrade to a specific revision
alembic upgrade abc123
# Downgrade one step
alembic downgrade -1
# Show current revision
alembic current
# Show migration history
alembic history --verboseCUBRID implicitly commits every DDL statement. The CubridImpl sets
transactional_ddl = False, which tells Alembic:
- No transaction wrapping around DDL statements
- Each
CREATE TABLE,ALTER TABLE,DROP TABLEcommits immediately - A failed migration may leave the database in a partially-migrated state
Implication: If a migration with multiple DDL operations fails halfway through, you cannot simply roll back — the earlier operations have already been committed. Write migrations with small, atomic steps.
The dialect registers CubridImpl via the alembic.ddl entry point in
pyproject.toml:
[project.entry-points."alembic.ddl"]
cubrid = "sqlalchemy_cubrid.alembic_impl:CubridImpl"When Alembic detects a cubrid:// connection URL, it automatically loads
CubridImpl. No imports or configuration are required in your migration files.
class CubridImpl(DefaultImpl):
__dialect__ = "cubrid"
transactional_ddl = FalseThe implementation inherits all standard Alembic operations from DefaultImpl:
add_column,drop_columnadd_constraint,drop_constraintcreate_table,drop_tablecreate_index,drop_indexalter_column(type change, rename, nullable, default — all native)bulk_insert
CUBRID supports changing a column's data type in place via MODIFY:
-- This works:
ALTER TABLE users MODIFY COLUMN name BIGINT;In Alembic, alter_column(type_=...) emits native
ALTER TABLE ... MODIFY <col> <definition>. Because CUBRID's MODIFY
restates the entire column definition, the dialect reconstructs the full
definition (NOT NULL / DEFAULT / AUTO_INCREMENT / COMMENT) from the
existing_* metadata Alembic supplies, so existing attributes are not
silently dropped:
def upgrade():
op.alter_column("users", "name", type_=sa.BigInteger())!!! warning "Lossy conversions may be rejected"
Incompatible or truncating conversions may be rejected by the server
depending on the alter_table_change_type_strict system parameter (when
yes, incompatible/truncating conversions raise an error; when no,
CUBRID may silently truncate). For genuinely lossy or unsupported
conversions, fall back to batch_alter_table (table recreate):
```python
def upgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("name", type_=sa.BigInteger())
```
CUBRID supports renaming columns via RENAME COLUMN:
-- This works:
ALTER TABLE users RENAME COLUMN old_name TO new_name;In Alembic, alter_column(new_column_name=...) emits native
ALTER TABLE ... RENAME COLUMN old TO new. When a rename is combined with a
type change, the dialect emits ALTER TABLE ... CHANGE old new <definition>
in a single statement:
def upgrade():
op.alter_column("users", "old_name", new_column_name="new_name")As noted above, DDL is auto-committed. Be aware:
- Keep migrations small (one logical change per migration)
- Test migrations against a staging database before production
- Maintain database backups before running migrations
The CUBRID Alembic implementation maps alter_column() to native CUBRID DDL:
type_=changes →MODIFY(orCHANGEwhen combined with a rename)new_column_name=renames →RENAME COLUMN(orCHANGEwith a type change)nullable=/server_default=/ comment changes routed throughDefaultImpl
Type changes reconstruct the full column definition from existing_* metadata
so that attributes such as NOT NULL / DEFAULT / COMMENT are preserved.
Important — hand-written type-changing migrations must pass
existing_*. Because CUBRID'sMODIFY/CHANGErestate the entire column definition, the dialect can only preserve an attribute it is told about. Alembic autogenerate populatesexisting_type,existing_nullable,existing_server_default, andexisting_commentfrom the reflected column, but a manualop.alter_column(..., type_=...)does not. When editing a migration by hand, passexisting_nullable=,existing_server_default=,existing_comment=, andexisting_autoincrement=for any attribute that must survive the type change — otherwise it will be dropped. To intentionally remove an attribute (e.g. a default), pass it explicitly (server_default=None), which overrides theexisting_*value.
| Operation | Supported | Workaround |
|---|---|---|
create_table |
✅ | — |
drop_table |
✅ | — |
add_column |
✅ | — |
drop_column |
✅ | — |
alter_column (nullable) |
✅ | — |
alter_column (default) |
✅ | — |
alter_column (type) |
✅ | batch_alter_table for lossy conversions |
alter_column (rename) |
✅ | — |
create_index |
✅ | — |
drop_index |
✅ | — |
add_constraint |
✅ | — |
drop_constraint |
✅ | — |
bulk_insert |
✅ | — |
| Transactional DDL | ❌ | Small atomic migrations |
"""create users table
Revision ID: 001
"""
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
"users",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("email", sa.String(200), unique=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("CURRENT_TIMESTAMP")),
)
op.create_index("ix_users_email", "users", ["email"])
def downgrade():
op.drop_index("ix_users_email", table_name="users")
op.drop_table("users")def upgrade():
op.add_column("users", sa.Column("is_active", sa.SmallInteger(), server_default="1"))
def downgrade():
op.drop_column("users", "is_active")def upgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("name", type_=sa.String(500))
def downgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("name", type_=sa.String(100))def upgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("name", new_column_name="full_name")
def downgrade():
with op.batch_alter_table("users") as batch_op:
batch_op.alter_column("full_name", new_column_name="name")Cause: sqlalchemy-cubrid is not installed, or not installed with the
alembic extra.
Fix:
pip install sqlalchemy-cubrid[alembic]Cause: The alembic_impl module was imported directly without Alembic
installed.
Fix:
pip install "alembic>=1.7,<2.0"Cause: A migration with multiple DDL statements failed partway through. Because CUBRID auto-commits DDL, some statements already took effect.
Fix:
- Manually inspect the database state
- Either complete the remaining operations manually, or reverse the completed ones
- Stamp the revision to the correct state:
alembic stamp <revision>
Cause: A lossy or incompatible type conversion when the
alter_table_change_type_strict system parameter is yes.
Fix: For genuinely lossy/unsupported conversions, use batch_alter_table — see ALTER COLUMN TYPE (native).
!!! warning "DDL is not transactional" CUBRID auto-commits DDL. A failed migration can leave partial schema changes applied. Prefer small revisions with one logical schema change each.
!!! warning "Lossy type changes may be rejected by the server"
alter_column(type_=...) and alter_column(new_column_name=...) emit native
CUBRID DDL (MODIFY / RENAME COLUMN / CHANGE). Only genuinely lossy or
incompatible type conversions are rejected (governed by
alter_table_change_type_strict); for those, use op.batch_alter_table()
with tested downgrade steps.
!!! tip "Always test upgrade + downgrade on staging" Validate full forward and backward migration chains before production rollout.
!!! tip "Create backups for destructive operations"
Back up data before drop_column, drop_table, or multi-step restructuring migrations.
Before running migrations in production:
- One DDL operation per revision — since each DDL auto-commits, a failure mid-revision leaves partial state. Split multi-DDL revisions.
- Backup database —
cubrid backupdb demodbbefore destructive operations - Test upgrade + downgrade cycle — run
alembic upgrade head && alembic downgrade -1 && alembic upgrade headon staging - Verify state after each step — query
db_classsystem table to confirm schema matches expectations - Maintenance window — schedule high-impact migrations during low-traffic periods
- Rollback script ready — for each
upgrade(), have a tested manual rollback SQL ifdowngrade()is insufficient
cubrid backupdb demodb— create a full backupalembic upgrade headon a staging copy- Run smoke tests and critical queries against staging
alembic downgrade -1andalembic upgrade headto verify reversibility- Deploy during a maintenance window for high-impact schema changes
- Monitor
alembic currentpost-deploy to confirm expected revision
Add the following script to catch multi-DDL revisions early. This is advisory (warning-only) and does not block CI:
#!/usr/bin/env python3
"""Check Alembic revisions for multiple DDL operations (advisory).
Warns when a single revision contains multiple DDL calls, which is risky
with CUBRID's non-transactional DDL.
Usage:
python scripts/alembic_safety_check.py alembic/versions/
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
DDL_CALLS = {
"create_table", "drop_table", "add_column", "drop_column",
"create_index", "drop_index", "alter_column",
"add_constraint", "drop_constraint",
}
def check_revision(path: Path) -> list[str]:
tree = ast.parse(path.read_text(encoding="utf-8"))
warnings = []
for func in ast.walk(tree):
if not isinstance(func, ast.FunctionDef) or func.name not in ("upgrade", "downgrade"):
continue
ddl_count = sum(
1 for node in ast.walk(func)
if isinstance(node, ast.Attribute) and node.attr in DDL_CALLS
)
if ddl_count > 1:
warnings.append(
f"{path.name}:{func.name}() has {ddl_count} DDL operations "
f"(recommended: 1 per revision for CUBRID)"
)
return warnings
def main() -> None:
versions_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("alembic/versions")
if not versions_dir.is_dir():
print(f"Directory not found: {versions_dir}")
sys.exit(1)
all_warnings = []
for py_file in sorted(versions_dir.glob("*.py")):
all_warnings.extend(check_revision(py_file))
if all_warnings:
print("⚠️ Alembic safety warnings (advisory):")
for w in all_warnings:
print(f" • {w}")
print(f"\nTotal: {len(all_warnings)} warning(s)")
print("Tip: Split multi-DDL revisions to avoid partial migration state.")
else:
print("✓ All revisions have single DDL operations per function.")
if __name__ == "__main__":
main()Example CI integration (.github/workflows/ci.yml):
- name: Alembic safety check (advisory)
run: python scripts/alembic_safety_check.py alembic/versions/ || trueFor critical migrations, create a companion rollback SQL file:
-- rollback_001_add_users_table.sql
-- Manual rollback for revision abc123 (add users table)
-- Use if alembic downgrade fails or is insufficient.
DROP TABLE IF EXISTS users;
-- Verify: SELECT class_name FROM db_class WHERE class_name = 'users';
-- Expected: no rowsStore rollback scripts in alembic/rollbacks/ alongside your versions directory.
See also: Connection Guide · Type Mapping · Feature Support