Skip to content

Commit

Permalink
Merge pull request #7 from juanedi/crystal-db
Browse files Browse the repository at this point in the history
Use Crystal-DB API
  • Loading branch information
juanedi authored Dec 26, 2016
2 parents 627fb9a + 4038e57 commit 3c60d8a
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 76 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/doc/
/libs/
/.crystal/
/lib/
/bin/
/.shards/


# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock

66 changes: 30 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# micrate

Micrate is a database migration tool written in crystal.
Micrate is a database migration tool written in Crystal.

It is inspired by [goose](https://bitbucket.org/liamstask/goose/). Some code was ported from there too, so check it out.

This is still a work in progress!
Micrate currently supports migrations for Postgres, Mysql and SQLite3, but it should be easy to add support for any other database engine with an existing [crystal-db API](https://github.com/crystal-lang/crystal-db) driver.

## Installation
## Command line

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

Expand All @@ -15,32 +15,7 @@ $ brew tap juanedi/micrate
$ brew install micrate
```

To use the Crystal API, add this to your application's `shard.yml`:

```yaml
dependencies:
micrate:
github: juanedi/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 create a simple script like the following:

```crystal
#! /usr/bin/env crystal
require "micrate"
Micrate::Cli.run
```

...and use it just as the binary program (after `chmod +x`ing it):
```
$ bin/micrate dbversion
0
```
## Usage
Execute `micrate help` for usage instructions. Micrate will connect to the postgres database specified by the `PG_URL` environment variable. Support for other database engines and better configuration options is on the way!
Execute `micrate help` for usage instructions. Micrate will connect to the database specified by the `DB_URL` environment variable.

To create a new migration use the `create` subcommand. For example, `micrate create 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:

Expand Down Expand Up @@ -111,6 +86,32 @@ language plpgsql;
-- +micrate StatementEnd
```

## API

To use the Crystal API, add this to your application's `shard.yml`:

```yaml
dependencies:
micrate:
github: juanedi/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:

```crystal
#! /usr/bin/env crystal
#
# To build a standalone command line client, require the
# driver you wish to use and use `Micrate::Cli`.
#

require "micrate"
require "pg"

Micrate::DB.connection_url = "postgresql://..."
Micrate::Cli.run
```

## Contributing

1. Fork it ( https://github.com/juanedi/micrate/fork )
Expand All @@ -119,13 +120,6 @@ language plpgsql;
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request

## TODOs

* Support for other database engines (currently only postgres is supported)
* Use common crystal API for DB access
* Multiple environments (development, test, production)
* Crystal DSL for database migrations

## Contributors

- [juanedi](https://github.com/juanedi) - creator, maintainer
4 changes: 0 additions & 4 deletions bin/micrate

This file was deleted.

12 changes: 12 additions & 0 deletions examples/micrate
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#! /usr/bin/env crystal

#
# To build a standalone command line client, require the
# driver you wish to use and use `Micrate::Cli`.
#

require "../src/micrate"
require "pg"

Micrate::DB.connection_url = "postgresql://..."
Micrate::Cli.run
6 changes: 3 additions & 3 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: micrate
version: 0.1.0
version: 0.3.0

authors:
- Juan Edi <jedi11235@gmail.com>

dependencies:
pg:
github: will/crystal-pg
db:
github: crystal-lang/crystal-db
10 changes: 4 additions & 6 deletions src/micrate.cr
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ module Micrate
def self.migration_status(db) : Hash(Migration, Time?)
# ensure that migration table exists
dbversion(db)
migration_status(migrations_by_version.values, db)
migration_status(migrations_by_version.values, db)
end

def self.migration_status(migrations : Array(Migration), db) : Hash(Migration, Time?)
({} of Migration => Time?).tap do |ret|
migrations.each do |m|
Expand Down Expand Up @@ -127,7 +127,6 @@ module Micrate
end
end


private def self.previous_version(current, all_versions)
all_previous = all_versions.select { |version| version < current }
if !all_previous.empty?
Expand All @@ -154,8 +153,8 @@ module Micrate

def self.migration_plan(status : Hash(Migration, Time?), current : Int, target : Int, direction)
status = ({} of Int64 => Bool).tap do |h|
status.each { |migration, migrated_at| h[migration.version] = !migrated_at.nil? }
end
status.each { |migration, migrated_at| h[migration.version] = !migrated_at.nil? }
end

migration_plan(status, current, target, direction)
end
Expand Down Expand Up @@ -206,7 +205,6 @@ module Micrate
end

class UnorderedMigrationsException < Exception

getter :versions

def initialize(@versions : Array(Int64))
Expand Down
5 changes: 2 additions & 3 deletions src/micrate/cli.cr
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ Commands:
else
print_help
end
rescue e: UnorderedMigrationsException
rescue e : UnorderedMigrationsException
report_unordered_migrations(e.versions)
exit 1
rescue e: Exception
rescue e : Exception
puts e.message
exit 1
end
Expand All @@ -119,4 +119,3 @@ Commands:
end
end
end

48 changes: 27 additions & 21 deletions src/micrate/db.cr
Original file line number Diff line number Diff line change
@@ -1,61 +1,67 @@
require "pg"
require "db"
require "./db/*"

module Micrate
module DB
@@connection_url = ENV["PG_URL"]?
@@connection_url = ENV["DB_URL"]?

def self.connection_url
@@connection_url
end

def self.connection_url=(connection_url)
@@dialect = nil
@@connection_url = connection_url
end

def self.connect
if !@@connection_url
raise "No postgresql connection URL is configured. Please set the PG_URL environment variable."
end

PG.connect(@@connection_url.not_nil!)
validate_connection_url
::DB.connect(@@connection_url.not_nil!)
end

def self.connect(&block)
db = connect
begin
validate_connection_url
::DB.open @@connection_url.not_nil! do |db|
yield db
ensure
db.close
end
end

def self.get_versions_last_first_order(db)
db.exec({Int64, Bool}, "SELECT version_id, is_applied from micrate_db_version ORDER BY id DESC").rows
db.query_all "SELECT version_id, is_applied from micrate_db_version ORDER BY id DESC", as: {Int64, Bool}
end

def self.create_migrations_table(db)
db.exec("CREATE TABLE micrate_db_version (
id serial NOT NULL,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default now(),
PRIMARY KEY(id)
);")
dialect.query_create_migrations_table(db)
end

def self.record_migration(migration, direction, db)
is_applied = direction == :forward
db.exec("INSERT INTO micrate_db_version (version_id, is_applied) VALUES ($1, $2);", [migration.version, is_applied])
dialect.query_record_migration(migration, is_applied, db)
end

def self.exec(statement, db)
db.exec(statement)
end

def self.get_migration_status(migration, db) : Time?
rows = db.exec({Time, Bool}, "SELECT tstamp, is_applied FROM micrate_db_version WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1", [migration.version]).rows
rows = dialect.query_migration_status(migration, db)

if !rows.empty? && rows[0][1]
rows[0][0]
else
nil
end
end

private def self.dialect
validate_connection_url
@@dialect ||= Dialect.from_connection_url(@@connection_url.not_nil!)
end

private def self.validate_connection_url
if !@@connection_url
raise "No database connection URL is configured. Please set the DB_URL environment variable."
end
end
end
end
21 changes: 21 additions & 0 deletions src/micrate/db/dialect.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Micrate::DB
abstract class Dialect
abstract def query_create_migrations_table(db)
abstract def query_migration_status(migration, db)
abstract def query_record_migration(migration, is_applied, db)

def self.from_connection_url(connection_url : String)
uri = URI.parse(connection_url)
case uri.scheme
when "postgresql", "postgres"
Postgres.new
when "mysql"
Mysql.new
when "sqlite3"
Sqlite3.new
else
raise "Could not infer SQL dialect from connection url #{connection_url}"
end
end
end
end
21 changes: 21 additions & 0 deletions src/micrate/db/mysql.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Micrate::DB
class Mysql < Dialect
def query_create_migrations_table(db)
db.exec("CREATE TABLE micrate_db_version (
id serial NOT NULL,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default now(),
PRIMARY KEY(id)
);")
end

def query_migration_status(migration, db)
db.query_all "SELECT tstamp, is_applied FROM micrate_db_version WHERE version_id=? ORDER BY tstamp DESC LIMIT 1", migration.version, as: {Time, Bool}
end

def query_record_migration(migration, is_applied, db)
db.exec("INSERT INTO micrate_db_version (version_id, is_applied) VALUES (?, ?);", [migration.version, is_applied])
end
end
end
21 changes: 21 additions & 0 deletions src/micrate/db/postgres.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Micrate::DB
class Postgres < Dialect
def query_create_migrations_table(db)
db.exec("CREATE TABLE micrate_db_version (
id serial NOT NULL,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default now(),
PRIMARY KEY(id)
);")
end

def query_migration_status(migration, db)
db.query_all "SELECT tstamp, is_applied FROM micrate_db_version WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1", migration.version, as: {Time, Bool}
end

def query_record_migration(migration, is_applied, db)
db.exec("INSERT INTO micrate_db_version (version_id, is_applied) VALUES ($1, $2);", [migration.version, is_applied])
end
end
end
25 changes: 25 additions & 0 deletions src/micrate/db/sqlite3.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Micrate::DB
class Sqlite3 < Dialect
def query_create_migrations_table(db)
# The current sqlite drive implementation does not store timestamps in the same
# format as the ones autogenerated by sqlite.
#
# As a workaround, we create timestamps locally so that the driver decides timestamp
# formats when writing and reading.
db.exec("CREATE TABLE micrate_db_version (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version_id INTEGER NOT NULL,
is_applied INTEGER NOT NULL,
tstamp TIMESTAMP
);")
end

def query_migration_status(migration, db)
db.query_all "SELECT tstamp, is_applied FROM micrate_db_version WHERE version_id=? ORDER BY tstamp DESC LIMIT 1", migration.version, as: {Time, Bool}
end

def query_record_migration(migration, is_applied, db)
db.exec("INSERT INTO micrate_db_version (version_id, is_applied, tstamp) VALUES (?, ?, ?);", [migration.version, is_applied, Time.now])
end
end
end
2 changes: 1 addition & 1 deletion src/micrate/version.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Micrate
VERSION = "0.2.2"
VERSION = "0.3.0"
end

0 comments on commit 3c60d8a

Please sign in to comment.