diesel-migration
Skill2 starsChanged 15 days agoMITView on GitHub
Diesel + SQLite schema workflow for this monorepo. Use when adding or changing a table or column - writing a migration in packages/desktop/migrations, updating packages/orm/src/schema.rs, or writing the model structs in packages/orm/src/models.
- Deletes or force-pushes
In this repository8 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- cavemanSkill
- gpui-coderSkill
- impeccableSkill
- pen-design-readerSkill
- rust-coderSkill
- design-taste-frontendSkill
- ui-ux-pro-maxSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 51
Use it as a reference, do not redirect it over schema.rs.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Diesel Migration & Model Workflow
The desktop app keeps its data in one SQLite file. Migrations are raw SQL, compiled into the binary
and applied at startup. There is no migrator package, no diesel.toml, no DATABASE_URL, and no
cargo make db:* task.
Where things live
| Path | Role |
packages/desktop/migrations/<date>_<name>/{up,down}.sql | SQL migrations, hand-written |
packages/desktop/src/store/mod.rs | embed_migrations!("migrations"), init_pool, run_db, the per-connection PRAGMAs |
packages/orm/src/schema.rs | Diesel table! macros, kept by hand (see step 3) |
packages/orm/src/models/<table>.rs | Model structs, exported from models/mod.rs |
packages/desktop/src/store/<area>.rs | Queries; map rows to the shared record types |
packages/shared/src/lib.rs | Record and request types the UI sees |
init_pool runs every pending migration when the app opens the database, so a migration ships by
being in the directory. Never edit a migration that has been released: users already ran it. Add a
new one.
Workflow
- Create the directory
packages/desktop/migrations/YYYY-MM-DD-HHMMSS_<name>/withup.sqlanddown.sql. Names sort by date; use000000,010000, ... for several on one day. - Write the SQL (patterns below). Start
up.sqlwith a one-line comment saying what the column or table means when that is not obvious from its name. - Update
schema.rsby hand to mirror the migration. A column added withALTER TABLE ... ADD COLUMNgoes at the end of itstable!block, because that is where SQLite puts it. A new table also goes inallow_tables_to_appear_in_same_query!, and each foreign key gets ajoinable!line. - Update the models in
packages/orm/src/models/and the exports inmodels/mod.rs. - Update the store function and the
sharedrecord if the UI needs the new data. - Verify:
cargo test -p desktop store. The store tests build a database from the embedded migrations and insert through the models, so a schema/model mismatch fails there.
Checking schema.rs against the SQL
diesel print-schema can show what the migrations really produce:
diesel migration run --database-url /tmp/schema.db --migration-dir packages/desktop/migrations
diesel print-schema --database-url /tmp/schema.dbUse it as a reference, do not redirect it over schema.rs. Two differences are expected: it prints
TEXT PRIMARY KEY columns as Nullable<Text> (SQLite allows NULL there; the checked-in file says
Text), and it orders tables alphabetically.
SQL conventions
-- A Link publishes target_host:target_port at https://<subdomain>.<relay domain>.
CREATE TABLE links (
id TEXT PRIMARY KEY,
subdomain TEXT NOT NULL UNIQUE,
target_host TEXT NOT NULL,
target_port INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);id TEXT PRIMARY KEY, filled by the store withUuid::new_v4().to_string().created_atandupdated_aton every table. There is no trigger: the store setsupdated_attoUtc::now().naive_utc()on every write.- Foreign keys say what happens on delete:
REFERENCES ssh_hosts (id) ON DELETE CASCADEorON DELETE SET NULL. They are enforced becausestore/mod.rssetsPRAGMA foreign_keys = ONon each connection. - Index a foreign key column:
CREATE INDEX <table>_<column>_idx ON <table> (<column>); - Settings, tokens, and secrets are rows in the
settingstable (store/settings.rs), not new tables or side files.
Patterns
-- add a column -- down.sql
ALTER TABLE host_snippets ADD COLUMN pin_color TEXT;
ALTER TABLE host_snippets DROP COLUMN pin_color;
-- add a foreign key column
ALTER TABLE ssh_hosts ADD COLUMN jump_host_id TEXT REFERENCES ssh_hosts (id) ON DELETE SET NULL;SQLite cannot change a column's type, nullability, or constraints in place. Rebuild the table, as
2026-09-28-000000_local_snippets does:
CREATE TABLE host_snippets_new ( ... );
INSERT INTO host_snippets_new SELECT id, host_id, title, body, created_at, updated_at FROM host_snippets;
DROP TABLE host_snippets;
ALTER TABLE host_snippets_new RENAME TO host_snippets;
CREATE INDEX host_snippets_host_id_idx ON host_snippets (host_id); -- indexes go with the old tableA NOT NULL column added to an existing table needs a DEFAULT.
Type mapping (SQLite)
| SQL | schema.rs | Rust |
TEXT | Text | String |
INTEGER | Integer | i32 |
TIMESTAMP | Timestamp | chrono::NaiveDateTime |
| nullable column | Nullable<T> | Option<T> |
Enums are stored as TEXT and converted in the store (ai_access on ssh_hosts). The orm crate
enables only diesel's sqlite and chrono features: no JSON, UUID, or array column types.
Model structs
Three structs per table, in packages/orm/src/models/<table>.rs:
#[derive(Debug, Clone, Queryable, Selectable, Serialize, Deserialize)]
#[diesel(table_name = links)]
pub struct Link { // read: every column, in schema.rs order
pub id: String,
pub subdomain: String,
pub target_host: String,
pub target_port: i32,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = links)]
pub struct NewLink { /* every column, id and both timestamps included */ }
#[derive(Debug, Clone, AsChangeset)]
#[diesel(table_name = links)]
pub struct UpdateLink { /* no id, no created_at; updated_at included */ }Queryablereads by position: field order must match thetable!column order, so a column appended to the table is appended to the struct.AsChangesetskips aNonefield instead of writing NULL. A nullable column that must be clearable isOption<Option<T>>in the update struct.- Export from
models/mod.rs:pub use link::{Link, NewLink, UpdateLink};
Using them from the store
pub async fn link_create(link: LinkWriteRequest, pool: State<'_, DbPool>) -> Result<LinkRecord, String> {
let pool = pool.inner().clone();
run_db(pool, move |connection| {
let now = Utc::now().naive_utc();
diesel::insert_into(links::table)
.values(&NewLink { id: Uuid::new_v4().to_string(), /* ... */ created_at: now, updated_at: now })
.returning(Link::as_returning())
.get_result(connection)
.map(to_record)
.map_err(internal_error)
})
.await
}run_db moves the query onto a blocking thread; never touch a connection on the UI thread or
inside an async task directly. Errors leave the store as String (internal_error, or a specific
message for a constraint the user can fix, as write_error in store/links.rs does).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

