Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .ameba.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Naming/BlockParameterName:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Documentation/DocumentationAdmonition:
Enabled: false
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:

jobs:
test:
name: Crystal 1.21 on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]

steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Install Crystal
uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1
with:
crystal: 1.21.0

- name: Install SQLite headers
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev

- name: Install dependencies
run: shards install

- name: Check formatting
run: crystal tool format --check src spec

- name: Lint
run: crystal run lib/ameba/src/cli.cr --

- name: Build standalone wrapper
run: shards build

- name: Run unit and SQLite integration specs
run: crystal spec
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock
.agents/
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ Micrate currently supports migrations for Postgres, Mysql and SQLite3, but it sh

## Command line

To install the standalone binary tool check out the releases page, or use homebrew:
Amber CLI ships Micrate inside the precompiled `amber` executable. In an Amber
application, prefer the matching commands so no second migration binary is
required:

```console
amber database migrate
amber database rollback
amber database status
```

To install the standalone binary tool, check the releases page or use Homebrew:

```
$ brew tap amberframework/micrate
Expand All @@ -17,7 +27,10 @@ $ brew install micrate

Execute `micrate help` for usage instructions. Micrate will connect to the database specified by the `DATABASE_URL` environment variable.

To create a new migration use the `scaffold` subcommand. For example, `micrate scaffold add_users_table` will create a new SQL migration file with a name such as `db/migrations/20160524162446_add_users_table.sql` that looks like this:
To create a new migration use the `scaffold` subcommand. For example,
`micrate scaffold add_users_table` will create a new SQL migration file with a
millisecond-resolution name such as
`db/migrations/20160524162446123_add_users_table.sql` that looks like this:

```sql
-- +micrate Up
Expand Down Expand Up @@ -96,7 +109,24 @@ dependencies:
github: amberframework/micrate
```

This allows you to programatically use micrate's features. You'll see the `Micrate` module has an equivalent for every CLI command. If you need to use micrate's CLI without installing the tool (which could be convenient in a CI environment), you can write a runner script as follows:
This allows you to programmatically use Micrate's features. Use a
`Micrate::Runner` when embedding migrations so the connection and migrations
directory remain explicit and do not leak through global state:

```crystal
require "micrate"
require "sqlite3"

runner = Micrate::Runner.new(
"sqlite3:./db/app_development.db",
"./db/migrations"
)

runner.connect { |database| runner.up(database) }
```

If you need to use Micrate's CLI without installing the tool (which can be
convenient in CI), write a runner script as follows:

```crystal
#! /usr/bin/env crystal
Expand All @@ -108,7 +138,7 @@ This allows you to programatically use micrate's features. You'll see the `Micra
require "micrate"
require "pg"

Micrate::DB.connection_url = "postgresql://..."
ENV["DATABASE_URL"] = "postgresql://..."
Micrate::Cli.run
```

Expand Down
7 changes: 6 additions & 1 deletion examples/micrate
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
require "../src/micrate"
require "pg"

Micrate::DB.connection_url = "postgresql://..."
# The CLI uses the DATABASE_URL environment variable by default:
# ENV["DATABASE_URL"] = "postgresql://..."
Micrate::Cli.run

# Or to run migrations programmatically:
# runner = Micrate::Runner.new("postgresql://...")
# runner.connect { |db| runner.up(db) }
13 changes: 11 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: micrate
version: 0.15.1
crystal: ">= 0.36.1, < 2.0.0"
version: 0.16.0-beta.1
crystal: ">= 1.20.0, < 2.0.0"

authors:
- Juan Edi <jedi11235@gmail.com>
Expand All @@ -22,8 +22,17 @@ executables:
dependencies:
db:
github: crystal-lang/crystal-db
version: 0.13.1

development_dependencies:
spectator:
gitlab: arctic-fox/spectator
version: ~> 0.11.3
ameba:
github: crystal-ameba/ameba
# Crystal 1.21 compatibility pending Ameba 1.7.0.
commit: cdd58b34b0d8a9c785d67183a7a8f07541549e55
sqlite3:
github: crystal-lang/crystal-sqlite3
# v0.21.0
commit: c58cea290c85e2a33dc8f494a5f04b519d3e0274
37 changes: 37 additions & 0 deletions spec/micrate_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ Spectator.describe Micrate do
end
end

context "with mixed timestamp precision" do
it "orders Amber millisecond and historic Micrate second timestamps chronologically" do
migrations = {
20220906195432 => false,
20220907021800909 => false,
20220908100000 => false,
}

plan = Micrate.migration_plan(migrations, 0, 20220908100000, :forward)
plan.should eq([20220906195432, 20220907021800909, 20220908100000])
end

it "detects a genuinely older migration without treating all 14-digit versions as old" do
migrations = {
20220906195432 => false,
20220907021800909 => true,
20220908100000 => false,
}

expect_raises(Micrate::UnorderedMigrationsException) do
Micrate.migration_plan(migrations, 20220907021800909, 20220908100000, :forward)
end
end
end

describe "detecting unordered migrations" do
it "fails if there are unapplied migrations with older timestamp than current version" do
migrations = {
Expand All @@ -63,6 +88,18 @@ Spectator.describe Micrate do
end
end
end

describe "create" do
it "uses Amber-compatible millisecond timestamps" do
root = File.join(Dir.tempdir, "micrate-create-#{Process.pid}-#{Random.rand(1_000_000)}")
begin
path = Micrate.create("create_pets", root, Time.utc(2026, 8, 11, 12, 34, 56, nanosecond: 789_000_000))
File.basename(path).should eq("20260811123456789_create_pets.sql")
ensure
FileUtils.rm_r(root) if Dir.exists?(root)
end
end
end
end

def sample_migrations
Expand Down
2 changes: 1 addition & 1 deletion spec/migration_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ baz;")
end

def statements(migration, direction)
migration.statements(direction).map { |stmt| stmt.strip }
migration.statements(direction).map(&.strip)
end
49 changes: 49 additions & 0 deletions spec/runner_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "./spec_helper"
require "sqlite3"

Spectator.describe Micrate::Runner do
it "runs, rolls back, and re-runs migrations from an explicit directory" do
root = File.join(Dir.tempdir, "micrate-runner-#{Process.pid}-#{Random.rand(1_000_000)}")
migrations_dir = File.join(root, "custom_migrations")
database_path = File.join(root, "pets.db")
database_url = "sqlite3:#{database_path}"

begin
Dir.mkdir_p(migrations_dir)
migration = <<-SQL
-- +micrate Up
CREATE TABLE pets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);

-- +micrate Down
DROP TABLE pets;
SQL
File.write(
File.join(migrations_dir, "20260811120000000_create_pets.sql"),
migration
)

runner = Micrate::Runner.new(database_url, migrations_dir)

runner.connect { |db| runner.up(db) }
::DB.open(database_url) do |db|
db.scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'pets'").should eq(1_i64)
end

runner.connect { |db| runner.down(db) }
::DB.open(database_url) do |db|
db.scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'pets'").should eq(0_i64)
end

runner.connect { |db| runner.up(db) }
runner.connect { |db| runner.redo(db) }
::DB.open(database_url) do |db|
db.scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'pets'").should eq(1_i64)
end
ensure
FileUtils.rm_r(root) if Dir.exists?(root)
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "log"
require "file_utils"
require "spectator"
require "spectator/should"
require "../src/micrate"
Expand Down
2 changes: 1 addition & 1 deletion src/micrate-cli.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require "log"
{% for db in %w(pg mysql sqlite3) %}
{% for db in %w[pg mysql sqlite3] %}
{% if file_exists?("lib/" + db) %}
require {{ db }}
{% end %}
Expand Down
Loading