cassandra-schema 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gems-test +2 -0
- data/.gitignore +3 -0
- data/Dockerfile.test +19 -0
- data/LICENSE +21 -0
- data/README.md +124 -0
- data/cassandra-schema.gemspec +15 -0
- data/circle.yml +16 -0
- data/examples/migration.rb +31 -0
- data/lib/.gitkeep +0 -0
- data/lib/cassandra-schema.rb +1 -0
- data/lib/cassandra-schema/migration.rb +53 -0
- data/lib/cassandra-schema/migrator.rb +141 -0
- data/lib/cassandra-schema/version.rb +3 -0
- data/ruby-version.sample +1 -0
- data/scripts/test +11 -0
- data/scripts/test-run +24 -0
- data/test/docker-compose.yml +21 -0
- data/test/migration_test.rb +33 -0
- data/test/migrations_test.rb +60 -0
- data/test/migrator_test.rb +159 -0
- data/test/support/connections.rb +51 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 56d06dbe1704f5dde353807ef36e71c49b6cd431
|
4
|
+
data.tar.gz: 3010c1e9b8556c0c8556ab95007299c4d925f0cc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a769484a9b5ea4d7edd06a783e1e2f29176ff0fbe4ba2ade021ac6c3829562f2591282ef3f55518d681c5afd21b08fcfaf2e3cf654da6fb76d5e805b569f2be4
|
7
|
+
data.tar.gz: dca8a92e44664c35676cb269d373306f11f63e563802d3f3bc802f5d5fd4d802ca8404c5a83fdf0c46d73bf91f706f3aa9f5a23d85ab911922ba45ea8de30f61
|
data/.gems-test
ADDED
data/.gitignore
ADDED
data/Dockerfile.test
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
FROM ruby:2.4.1-alpine
|
2
|
+
|
3
|
+
# Tell pry to use `more` as the pager because the installed version of `less`
|
4
|
+
# does not support passing the `-R` which is used by pry.
|
5
|
+
ENV PAGER="more"
|
6
|
+
|
7
|
+
# Install runtime dependencies
|
8
|
+
RUN apk add --update libcurl
|
9
|
+
|
10
|
+
# Install gem dependency specifications.
|
11
|
+
COPY .gems-test /tmp/
|
12
|
+
|
13
|
+
# Install gem build dependencies, install gems, purge gem build dependencies.
|
14
|
+
RUN apk add --update -t gem-build-deps libffi-dev make gcc g++ musl-dev && \
|
15
|
+
gem install dep:1.1.0 && \
|
16
|
+
cd /tmp && \
|
17
|
+
dep -f .gems install && dep -f .gems && rm .gems && \
|
18
|
+
dep -f .gems-test install && dep -f .gems-test && rm .gems-test && \
|
19
|
+
apk del --purge gem-build-deps
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Lautaro Orazi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# Cassandra Schema [](https://circleci.com/gh/tarolandia/cassandra-schema/tree/master)
|
2
|
+
|
3
|
+
Simple reversible schema migrations for cassandra.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
CassandraSchema uses a DSL via the `CassandraSchema.migration(version)` method. A migration must have an `up` block with the changes you want to apply to the schema, and a `down` block reversing the change made by `up`.
|
8
|
+
|
9
|
+
Use `execute` inside `up` and `down` to run the queries that will modify the schema.
|
10
|
+
|
11
|
+
|
12
|
+
Here is an example of a migration file:
|
13
|
+
|
14
|
+
```cql
|
15
|
+
require "cassandra-schema"
|
16
|
+
|
17
|
+
CassandraSchema.migration(1) do
|
18
|
+
up do
|
19
|
+
execute <<~CQL
|
20
|
+
CREATE TABLE table_by_name (
|
21
|
+
id uuid,
|
22
|
+
name text,
|
23
|
+
description text,
|
24
|
+
PRIMARY KEY (id, name)
|
25
|
+
) WITH CLUSTERING ORDER BY (name ASC)
|
26
|
+
CQL
|
27
|
+
|
28
|
+
execute <<~CQL
|
29
|
+
CREATE MATERIALIZED VIEW table_by_description AS
|
30
|
+
SELECT
|
31
|
+
id,
|
32
|
+
name,
|
33
|
+
description
|
34
|
+
FROM table_by_name
|
35
|
+
WHERE id IS NOT NULL
|
36
|
+
AND name IS NOT NULL
|
37
|
+
AND description IS NOT NULL
|
38
|
+
PRIMARY KEY (id, description, name)
|
39
|
+
WITH CLUSTERING ORDER BY (description ASC, name ASC)
|
40
|
+
CQL
|
41
|
+
end
|
42
|
+
|
43
|
+
down do
|
44
|
+
execute "DROP MATERIALIZED VIEW table_by_description"
|
45
|
+
execute "DROP TABLE table_by_name"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
## Running Migrations
|
51
|
+
|
52
|
+
Once you defined your migrations, you can use `CassandraSchema::Migrator` to run them.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
require "cassandra-schema/migrator"
|
56
|
+
|
57
|
+
migrator = CassandraSchema::Migrator.new(
|
58
|
+
connection: CONN, # any connection object implementing `execute` method
|
59
|
+
migrations: CassandraSchema.migrations, # list of migrations
|
60
|
+
logger: Logger.new, # any logger object implementing `info` and `error` methods
|
61
|
+
)
|
62
|
+
```
|
63
|
+
|
64
|
+
Migrate to lastest version:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
migrator.migrate
|
68
|
+
```
|
69
|
+
|
70
|
+
Migrate to certain version:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
migrator.migrate(2)
|
74
|
+
```
|
75
|
+
|
76
|
+
CassandraSchema tracks which migrations you have already run.
|
77
|
+
|
78
|
+
## Installation
|
79
|
+
|
80
|
+
You can install it using rubygems.
|
81
|
+
|
82
|
+
```
|
83
|
+
gem install cassandra-schema
|
84
|
+
```
|
85
|
+
|
86
|
+
## How to collaborate
|
87
|
+
|
88
|
+
If you find a bug or want to collaborate with the code, you can:
|
89
|
+
|
90
|
+
* Report issues trhough the issue tracker
|
91
|
+
* Fork the repository into your own account and submit a Pull Request
|
92
|
+
|
93
|
+
## Credits
|
94
|
+
|
95
|
+
These people have donated time to reviewing and improving this gem:
|
96
|
+
|
97
|
+
* [Ary Borenszweig](https://github.com/asterite)
|
98
|
+
* [Joe Eli McIlvain](https://github.com/jemc)
|
99
|
+
* [Lucas Tolchinsky](https://github.com/tonchis)
|
100
|
+
* [Matías Flores](https://github.com/matflores)
|
101
|
+
|
102
|
+
## Copyright
|
103
|
+
|
104
|
+
MIT License
|
105
|
+
|
106
|
+
Copyright (c) 2017 Lautaro Orazi
|
107
|
+
|
108
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
109
|
+
of this software and associated documentation files (the "Software"), to deal
|
110
|
+
in the Software without restriction, including without limitation the rights
|
111
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
112
|
+
copies of the Software, and to permit persons to whom the Software is
|
113
|
+
furnished to do so, subject to the following conditions:
|
114
|
+
|
115
|
+
The above copyright notice and this permission notice shall be included in all
|
116
|
+
copies or substantial portions of the Software.
|
117
|
+
|
118
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
119
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
120
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
121
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
122
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
123
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
124
|
+
SOFTWARE.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "cassandra-schema"
|
5
|
+
s.version = "0.1.0"
|
6
|
+
s.summary = "Cassandra schema migrations"
|
7
|
+
s.license = "MIT"
|
8
|
+
s.description = "Simple reversible schema migrations for Cassandra."
|
9
|
+
s.authors = ["Lautaro Orazi"]
|
10
|
+
s.email = ["orazile@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/tarolandia/cassandra-schema"
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
end
|
data/circle.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
CassandraSchema.migration(1) do
|
2
|
+
up do
|
3
|
+
execute <<~CQL
|
4
|
+
CREATE TABLE table_by_name (
|
5
|
+
id uuid,
|
6
|
+
name text,
|
7
|
+
description text,
|
8
|
+
PRIMARY KEY (id, name)
|
9
|
+
) WITH CLUSTERING ORDER BY (name ASC)
|
10
|
+
CQL
|
11
|
+
|
12
|
+
execute <<~CQL
|
13
|
+
CREATE MATERIALIZED VIEW table_by_description AS
|
14
|
+
SELECT
|
15
|
+
id,
|
16
|
+
name,
|
17
|
+
description
|
18
|
+
FROM table_by_name
|
19
|
+
WHERE id IS NOT NULL
|
20
|
+
AND name IS NOT NULL
|
21
|
+
AND description IS NOT NULL
|
22
|
+
PRIMARY KEY (id, description, name)
|
23
|
+
WITH CLUSTERING ORDER BY (description ASC, name ASC)
|
24
|
+
CQL
|
25
|
+
end
|
26
|
+
|
27
|
+
down do
|
28
|
+
execute "DROP MATERIALIZED VIEW table_by_description"
|
29
|
+
execute "DROP TABLE table_by_name"
|
30
|
+
end
|
31
|
+
end
|
data/lib/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "cassandra-schema/migration"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module CassandraSchema
|
2
|
+
@@migrations = {}
|
3
|
+
|
4
|
+
def self.migrations
|
5
|
+
@@migrations
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.reset_migrations!
|
9
|
+
@@migrations = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.migration(version, &block)
|
13
|
+
fail "Migration version #{version} is already registered" if @@migrations[version]
|
14
|
+
|
15
|
+
@@migrations[version] = MigrationDSL.new(&block).migration
|
16
|
+
end
|
17
|
+
|
18
|
+
class MigrationDSL
|
19
|
+
attr_reader :migration
|
20
|
+
|
21
|
+
def initialize(&block)
|
22
|
+
@migration = Migration.new
|
23
|
+
instance_eval(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def up(&block)
|
27
|
+
@buffer = []
|
28
|
+
@migration.set_commands(:up, block.call)
|
29
|
+
end
|
30
|
+
|
31
|
+
def down(&block)
|
32
|
+
@buffer = []
|
33
|
+
@migration.set_commands(:down, block.call)
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute(command)
|
37
|
+
@buffer << command
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Migration
|
42
|
+
def commands
|
43
|
+
@commands ||= {
|
44
|
+
up: [],
|
45
|
+
down: [],
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_commands(key, _commands)
|
50
|
+
commands[key] = _commands
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require_relative "migration"
|
2
|
+
|
3
|
+
module CassandraSchema
|
4
|
+
class Migrator
|
5
|
+
attr_reader :connection, :current_version
|
6
|
+
|
7
|
+
def initialize(connection:, migrations:, logger: Logger.new(STDOUT))
|
8
|
+
@connection = connection
|
9
|
+
@logger = logger
|
10
|
+
@migrations = migrations
|
11
|
+
|
12
|
+
generate_migrator_schema!
|
13
|
+
|
14
|
+
@current_version = get_current_version || init_versioning
|
15
|
+
end
|
16
|
+
|
17
|
+
def migrate(target = nil)
|
18
|
+
target ||= @migrations.keys.max || 0
|
19
|
+
|
20
|
+
@logger.info "Running migrations..."
|
21
|
+
|
22
|
+
if target == current_version || @migrations.empty?
|
23
|
+
@logger.info "Nothing to migrate."
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
if target > current_version
|
29
|
+
# excludes current version's up
|
30
|
+
(current_version + 1).upto(target) do |next_version|
|
31
|
+
migrate_to(next_version, :up)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# includes current version's :down
|
35
|
+
# excludes target version's :down
|
36
|
+
current_version.downto(target + 1) do |version|
|
37
|
+
migrate_to(version, :down)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@logger.info "Current version: #{current_version}"
|
42
|
+
@logger.info "Done!"
|
43
|
+
rescue => ex
|
44
|
+
@logger.info "Failed migrating all files. Current schema version: #{@current_version}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def generate_migrator_schema!
|
51
|
+
result = @connection.execute <<~CQL
|
52
|
+
CREATE TABLE IF NOT EXISTS schema_information (
|
53
|
+
name VARCHAR,
|
54
|
+
value VARCHAR,
|
55
|
+
PRIMARY KEY (name)
|
56
|
+
);
|
57
|
+
CQL
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_current_version
|
61
|
+
result = @connection.execute <<~CQL
|
62
|
+
SELECT value FROM schema_information WHERE name = 'version'
|
63
|
+
CQL
|
64
|
+
|
65
|
+
result.rows.any? && result.rows.first["value"].to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def init_versioning
|
69
|
+
@connection.execute <<~CQL
|
70
|
+
INSERT INTO schema_information(name, value) VALUES('version', '0')
|
71
|
+
CQL
|
72
|
+
|
73
|
+
0
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_version(target)
|
77
|
+
@connection.execute(
|
78
|
+
<<~CQL,
|
79
|
+
UPDATE schema_information SET value = ? WHERE name = 'version'
|
80
|
+
CQL
|
81
|
+
arguments: [target.to_s]
|
82
|
+
)
|
83
|
+
|
84
|
+
@current_version = target
|
85
|
+
end
|
86
|
+
|
87
|
+
def migrate_to(target, direction)
|
88
|
+
new_version = direction == :up ? target : target - 1
|
89
|
+
@logger.info "Migrating to version #{new_version}"
|
90
|
+
|
91
|
+
unless @migrations[target]
|
92
|
+
@logger.info "Missing migration with version #{target}"
|
93
|
+
fail
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get commands list
|
97
|
+
commands = @migrations.fetch(target).commands.fetch(direction)
|
98
|
+
index = 0
|
99
|
+
|
100
|
+
commands.each do |command|
|
101
|
+
unless execute_command(command)
|
102
|
+
message = "Failed migrating to version #{target}."
|
103
|
+
|
104
|
+
if index > 0
|
105
|
+
message += " Recovering..."
|
106
|
+
|
107
|
+
#recover
|
108
|
+
recover_commands = @migrations
|
109
|
+
.fetch(target)
|
110
|
+
.commands
|
111
|
+
.fetch(direction == :up ? :down : :up)
|
112
|
+
.last(index)
|
113
|
+
|
114
|
+
results = recover_commands.map { |cmd| execute_command(cmd) }
|
115
|
+
|
116
|
+
message += results.all? ? "Ok." : "Failed."
|
117
|
+
end
|
118
|
+
|
119
|
+
@logger.info message
|
120
|
+
|
121
|
+
fail
|
122
|
+
end
|
123
|
+
|
124
|
+
index += 1
|
125
|
+
end
|
126
|
+
|
127
|
+
update_version(new_version)
|
128
|
+
end
|
129
|
+
|
130
|
+
def execute_command(command)
|
131
|
+
begin
|
132
|
+
@connection.execute command
|
133
|
+
true
|
134
|
+
rescue => ex
|
135
|
+
@logger.error ex.message
|
136
|
+
false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
data/ruby-version.sample
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/scripts/test
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
|
3
|
+
# Move to the base project directory if not there already.
|
4
|
+
cd "$(dirname "$0")"/..
|
5
|
+
|
6
|
+
# Make sure the latest test image is built.
|
7
|
+
docker build -t cassandra/schema:test -f Dockerfile.test .
|
8
|
+
|
9
|
+
# Run the given tests in the test container.
|
10
|
+
docker-compose -p cassandraschematest -f test/docker-compose.yml run --rm test \
|
11
|
+
sh -c "scripts/test-run ${@}"
|
data/scripts/test-run
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
usage = <<-USAGE
|
4
|
+
Usage:
|
5
|
+
|
6
|
+
scripts/test-run --help # Show this help text
|
7
|
+
scripts/test-run # Run all tests in test/**/*.rb
|
8
|
+
scripts/test-run DIR # Run all tests in test/DIR/**/*.rb
|
9
|
+
scripts/test-run FILE1.rb FILE2.rb # Run all tests in FILE1.rb and FILE2.rb
|
10
|
+
USAGE
|
11
|
+
|
12
|
+
TEST_DIR = File.expand_path("../test", File.dirname(__FILE__))
|
13
|
+
TEST_SEED = ENV.fetch("TEST_SEED", rand(99999)).to_i
|
14
|
+
TEST_FILES = case ARGV.first
|
15
|
+
when "--help" then warn usage; exit 0
|
16
|
+
when nil then Dir["#{TEST_DIR}/**/*.rb"]
|
17
|
+
when %r(\A[\w/]+\z) then Dir["#{TEST_DIR}/#{ARGV.first}/**/*.rb"]
|
18
|
+
else ARGV.map { |file| "#{TEST_DIR}/#{file}" }
|
19
|
+
end
|
20
|
+
|
21
|
+
puts "Loading test files with TEST_SEED=#{TEST_SEED}"
|
22
|
+
puts
|
23
|
+
|
24
|
+
TEST_FILES.shuffle(random: Random.new(TEST_SEED)).each { |rb| require rb }
|
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
version: '2'
|
3
|
+
|
4
|
+
services:
|
5
|
+
cassandra:
|
6
|
+
image: cassandra:3.11
|
7
|
+
|
8
|
+
test:
|
9
|
+
command: tail -f /dev/null # wait for tests to be run via docker exec
|
10
|
+
image: cassandra/schema:test
|
11
|
+
working_dir: /opt/test
|
12
|
+
volumes:
|
13
|
+
- ${PWD}:/opt/test
|
14
|
+
links:
|
15
|
+
- cassandra
|
16
|
+
environment:
|
17
|
+
CASSANDRA_USERNAME: cassandra
|
18
|
+
CASSANDRA_PASSWORD: cassandra
|
19
|
+
CASSANDRA_HOSTS: cassandra
|
20
|
+
CASSANDRA_PORT: 9042
|
21
|
+
CASSANDRA_KEYSPACE: cassandra_schema_test
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
|
3
|
+
require_relative "../lib/cassandra-schema/migration"
|
4
|
+
|
5
|
+
describe "CassandraSchema::Migration" do
|
6
|
+
before do
|
7
|
+
@migration = CassandraSchema::Migration.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "returns empty commands hash" do
|
11
|
+
assert_equal(
|
12
|
+
{
|
13
|
+
up: [],
|
14
|
+
down: [],
|
15
|
+
},
|
16
|
+
@migration.commands
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "sets :up commands" do
|
21
|
+
commands = ["CQL command 1", "CQL command 2"]
|
22
|
+
@migration.set_commands(:up, commands)
|
23
|
+
|
24
|
+
assert_equal commands, @migration.commands.fetch(:up)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sets :down commands" do
|
28
|
+
commands = ["CQL command 1", "CQL command 2"]
|
29
|
+
@migration.set_commands(:down, commands)
|
30
|
+
|
31
|
+
assert_equal commands, @migration.commands.fetch(:down)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
|
3
|
+
require_relative "../lib/cassandra-schema/migration"
|
4
|
+
|
5
|
+
describe "CassandraSchema" do
|
6
|
+
before do
|
7
|
+
CassandraSchema.reset_migrations!
|
8
|
+
|
9
|
+
CassandraSchema.migration(1) do; end
|
10
|
+
CassandraSchema.migration(2) do; end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "registers migrations" do
|
14
|
+
assert_equal 2, CassandraSchema.migrations.size
|
15
|
+
|
16
|
+
CassandraSchema.migrations.each_with_index do |(version, migration), index|
|
17
|
+
assert_equal index + 1, version
|
18
|
+
assert_instance_of CassandraSchema::Migration, migration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "resets migrations" do
|
23
|
+
CassandraSchema.reset_migrations!
|
24
|
+
|
25
|
+
assert_equal 0, CassandraSchema.migrations.size
|
26
|
+
end
|
27
|
+
|
28
|
+
it "fails adding migration with the same version number" do
|
29
|
+
assert_raises(RuntimeError, "Migration version 2 is already registered") do
|
30
|
+
CassandraSchema.migration(2) do; end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "DSL" do
|
35
|
+
it "generates migration with up and down commands" do
|
36
|
+
CassandraSchema.migration(3) do
|
37
|
+
up do
|
38
|
+
execute "CQL command 1"
|
39
|
+
execute "CQL command 2"
|
40
|
+
end
|
41
|
+
|
42
|
+
down do
|
43
|
+
execute "CQL revert command 2"
|
44
|
+
execute "CQL revert command 1"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
migration = CassandraSchema.migrations[3]
|
49
|
+
|
50
|
+
assert migration
|
51
|
+
assert_equal(
|
52
|
+
{
|
53
|
+
up: ["CQL command 1", "CQL command 2"],
|
54
|
+
down: ["CQL revert command 2", "CQL revert command 1"],
|
55
|
+
},
|
56
|
+
migration.commands
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
|
3
|
+
require_relative "support/connections"
|
4
|
+
require_relative "../lib/cassandra-schema/migrator"
|
5
|
+
|
6
|
+
class FakeLogger
|
7
|
+
attr_reader :stdout
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@stdout = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def info(value)
|
14
|
+
@stdout << value
|
15
|
+
end
|
16
|
+
|
17
|
+
def error(value)
|
18
|
+
@stdout << value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
CONN = Connections::Cassandra.create_with_retries
|
23
|
+
CONN.execute <<~CQL
|
24
|
+
CREATE KEYSPACE IF NOT EXISTS cassandra_schema_migrator
|
25
|
+
WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
|
26
|
+
CQL
|
27
|
+
|
28
|
+
CONN.execute "USE cassandra_schema_migrator"
|
29
|
+
|
30
|
+
describe "CassandraSchema::Migrator" do
|
31
|
+
before do
|
32
|
+
CONN.execute "DROP TABLE IF EXISTS schema_information"
|
33
|
+
CONN.execute "DROP MATERIALIZED VIEW IF EXISTS table_by_description"
|
34
|
+
CONN.execute "DROP TABLE IF EXISTS table_by_name"
|
35
|
+
|
36
|
+
CassandraSchema.reset_migrations!
|
37
|
+
|
38
|
+
CassandraSchema.migration(1) do
|
39
|
+
up do
|
40
|
+
execute <<~CQL
|
41
|
+
CREATE TABLE table_by_name (
|
42
|
+
id uuid,
|
43
|
+
name text,
|
44
|
+
description text,
|
45
|
+
PRIMARY KEY (id, name)
|
46
|
+
) WITH CLUSTERING ORDER BY (name ASC)
|
47
|
+
CQL
|
48
|
+
|
49
|
+
execute <<~CQL
|
50
|
+
CREATE MATERIALIZED VIEW table_by_description AS
|
51
|
+
SELECT
|
52
|
+
id,
|
53
|
+
name,
|
54
|
+
description
|
55
|
+
FROM table_by_name
|
56
|
+
WHERE id IS NOT NULL
|
57
|
+
AND name IS NOT NULL
|
58
|
+
AND description IS NOT NULL
|
59
|
+
PRIMARY KEY (id, description, name)
|
60
|
+
WITH CLUSTERING ORDER BY (description ASC, name ASC)
|
61
|
+
CQL
|
62
|
+
end
|
63
|
+
|
64
|
+
down do
|
65
|
+
execute "DROP MATERIALIZED VIEW table_by_description"
|
66
|
+
execute "DROP TABLE table_by_name"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
CassandraSchema.migration(2) do
|
71
|
+
up do
|
72
|
+
execute "ALTER TABLE table_by_name ADD email text"
|
73
|
+
execute "ALTER TABLE table_by_name ADD alt_email text"
|
74
|
+
end
|
75
|
+
|
76
|
+
down do
|
77
|
+
execute "ALTER TABLE table_by_name DROP alt_email"
|
78
|
+
execute "ALTER TABLE table_by_name DROP email"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
@fake_logger = FakeLogger.new
|
83
|
+
end
|
84
|
+
|
85
|
+
it "initializes schema_information table if not exists" do
|
86
|
+
migrator = CassandraSchema::Migrator.new(
|
87
|
+
connection: CONN,
|
88
|
+
migrations: {},
|
89
|
+
logger: @fake_logger,
|
90
|
+
)
|
91
|
+
|
92
|
+
assert_equal 0, migrator.current_version
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "migrating up" do
|
96
|
+
it "migrates to last version" do
|
97
|
+
migrator = CassandraSchema::Migrator.new(
|
98
|
+
connection: CONN,
|
99
|
+
migrations: CassandraSchema.migrations,
|
100
|
+
logger: @fake_logger,
|
101
|
+
)
|
102
|
+
|
103
|
+
migrator.migrate
|
104
|
+
|
105
|
+
assert_equal 2, migrator.current_version
|
106
|
+
end
|
107
|
+
|
108
|
+
it "migrates to target version" do
|
109
|
+
migrator = CassandraSchema::Migrator.new(
|
110
|
+
connection: CONN,
|
111
|
+
migrations: CassandraSchema.migrations,
|
112
|
+
logger: @fake_logger,
|
113
|
+
)
|
114
|
+
|
115
|
+
migrator.migrate(1)
|
116
|
+
|
117
|
+
assert_equal 1, migrator.current_version
|
118
|
+
end
|
119
|
+
|
120
|
+
it "fails if there is a missing version" do
|
121
|
+
CassandraSchema.migration(4) do
|
122
|
+
up do
|
123
|
+
execute "ALTER TABLE users DROP email"
|
124
|
+
end
|
125
|
+
|
126
|
+
down do
|
127
|
+
execute "ALTER TABLE users ADD email text"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
migrator = CassandraSchema::Migrator.new(
|
132
|
+
connection: CONN,
|
133
|
+
migrations: CassandraSchema.migrations,
|
134
|
+
logger: @fake_logger,
|
135
|
+
)
|
136
|
+
|
137
|
+
migrator.migrate
|
138
|
+
assert_equal "Failed migrating all files. Current schema version: 2", @fake_logger.stdout.pop
|
139
|
+
assert_equal "Missing migration with version 3", @fake_logger.stdout.pop
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe "migrating down" do
|
144
|
+
before do
|
145
|
+
@migrator = CassandraSchema::Migrator.new(
|
146
|
+
connection: CONN,
|
147
|
+
migrations: CassandraSchema.migrations,
|
148
|
+
logger: @fake_logger,
|
149
|
+
)
|
150
|
+
# Start on version 2
|
151
|
+
@migrator.migrate
|
152
|
+
end
|
153
|
+
|
154
|
+
it "migrates to target version" do
|
155
|
+
@migrator.migrate(1)
|
156
|
+
assert_equal 1, @migrator.current_version
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "cassandra"
|
2
|
+
require "lz4-ruby"
|
3
|
+
|
4
|
+
module Connections
|
5
|
+
def self.create_with_retries(mod, *args)
|
6
|
+
waits = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
|
7
|
+
begin
|
8
|
+
Connections::Cassandra.create(*args)
|
9
|
+
|
10
|
+
rescue *mod::CONNECTION_ERRORS
|
11
|
+
wait = waits.shift
|
12
|
+
fail "Gave up on connecting to #{mod}" if wait.nil?
|
13
|
+
|
14
|
+
puts "Couldn't connect to #{mod}; retrying in #{wait} seconds"
|
15
|
+
sleep wait
|
16
|
+
retry
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Cassandra
|
21
|
+
DEFAULT_USERNAME = ENV.fetch("CASSANDRA_USERNAME")
|
22
|
+
DEFAULT_PASSWORD = ENV.fetch("CASSANDRA_PASSWORD")
|
23
|
+
DEFAULT_HOSTS = ENV.fetch("CASSANDRA_HOSTS").strip.split(" ")
|
24
|
+
DEFAULT_PORT = ENV.fetch("CASSANDRA_PORT", "9042").to_i
|
25
|
+
DEFAULT_COMPRESSION = :lz4
|
26
|
+
|
27
|
+
CONNECTION_ERRORS = [::Cassandra::Errors::NoHostsAvailable]
|
28
|
+
|
29
|
+
class << self
|
30
|
+
attr_accessor :current
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.create(options = {})
|
34
|
+
options = {
|
35
|
+
username: DEFAULT_USERNAME,
|
36
|
+
password: DEFAULT_PASSWORD,
|
37
|
+
hosts: DEFAULT_HOSTS,
|
38
|
+
port: DEFAULT_PORT,
|
39
|
+
compression: DEFAULT_COMPRESSION,
|
40
|
+
}.merge(options)
|
41
|
+
|
42
|
+
keyspace = options.delete(:keyspace)
|
43
|
+
|
44
|
+
::Cassandra.cluster(options).connect(keyspace)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.create_with_retries(*args)
|
48
|
+
Connections.create_with_retries(self, *args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cassandra-schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lautaro Orazi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-15 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Simple reversible schema migrations for Cassandra.
|
14
|
+
email:
|
15
|
+
- orazile@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".gems-test"
|
21
|
+
- ".gitignore"
|
22
|
+
- Dockerfile.test
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
- cassandra-schema.gemspec
|
26
|
+
- circle.yml
|
27
|
+
- examples/migration.rb
|
28
|
+
- lib/.gitkeep
|
29
|
+
- lib/cassandra-schema.rb
|
30
|
+
- lib/cassandra-schema/migration.rb
|
31
|
+
- lib/cassandra-schema/migrator.rb
|
32
|
+
- lib/cassandra-schema/version.rb
|
33
|
+
- ruby-version.sample
|
34
|
+
- scripts/test
|
35
|
+
- scripts/test-run
|
36
|
+
- test/docker-compose.yml
|
37
|
+
- test/migration_test.rb
|
38
|
+
- test/migrations_test.rb
|
39
|
+
- test/migrator_test.rb
|
40
|
+
- test/support/connections.rb
|
41
|
+
homepage: https://github.com/tarolandia/cassandra-schema
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata: {}
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 2.6.11
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Cassandra schema migrations
|
65
|
+
test_files: []
|