Migrating from Granite to Grant
Grant is the default model layer in new Amber CLI 2.0.6 web applications.
That does not make an ORM replacement part of the Amber 1-to-2 framework
upgrade. First prove that the existing application can run on Amber
2.0.0-beta.5 with its current persistence stack. Start this guide only when
moving to Grant is an explicit second decision.
Establish the safety boundary
Before editing a model:
- Record the current Crystal, Amber, Granite, driver, and database versions.
- Run the complete test suite and compile the application binary.
- Back up the database and restore that backup into a disposable environment.
- Capture representative reads, writes, validations, associations, transactions, callbacks, and error behavior.
- Choose one low-risk model boundary for the first migration.
Do not run two migration systems against the same schema without one explicit
owner. Amber CLI uses Micrate SQL under db/migrations/; keep the application's
existing migration history and decide where new versions will be recorded
before applying anything.
Pin Grant and one driver
File: shard.yml — add the same reviewed Grant source used by a generated
Amber CLI 2.0.6 application plus the application's database driver.
dependencies:
grant:
github: crimson-knight/grant
commit: 2665a978b43ac608c68cde9243821f8f8f053372
pg:
github: will/crystal-pg
version: 0.30.0
The example uses PostgreSQL. Use the SQLite or MySQL dependency from a freshly
generated 2.0.6 app when that is the database being migrated. Do not add all
three drivers.
Register the Grant connection
File: config/database.cr — register a connection loaded by the app's
existing require "../config/*" entry point.
require "grant"
require "grant/adapter/pg"
Grant::Connections << Grant::Adapter::Pg.new(
name: "primary",
url: ENV["DATABASE_URL"]? || Amber.settings.database_url
)
Use Grant::Adapter::Sqlite with require "grant/adapter/sqlite" or
Grant::Adapter::Mysql with require "grant/adapter/mysql" for those drivers.
Translate one model without changing its table
Existing Granite file: src/models/user.cr.
class User < Granite::Base
connection pg
table users
column id : Int64, primary: true
column email : String
column name : String?
column admin : Bool = false
column created_at : Time?
column updated_at : Time?
end
Grant replacement: src/models/user.cr.
class User < Grant::Base
connection primary
table users
column id : Int64, primary: true
column email : String
column name : String?
column admin : Bool = false
timestamps
end
Keep connection primary and table users explicit during a migration. This
matches the supported generator and prevents an inference change from silently
pointing at another connection or table. timestamps maps the conventional
created_at and updated_at columns; verify their exact database types before
removing the previous declarations.
Preserve schema before changing behavior
An ORM migration does not inherently require a database schema migration. If the existing table already matches the Grant columns, first make the new model read and write the existing schema. Add Micrate SQL only for an intentional schema change.
Write a focused spec against the restored disposable database:
user = User.new
user.email = "[email protected]"
user.admin = false
user.save.should be_true
persisted = User.find(user.id)
persisted.should_not be_nil
persisted.not_nil!.email.should eq("[email protected]")
Then prove update and destroy, required and nullable values, unique constraints, timestamps, and the error paths used by the application.
Translate application operations deliberately
Do not perform a global search-and-replace. Convert one behavior at a time and keep a spec beside it.
# Collection
users = User.all.to_a
# Primary-key lookup
user = User.find(params[:id])
# Typed assignment and persistence
user = User.new
user.email = schema.email.not_nil!
user.name = schema.name
user.save
# Delete
user.destroy
For filtering, associations, validations, callbacks, transactions, and security APIs, follow the matching Grant guides and verify the behavior against the pinned commit. Do not assume a similarly named Granite method has identical return types, callback order, transaction scope, or error semantics.
Decide whether the ORMs may coexist
Coexistence can be useful for a staged migration, but it is not automatic. Before running Granite and Grant together, prove:
- their connection pools do not compete for lifecycle ownership;
- only one migration system advances the schema;
- a transaction does not falsely imply atomicity across different pools;
- callbacks and validations are not executed twice;
- two classes writing one table agree on types, defaults, timestamps, and optimistic-locking behavior;
- application code names which ORM owns each model.
If those conditions are not testable, migrate in a maintenance window or a separate deployment rather than carrying two active writers.
Completion gates
For every migrated model, keep evidence for:
- schema compatibility and reversible migration SQL when schema changed;
- representative create, read, update, and destroy operations;
- nullable and required fields on new records;
- validations and database constraints;
- associations and query counts;
- callback order and external side effects;
- transaction rollback behavior;
- production-shaped performance for critical queries.
Only remove Granite after no application file, job, task, or maintenance script requires it and a restored production backup passes the Grant-backed suite.