|
Lightweight 0.20261002.0
|
SQL migrations provide a structured way to evolve your database schema over time. Each migration represents a discrete change (creating tables, adding columns, etc.) that can be applied or reverted independently.
Key benefits:
The simplest way to create a migration:
The migration is automatically registered with the MigrationManager when the program starts, and unregistered again when it is destroyed. Registration is tied to the object's lifetime, so a migration that does not live for the whole program – one with automatic storage duration, say – leaves no dangling pointer behind in the manager it registered with.
For more control, including rollback support:
Migration timestamps use the format YYYYMMDDHHMMSS (14 digits):
Timestamps must be:
When creating migrations in a shared library plugin, add this macro to exactly one source file:
The LIGHTWEIGHT_MIGRATION_PLUGIN() macro exports the AcquireMigrationManager() function that dbtool uses to load migrations from the plugin.
Create a new table with various column types:
Column modifiers (chain after column declaration):
.Unique() - Add unique constraint.Index() - Create an index on this column.UniqueIndex() - Create a unique indexConditional creation:
Modify an existing table:
Available operations:
| Method | Description |
|---|---|
.AddColumn(name, type) | Add a non-nullable column |
.AddNotRequiredColumn(name, type) | Add a nullable column |
.RenameColumn(old, new) | Rename a column |
.DropColumn(name) | Remove a column |
.AlterColumn(name, type, nullable) | Change column type or nullability |
.AddIndex(column) | Create an index |
.AddUniqueIndex(column) | Create a unique index |
.DropIndex(column) | Remove an index |
.AddForeignKey(column, ref) | Add foreign key to existing column |
.AddForeignKeyColumn(name, type, ref) | Add new column with foreign key |
.DropForeignKey(column) | Remove foreign key constraint |
.RenameTo(newName) | Rename the table |
SQLite note: SQLite has no native
ALTER TABLE … ALTER COLUMNor… ADD/DROP CONSTRAINT, so.AlterColumn(...),.AddForeignKey(...), and.DropForeignKey(...)are applied by rebuilding the table. That rebuild runs only when the migration is applied throughMigrationManager(ApplyPendingMigrations()); the generatedToSql()text for these operations is a-- LIGHTWEIGHT_SQLITE_GUARD:sentinel comment that does nothing if executed directly. Applying such a migration viaSqlStatement::MigrateDirectthrows rather than silently skipping the change.
Conditional operations:
Remove a table:
Conditional drop:
Cascade drop (removes foreign key constraints):
Insert data during migrations:
Update existing data:
Remove data:
Create standalone indexes:
Composite indexes:
Pass a SqlCreateIndexOptions to index only the rows that match a predicate. SQLite and PostgreSQL call this a partial index, SQL Server a filtered index; all three accept the same WHERE clause after the column list.
A typical use is deduplicating on an optional key: rows that have a key must be unique, rows without one may repeat. An unconditional unique index would reject the second row with an empty key.
whereExpression is the predicate body without the leading WHERE. It is emitted verbatim, so quote identifiers and literals yourself. To stay portable, keep to what SQL Server accepts in a filter: comparisons of a column with a constant, IS [NOT] NULL and IN (...) lists, joined with AND (no OR, functions or computed expressions).
SQL Server: a filtered index requires
ANSI_WARNINGS ONboth to be created and for everyINSERT,UPDATEandDELETEon its table; otherwise SQL Server fails the statement with error 1934. Lightweight's default string-truncation mode (SqlStringTruncationMode::Truncate) turnsANSI_WARNINGSoff, so connections that apply or write through a filtered index must useSqlStringTruncationMode::Error, set per connection withSqlConnection::SetStringTruncationModeor for new connections withSqlConnection::SetDefaultStringTruncationMode. SQLite and PostgreSQL have no such requirement.
Limitation: the backup tool (
dbtool backup/SqlBackup) reads indexes from the live schema and does not capture index predicates yet. It backs a partial index up as an index over all rows, and a single-column partial unique index can also mark its column unique. Restoring data that relies on the predicate (such as two rows with an emptydedup_key) therefore fails for such tables.
For database-specific features or complex operations:
| C++ Type | SQL Type | Notes |
|---|---|---|
Integer() | INTEGER | 32-bit integer |
Smallint() | SMALLINT | 16-bit integer |
Bigint() | BIGINT | 64-bit integer |
Tinyint() | TINYINT | 8-bit integer |
Real() | REAL/FLOAT | Floating point |
Bool() | BOOLEAN/BIT | Boolean |
Char(n) | CHAR(n) | Fixed-length string |
Varchar(n) | VARCHAR(n) | Variable-length string |
NChar(n) | NCHAR(n) | Fixed-length Unicode string |
NVarchar(n) | NVARCHAR(n) | Variable-length Unicode string |
Text() | TEXT | Large text |
DateTime() | DATETIME/TIMESTAMP | Date and time |
Date() | DATE | Date only |
Time() | TIME | Time only |
Guid() | UNIQUEIDENTIFIER/UUID | UUID/GUID |
Decimal(p, s) | DECIMAL(p, s) | Fixed-point number |
Binary(n) | BINARY(n) | Fixed-length binary |
VarBinary(n) | VARBINARY(n) | Variable-length binary |
Usage:
When the target database expects unqualified DDL to land in a non-default schema (e.g. lasa instead of dbo / public), tell the manager about it before opening the first connection:
SetDefaultSchema installs a post-connect hook that emits the dialect- specific "make this the session default" statement for every new connection:
SET search_path TO "lasa", public. Both schema_migrations and unqualified DDL inside migrations land in lasa.DEFAULT_SCHEMA server-side (ALTER USER … WITH DEFAULT_SCHEMA = lasa). Migrations that need to write to a specific schema regardless of the login default should use the WithSchema(...) builder.Schema names are validated against [A-Za-z0-9_] and rejected via std::invalid_argument otherwise. Passing "" clears any previously installed hook.
dbtool exposes this via the --schema flag and dbtool-gui exposes it as a "Schema" input on both the Profile and the direct-ODBC connection tabs.
Some databases predate the migration set — they were created by older tooling and differ from what the migrations declare. A compat policy returns, per migration, a set of opt-in flags that relax strict rendering for exactly those migrations. Migration plugins usually install one; it can also be set directly:
| Flag | Effect |
|---|---|
lup-truncate | String values in Insert / Update steps are truncated to the destination column's width, with a warning, instead of failing. |
fk-match-referenced-text | A foreign-key column declared by CreateTable / AlterTable (AddForeignKeyColumn) takes the narrow/wide variant of the column it references when the two differ only in that respect (CHAR(n)↔NCHAR(n), VARCHAR(n)↔NVARCHAR(n)). The referenced column's type is read from the live database (or from an earlier migration of the same run), and each change is logged as a warning. SQL Server only — PostgreSQL and SQLite store both kinds the same way. |
Flags only change the SQL sent to the database. Checksums are computed from the migration as declared, so the same migration has the same checksum on every database.
Generate SQL without executing:
Revert migrations:
Mark a migration as applied without executing:
Lightweight automatically creates a schema_migrations table to track applied migrations:
| Column | Type | Description |
|---|---|---|
| version | BIGINT | Migration timestamp |
| checksum | VARCHAR(64) | SHA-256 checksum of migration SQL |
| applied_at | DATETIME | When the migration was applied |
Use SqlScopedLock (the generic distributed-lock RAII type) to prevent concurrent migrations:
SqlScopedLock is not migration-specific — any caller that needs a named cross-process token can use it (cron leadership, "only one worker
processes batch X", queue ownership, …). Pick any string for the lock name; two processes that pass the same string will serialise on it.
For structured error handling — distinguishing timeout from deadlock from driver error programmatically — use the non-throwing SqlScopedLock::TryConstruct(connection, name, timeout) factory, which returns std::expected<SqlScopedLock, SqlLockError>.
The dialect-specific primitive is selected automatically by the active SqlQueryFormatter:
sp_getapplock / sp_releaseapplockpg_advisory_lock / pg_advisory_unlock_lightweight_locks table guarded by a unique constraintOn SQLite the bookkeeping table _lightweight_locks is treated as infrastructure — dbtool hard-reset drops it alongside schema_migrations rather than mistaking it for user data.
IfNotExists/IfExists variants when idempotency is important.dbtool backup before applying migrations to production.--dry-run before applying: