Documentation

Granite to Grant Migration

New
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

1.4.1
Unavailable in this version
1.5
Unavailable in this version

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:

  1. Record the current Crystal, Amber, Granite, driver, and database versions.
  2. Run the complete test suite and compile the application binary.
  3. Back up the database and restore that backup into a disposable environment.
  4. Capture representative reads, writes, validations, associations, transactions, callbacks, and error behavior.
  5. 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.

YAML
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.

Crystal
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.

Crystal
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.

Crystal
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:

Crystal
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.

Crystal
# 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.