sqlite_schema_forge 0.1.1
sqlite_schema_forge: ^0.1.1 copied to clipboard
Declarative SQLite schema and auto-migration for Dart: define tables as immutable objects and let the differ plan and apply safe CREATE, ALTER, and rebuild steps.
SQLite Schema Forge (SSF) #
A dead-simple, ultra-minimalist, intuitive, and declarative SQLite schema manager with auto-migration support for Dart & Flutter.
Key Features #
- 100% Declarative: Define desired schemas with plain Dart objects—no callback wrappers or raw SQL
CREATE TABLEstrings needed. - Auto-Diffing Engine: Introspects live SQLite
PRAGMAmetadata and auto-generates nativeALTER TABLEstatements or safe copy-and-swap table rebuilds. - Fast-Path Boot: Reads
PRAGMA user_versionon startup (<1ms) to ensure zero overhead on warm launches. - Driver Agnostic: Runs on any SQLite driver (
sqflite,sqlite3, FFI) via a lightweightDatabaseExecutorcontract. - Global Orchestration Ready: Exposes
plan()andapplyPlan()for dry runs and multi-storage migrations (Hive, DataStore, SharedPreferences).
Quick Start #
import 'package:sqlite_schema_forge/sqlite_schema_forge.dart';
final appSchema = Schema(
version: 1,
tables: [
Table(
'wallets',
[
Column.id(),
Column.text('name', nullable: false),
Column.integer('wallet_type_id', nullable: false),
Column.integer('is_deleted', nullable: false, defaultValue: 0),
Column.datetime('created_at', defaultValue: RawSql.currentTimestamp),
Column.datetime('updated_at', defaultValue: RawSql.currentTimestamp),
],
indexes: [
Index(columns: ['wallet_type_id']), // Auto-named: 'idx_wallets_wallet_type_id'
],
),
],
);
void main() async {
final db = MyDatabaseDriver(); // Wraps sqflite, sqlite3, or FFI
await SqliteSchemaForge.enableForeignKeys(db);
final result = await SqliteSchemaForge.migrate(
db: db,
targetSchema: appSchema,
);
print('Migrated: ${result.migrated}');
print('Executed SQL:\n${result.executedSql.join('\n')}');
}
Migration and Query Planning #
SSF eliminates manual up and down migration files. Instead, the diffing engine calculates structural changes at runtime.
1. Data Transformation Callbacks #
When structural changes require data transformations (like splitting a column or formatting data before a type change), use onUpgrade or onDowngrade:
final result = await SqliteSchemaForge.migrate(
db: db,
targetSchema: appSchemaV2,
allowDestructive: true, // Required for column removals or type modifications
onUpgrade: (db, from, to) async {
if (from == 1 && to >= 2) {
// Phase 1: Transform existing data before structural table rebuild
await db.execute("UPDATE users SET age = 0 WHERE age IS NULL;");
}
},
);
2. Inspecting Plans (plan()) #
To preview migration queries without writing to the database:
final MigrationPlan plan = await SqliteSchemaForge.plan(
db: db,
targetSchema: appSchema,
);
if (plan.hasDestructiveChanges) {
print('Warning: Migration requires table rebuilds!');
}
for (final step in plan.steps) {
print('[${step.type.name}] ${step.description}');
print('SQL: ${step.sql}\n');
}
Custom Migration Control (applyPlan) #
If you manage application versioning externally (for example, in a multi-storage setup alongside Hive or DataStore), use applyPlan.
Unlike migrate(), applyPlan() executes the schema diffs **without modifying PRAGMA user_version**:
final plan = await SqliteSchemaForge.plan(db: db, targetSchema: appSchema);
// Executes structural updates without touching PRAGMA user_version
await SqliteSchemaForge.applyPlan(
db: db,
plan: plan,
allowDestructive: true,
);
Foreign Keys #
By default, SQLite disables foreign key constraint enforcement on new connections. SSF provides a helper to enable enforcement:
await SqliteSchemaForge.enableForeignKeys(db);
When defining foreign key relationships in Table:
Column.integer(
'wallet_type_id',
nullable: false,
refTable: 'wallet_types',
refCol: 'id',
);
Triggers and Virtual Tables #
SSF provides first-class, declarative support for Full-Text Search (FTS3/4/5) virtual tables and custom SQLite triggers. They live directly inside your Schema definition alongside standard tables.
The diffing engine automatically inspects sqlite_master to detect missing or modified virtual tables and triggers, creating or re-creating them dynamically during migration.
1. Virtual Tables (FTS5 / FTS4 / FTS3) #
Use VirtualTable.fts5() to define full-text search tables with content bindings:
final transactionsFts = VirtualTable.fts5(
'transactions_fts',
columns: ['name', 'note'],
contentTable: 'transactions', // Binds FTS index directly to content table
contentRowId: 'id',
);
2. Triggers #
Triggers can be defined using raw SQL execution bodies or via express helpers for common patterns like FTS synchronization.
// Option A: FTS Sync Preset (Zero-boilerplate)
final ftsInsertTrigger = Trigger.ftsSync(
name: 'transactions_fts_insert',
targetTable: 'transactions',
ftsTable: 'transactions_fts',
columns: ['name', 'note'],
);
// Option B: Custom Trigger Object
final updateTimestampTrigger = Trigger(
name: 'update_wallet_timestamp',
timing: TriggerTiming.before,
event: TriggerEvent.update,
targetTable: 'wallets',
body: 'SET updated_at = CURRENT_TIMESTAMP WHERE id = old.id;',
);
Complete Declarative Schema Example #
Include standard tables, virtual tables, and triggers together in a single immutable Schema object:
final appSchema = Schema(
version: 1,
tables: [
Table('transactions', [
Column.id(),
Column.text('name', nullable: false),
Column.text('note'),
]),
],
virtualTables: [
VirtualTable.fts5(
'transactions_fts',
columns: ['name', 'note'],
contentTable: 'transactions',
),
],
triggers: [
Trigger.ftsSync(
name: 'transactions_fts_insert',
targetTable: 'transactions',
ftsTable: 'transactions_fts',
columns: ['name', 'note'],
),
],
);
// Runs diffing engine across Tables -> Virtual Tables -> Triggers sequentially
await SqliteSchemaForge.migrate(
db: db,
targetSchema: appSchema,
);
How Diffing Works Under the Hood #
Unlike standard tables that rely on PRAGMA table_info, SSF inspects SQLite's catalog metadata (sqlite_master) for virtual tables and triggers:
- Missing Entities: If a trigger or virtual table does not exist in
sqlite_master,SSFappendsCREATE VIRTUAL TABLEorCREATE TRIGGERsteps to the migration plan. - Modified Definitions: If the declarative SQL definition in code differs from the live SQL stored in
sqlite_master,SSFgenerates a drop-and-recreate sequence (DROP ... IF EXISTSfollowed byCREATE ...). - Dependency Ordering:
plan()automatically sorts migration steps ($\text{Tables} \longrightarrow \text{Virtual Tables} \longrightarrow \text{Triggers}$) so referenced tables always exist before triggers or FTS bindings attempt to hook into them.