Repository navigation
Migrations
A migration is one change to a table that already holds data, written as a numbered SQL file that runs once. Every computer and every server runs the same files in the same order, so they all end up with the same tables. Since 2.18.0.
Which file to write:
| You want to... | Write | Run it with |
|---|---|---|
| create a new table (its first version) |
CREATE TABLE IF NOT EXISTS in application/models/<Folder>/create_db.sql (Models) |
php caligrafer.php sql application/models/<Folder>/create_db.sql |
| change a table that holds data: add a column or an index, rename, fix rows | a migration in application/migrations/ (this page) |
php caligrafer.php migrate |
A create_db.sql that already ran is never edited: once a table has rows, every change to it goes in a new migration.
Ask for the change with the db skill of the Claude Code plugin:
/caligrafy-quill:db add an isbn column (20 characters, optional, unique) to the books table
Claude writes application/migrations/<number>_<name>.sql with php caligrafer.php migrate make <name>, puts the SQL
in it, runs php caligrafer.php migrate (in the Docker container when the project uses the Docker server) and checks
php caligrafer.php migrate status. It never edits a migration that already ran.
Check before you say OK:
- the new file is in
application/migrations/and holds one change; -
php caligrafer.php migrate statusshows every file asran; - the model of the table has a property for each new column (
public $isbn;).
The Bookshelf books table exists and holds rows (Models).
Add an isbn column to it. With the Docker server of the
Quickstart, run every command in the container:
docker compose exec dev-box php caligrafer.php ... (without Docker, drop docker compose exec dev-box).
-
Create the file. Run:
It prints the file it wrote (the number is the date and time):
docker compose exec dev-box php caligrafer.php migrate make add_books_isbnapplication/migrations/20261107120200_add_books_isbn.sql written: add its SQL, then php caligrafer.php migrate -
Write the change. Open that file and make it read:
Two books never share an ISBN, and a book without one has
-- add_books_isbn -- Runs once (php caligrafer.php migrate, and at every deploy). Never edit it after it ran: write a new one. -- One statement: the column and its unique index are added together, or not at all. ALTER TABLE books ADD COLUMN isbn VARCHAR(20) NULL AFTER summary, ADD UNIQUE KEY uq_books_isbn (isbn);
NULL, which the unique index allows on many rows. -
See what will run. Run:
docker compose exec dev-box php caligrafer.php migrate statuspending 20261107120200_add_books_isbn.sql -
Run it. Run:
docker compose exec dev-box php caligrafer.php migrateRun it again and it saysOK 20261107120200_add_books_isbn.sql 1 migration(s) run on caligrafyNothing to migrate: caligrafy is up to date. -
Give the model the new column. In
application/models/Bookshelf/Book.php, addpublic $isbn;afterpublic $summary;(the whole model is on the Models page). -
Commit the file. It runs online on its own: the deploy step (
php caligrafer.php deploy, which the production image runs at every start, and so Caligrafy Cloud) runs the pending migrations after thecreate_db.sqlscripts.
Check:
-
docker compose exec dev-box php caligrafer.php migrate statusshowsran 20261107120200_add_books_isbn.sql (<date and time>); -
docker compose exec dev-box php caligrafer.php doctor --only databaseshowsOK Migrations: 1 ran, none pending; - the column is there:
docker compose exec mysql mysql -uroot -proot caligrafy -e "DESCRIBE books"listsisbn varchar(20) YES UNI NULL.
Commands
| Command | What it does |
|---|---|
php caligrafer.php migrate make <name> |
writes application/migrations/<YYYYMMDDHHMMSS>_<name>.sql with a comment header (the name becomes lower case, letters, digits and _). Needs no database |
php caligrafer.php migrate |
runs, in number order, every file that never ran on this database, and records each one |
php caligrafer.php migrate status |
each file with its state: pending, ran (with when) or changed
|
php caligrafer.php migrate --env .env.testing |
the same on the database of another .env file, here the test database; --env goes with migrate and migrate status (2.19.1; before, write migrate "" --env .env.testing) |
php caligrafer.php doctor |
WARN Migrations: 1 pending: ... or ... changed after it ran: it will not run again; OK Migrations: N ran, none pending
|
php caligrafer.php deploy |
(production start) runs the create_db.sql scripts, then the pending migrations: OK Migrations: 1 run: <name> or up to date. A failing migration stops it with exit 1 |
Files
- Named
<number>_<name>.sql: the number is 1 to 14 digits, the name letters, digits and_. Another.sqlname in the folder stops every command:notes.sql: a migration is named <number>_<name>.sql, e.g. 20261107120000_add_books_isbn.sql. - Two files with the same number (as a number:
5_a.sqland005_b.sql) are refused. - The statements are split on
;outside quotes ('...',"...",`...`) and comments (--,#,/* */), so text such as'a;b'is safe.DELIMITERis not supported: write triggers and procedures in a script you run withphp caligrafer.php sql <file>. - An empty file runs and is recorded: write its SQL before you run
migrate.
The table caligrafy_migrations (made by php caligrafer.php platform install, or by the first migrate): name
(the file name), sha256 (of its content when it ran) and ran_at. A file whose content no longer matches its
sha256 is changed: it is never run again.
-
Never edit a migration that ran. It will not run again:
statusshowschanged, andmigrateprintsWARN <name>: changed after it ran, not run again (write a new migration for the change). Put the file back as it was and write a new one. - Forward only. There is no "down": to undo a change, write a migration that undoes it.
-
One change per file, or files that can run twice. MySQL saves a
CREATEor anALTERat once. When the second statement of a file fails, the first one stays applied and the file is not recorded:Keep one change per file, or write it so that it can run again (migrate: 20261107120300_add_more.sql, statement 2: SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'isbn'CREATE TABLE IF NOT EXISTS).
create_db.sql gives a new database its tables: php caligrafer.php sql <file> runs it by hand, and the deploy step
runs it when it is new or changed (a changed script that drops or deletes data is not run again: it says so in a WARN
line). A migration runs exactly once on each database, in order, after the scripts. So:
- a new table:
create_db.sql(its first version,CREATE TABLE IF NOT EXISTS); - any change to a table that holds data: a migration;
- sample rows for your computer: a
seed.sqlyou run withphp caligrafer.php sql(the deploy step never runs it).
- Editing the file after it ran (G120): nothing happens on any database. Restore it and write a new migration.
-
Changing
create_db.sqlinstead of writing a migration: a database that already has the table keeps the old one (CREATE TABLE IF NOT EXISTSskips it). -
A table change inside a transaction or a test: MySQL ends the transaction at the first
CREATEorALTER. Keep table changes in migrations. - Forgetting the model: the new column is still read and written (the rows carry it), but the model no longer says what the table holds. Add its property in the same change.
- Models: the first version of a table, and its model
- Caligrafer: migrate
- Testing your app: migrations on a test database
- Putting it online: the deploy step
Caligrafy: Describe it. Approve it. Ship it. Your idea turned into a complete app: every feature built once, for web pages, Vue apps, a REST API and AI agents, and online.
-
Quickstart with Claude: describe it, approve it, ship it
or the Quickstart: the same three steps by hand, in 5 minutes
- How Caligrafy works: the ideas in 5 minutes
- Your first capability: build a feature, page included
-
Capabilities in depth
- Scheduled jobs: capabilities on a timer
- Notifications: web push and email
- Background jobs: capabilities run later
- Migrations: database changes, in order
- The cache
- Testing your app
- Dependency injection
- Validation rules
-
Web pages
-
Vue apps
-
Mobile and tablet
-
REST API
-
AI agents
- Models
- Relationships
- Queries: filters, paging, related rows
- Sign-in, users and tokens · with an email
- Payments: Stripe and crypto
- Face detection and face login
- OpenAI: chat and images · Assistants
- Open models: Llama, Mistral...
- Chatbots with Watson
- Machine learning in the browser
- Metadata and social cards · search and analytics
-
Putting it online: the production image, the database at every start
- Caligrafer: every command
- Installation, configuration and Caligrafer
- Routing · Request · Controllers · Views
- Vue: flow of information · routes · validations
- Security and upgrading
- Release notes: 2.21 · 2.20 · 2.19 · 2.18 · 2.17 · 2.16 · 2.15 · 2.14 · 2.13 · 2.12 · 2.11 · 2.10 · 2.9 · 2.8 · 2.7 · 2.6 · 2.5 · 2.4 · 2.3 · 2.1 · October 2026 · changelog
- Testing Caligrafy (contributors)