cass_schema 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +22 -0
- data/README.md +127 -0
- data/Rakefile +20 -0
- data/cass_schema.gemspec +26 -0
- data/lib/cass_schema/cluster.rb +17 -0
- data/lib/cass_schema/datastore.rb +109 -0
- data/lib/cass_schema/errors.rb +7 -0
- data/lib/cass_schema/runner.rb +87 -0
- data/lib/cass_schema/statement_loader.rb +24 -0
- data/lib/cass_schema/tasks/schema.rake +35 -0
- data/lib/cass_schema/version.rb +3 -0
- data/lib/cass_schema/yaml_helper.rb +14 -0
- data/lib/cass_schema.rb +18 -0
- data/test/cass_schema/datastore_test.rb +13 -0
- data/test/cass_schema/runner_test.rb +133 -0
- data/test/fixtures/invalid_datastore/schema.cql +11 -0
- data/test/fixtures/test2/schema.cql +12 -0
- data/test/fixtures/test_config.yml +16 -0
- data/test/fixtures/test_datastore/migrations/migration.cql +1 -0
- data/test/fixtures/test_datastore/schema.cql +14 -0
- data/test/test_helper.rb +18 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ed62cce9c48b24a103e72a5ac83df927bee4986a
|
4
|
+
data.tar.gz: 855847953f7f9c54933ab532b7524d81b80aac40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 90961f405aca7ae569903d92368e5f0fd1a36eb123852644734974956d8ea64d2a75e5b73e9064e4a5797e1ae71f6eda892fe3cd2084bd7745e660cf8f22bf04
|
7
|
+
data.tar.gz: 5cad1a2a5eff5824ce8817ca336ccc79e8fdcfc45121e9f99f4f30d6d9e92b0f27b7df20eef12ed850ea349307fabfa18da8190ac96a5c0ed1d5bc0d1eab4622
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in cass_migrations.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem "pry"
|
8
|
+
gem "awesome_print"
|
9
|
+
gem 'm', :git => 'git@github.com:ANorwell/m.git', :branch => 'minitest_5'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'minitest_should', :git => 'git@github.com:citrus/minitest_should.git'
|
14
|
+
gem "mocha"
|
15
|
+
end
|
16
|
+
|
17
|
+
gem 'cassandra-driver', :git => 'git@github.com:datastax/ruby-driver.git', :tag => 'v2.0.1'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Datto
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# CassSchema
|
2
|
+
|
3
|
+
A gem for managing multiple cassandra schemas across multiple clusters. CassSchema supports loading a table schema from scratch, as well as running a migration to apply a change to the schema. Unlike some other database migration tools, there is no stored state about which migrations have and have not been run -- a migration is simply a CQL statement to be run against the database.
|
4
|
+
|
5
|
+
CassSchema operations apply to multiple 'datastores'. A datastore is a cluster, keyspace pair, so there may be multiple schemas for a single cluster, but only a single schema for a given cluster plus keyspace.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'cass_schema', github: 'backupify/cass_schema'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install cass_schema
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Before usage, CassSchema must be initialized with a set of datastore objects, as well as a base directory where the schema definitions live:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
CassSchema::Runner.initialize(
|
29
|
+
datastores: CassSchema::YamlHelper.datastores('my/path/to/config.yml'),
|
30
|
+
schema_base_path: 'my/path/to/schema/root')
|
31
|
+
```
|
32
|
+
|
33
|
+
Here the datastores are a list of `CassSchema::DataStore` objects. These may be build manually, or loaded from a yaml file using the `CassSchema::YamlHelper`. CassSchema is intentionally agnostic about the source of these datastores.
|
34
|
+
|
35
|
+
The schema_base_path is a directory where the schema definitions and migrations live. The structure of this directory should be:
|
36
|
+
```
|
37
|
+
<schema_base_path>/<datastore_name>/schema.cql
|
38
|
+
<schema_base_path>/<datastore_name>/migrations/<migration1>.cql
|
39
|
+
<schema_base_path>/<datastore_name>/migrations/<migration2>.cql
|
40
|
+
...
|
41
|
+
|
42
|
+
```
|
43
|
+
|
44
|
+
The contents of each cql file should be a list of CQL statements. Comments starting with '#' are supported, and each statement should be separated by two new lines.
|
45
|
+
|
46
|
+
schema.cql and each migration should be maintained by hand. It is recommended that schema.cql contain a complete list of CQL statements for the entire, up-to-date schema.
|
47
|
+
|
48
|
+
An example yml config and datastore schema definition are in the `test/fixtures` directory.
|
49
|
+
|
50
|
+
### Rake tasks
|
51
|
+
|
52
|
+
The file `lib/cass_schema/tasks/schema.rake` defines several rake tasks for schema management. In a Rails project, these tasks are loaded when cass_schema is first loaded.
|
53
|
+
|
54
|
+
* `rake cass:schema:create_all` - create all schemas
|
55
|
+
* `rake cass:schema:drop_all` - drop all schemas
|
56
|
+
* `rake cass:schema:create[datastore]` - create the schema for the datastore named 'datastore'
|
57
|
+
* `rake cass:schema:drop[datastore]` - drop the schema for the datastore named 'datastore'
|
58
|
+
* `rake cass:schema:migrate[datastore, migration]` - run the migration 'migration' for the datastore 'datastore'
|
59
|
+
|
60
|
+
### Ruby Library Usage
|
61
|
+
|
62
|
+
To create all datastore schemas:
|
63
|
+
|
64
|
+
```
|
65
|
+
CassSchema::Runner.create_all
|
66
|
+
```
|
67
|
+
|
68
|
+
To create a particular datastore schemas:
|
69
|
+
|
70
|
+
```
|
71
|
+
CassSchema::Runner.create('datastore')
|
72
|
+
```
|
73
|
+
|
74
|
+
To drop all datastore schemas:
|
75
|
+
|
76
|
+
```
|
77
|
+
CassSchema::Runner.drop_all
|
78
|
+
```
|
79
|
+
|
80
|
+
To drop a particular datastore schemas:
|
81
|
+
|
82
|
+
```
|
83
|
+
CassSchema::Runner.drop('datastore')
|
84
|
+
```
|
85
|
+
|
86
|
+
To run a particular migration for a particular datastore:
|
87
|
+
|
88
|
+
```
|
89
|
+
CassSchema::Runner.migrate('datastore', 'migration')
|
90
|
+
```
|
91
|
+
|
92
|
+
Here the migration file 'migration.cql' should exist.
|
93
|
+
|
94
|
+
### Integration with other libraries
|
95
|
+
|
96
|
+
While `CassSchema::YamlLoader` provides ease of use, it is not required: the runner can be instantiated
|
97
|
+
with any list of `CassSchema::Datastore` objects. Here is an example manual set up:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
cluster = CassSchema::Cluster.build(hosts: ['127.0.0.1'], port: 9242)
|
101
|
+
replication = "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"
|
102
|
+
datastores = [
|
103
|
+
CassSchema::Datastore.build('test_datastore', cluster: cluster, keyspace: 'test_keyspace', replication: replication),
|
104
|
+
CassSchema::Datastore.build('test_datastore2', cluster: cluster, keyspace: 'test_keyspace2', replication: replication),
|
105
|
+
]
|
106
|
+
Runner.setup(datastores: datastores, schema_base_path: 'my/base/path')
|
107
|
+
```
|
108
|
+
|
109
|
+
This allows CassSchema to be integrated into other configuration schemes more easily.
|
110
|
+
|
111
|
+
Additionally, the global `CassSchema::Runner` object need not be used: An instance of `CassSchema::Runner` can be
|
112
|
+
constructed and used in the same manner as the global object:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# Accepts the same arguments as Runner#setup
|
116
|
+
runner = Runner.new(datastores: datastores, schema_base_path: 'my/base/path')
|
117
|
+
runner.create_all
|
118
|
+
```
|
119
|
+
|
120
|
+
|
121
|
+
## Contributing
|
122
|
+
|
123
|
+
1. Fork it ( https://github.com/backupify/cass_schema/fork )
|
124
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
125
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
126
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
127
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
7
|
+
t.libs.push 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
task :setup do
|
11
|
+
require 'bundler/setup'
|
12
|
+
Bundler.require(:default, :development)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
task :console => [:setup] do
|
17
|
+
Pry.start
|
18
|
+
end
|
19
|
+
|
20
|
+
task default: :test
|
data/cass_schema.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cass_schema/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "cass_schema"
|
8
|
+
spec.version = CassSchema::VERSION
|
9
|
+
spec.authors = ["Arron Norwell"]
|
10
|
+
spec.email = ["anorwell@gmail.com"]
|
11
|
+
spec.summary = %q{Manage Cassandra Schemas for multiple conceptual datastores.}
|
12
|
+
spec.description = %q{A gem for managing the schema multiple Cassandra instances}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
|
24
|
+
spec.add_dependency 'cassandra-driver'
|
25
|
+
spec.add_dependency "activesupport"
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'cassandra'
|
2
|
+
|
3
|
+
module CassSchema
|
4
|
+
# A struct representing a Cassandra cluster.
|
5
|
+
# @param [Cassandra::Cluster] connection to the cassandra cluster to be used.
|
6
|
+
Cluster = Struct.new(:connection) do
|
7
|
+
def self.build(hash)
|
8
|
+
l = hash.with_indifferent_access
|
9
|
+
|
10
|
+
if l[:hosts]
|
11
|
+
l[:connection] ||= Cassandra.cluster(hosts: l[:hosts], port: l[:port])
|
12
|
+
end
|
13
|
+
|
14
|
+
new(l[:connection])
|
15
|
+
end
|
16
|
+
end if !defined?(Cluster)
|
17
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require_relative 'errors'
|
2
|
+
require_relative 'statement_loader'
|
3
|
+
require_relative 'cluster'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
require 'active_support/core_ext/object'
|
6
|
+
|
7
|
+
module CassSchema
|
8
|
+
# A struct representing a datastore, composed of the following fields:
|
9
|
+
# @param [String] name of the datastore
|
10
|
+
# @param [CassSchema::Cluster] A list of hosts and a port representing the cluster.
|
11
|
+
# @param [String] keyspace
|
12
|
+
# @param [String] A string defining the options with which the keyspace should be created, e.g.:
|
13
|
+
# "{ 'class' : 'SimpleStrategy', 'replication_factor' : 3 }"
|
14
|
+
# @param [String] The name of the schema directory within the cass_schema directory, typically the same as the name.
|
15
|
+
# @param [String] Optional base directory to find the schema and migration files.
|
16
|
+
# statements are separated by two new lines, and a statement cannot have two newlines inside of it
|
17
|
+
# comments start with '#'
|
18
|
+
DataStore = Struct.new(:name, :cluster, :keyspace, :replication, :schema, :schema_base_path) do
|
19
|
+
|
20
|
+
attr_reader :logger
|
21
|
+
|
22
|
+
# Creates a datastore object from a hash containing the required keys
|
23
|
+
# @param [String] name of the data store
|
24
|
+
# @option [CassSchema::Cluster] :cluster
|
25
|
+
# @option [String] :schema, defines the schema directory used. Sefaults to the name if not given.
|
26
|
+
# @option [String] :keyspace
|
27
|
+
# @option [String] :replication
|
28
|
+
def self.build(name, hash)
|
29
|
+
l = hash.with_indifferent_access
|
30
|
+
schema = l[:schema] || name
|
31
|
+
new(name, l[:cluster], l[:keyspace], l[:replication], schema, l[:schema_base_path])
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates the datastore
|
35
|
+
def create
|
36
|
+
run_statement(create_keyspace, general_client)
|
37
|
+
create_statements.each { |statement| run_statement(statement, client) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Drops the datastore
|
41
|
+
def drop
|
42
|
+
run_statement(drop_keyspace, general_client)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Runs a given migration for this datastore
|
46
|
+
# @param migration_name [String] the name of the migration
|
47
|
+
def migrate(migration_name)
|
48
|
+
migration_statements(migration_name).each { |statement| run_statement(statement, client) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# A Cassava client connected to the cluster and keyspace with which this datastore is associated
|
52
|
+
# @return [Cassandra::Session]
|
53
|
+
def client
|
54
|
+
@client ||= cluster.connection.connect(keyspace)
|
55
|
+
end
|
56
|
+
|
57
|
+
# A Cassava client connected to the cluster with which this datastore is associated
|
58
|
+
# @return [Cassandra::Session]
|
59
|
+
def general_client
|
60
|
+
@general_client ||= cluster.connection.connect
|
61
|
+
end
|
62
|
+
|
63
|
+
# Internal method used by Runner to pass state into the datastore
|
64
|
+
def _setup(options = {})
|
65
|
+
self.schema_base_path ||= options[:schema_base_path]
|
66
|
+
@logger = options[:logger]
|
67
|
+
end
|
68
|
+
|
69
|
+
def schema_path
|
70
|
+
File.join(schema_base_path, schema, 'schema.cql')
|
71
|
+
end
|
72
|
+
|
73
|
+
def schema_base_path
|
74
|
+
self[:schema_base_path]
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def run_statement(statement, client)
|
80
|
+
log("CassSchema: #{statement}")
|
81
|
+
client.execute(statement)
|
82
|
+
rescue Cassandra::Errors::ConfigurationError
|
83
|
+
# Special case if we cannot create/drop the keyspace, do nothing
|
84
|
+
rescue => e
|
85
|
+
log(e, :error)
|
86
|
+
raise SchemaError.create(e, statement)
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_statements
|
90
|
+
StatementLoader.statements(schema_path)
|
91
|
+
end
|
92
|
+
|
93
|
+
def migration_statements(migration_name)
|
94
|
+
StatementLoader.statements(schema_base_path, schema, 'migrations', "#{migration_name}.cql")
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_keyspace
|
98
|
+
"CREATE KEYSPACE #{keyspace} with replication = #{replication}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def drop_keyspace
|
102
|
+
"DROP KEYSPACE #{keyspace}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def log(msg, level = :info)
|
106
|
+
logger.try { |l| l.send(level, msg) }
|
107
|
+
end
|
108
|
+
end if !defined?(DataStore)
|
109
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module CassSchema
|
2
|
+
class Runner
|
3
|
+
|
4
|
+
class DropCommandsNotAllowed < StandardError
|
5
|
+
def initialize(*_)
|
6
|
+
super('Drop commands have been disabled by the :disallow_drops option')
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :datastores, :schema_base_path, :logger, :cluster_builder, :disallow_drops
|
11
|
+
|
12
|
+
# Create a new Runner
|
13
|
+
# @option options [Array<CassSchema::Datastore>] :datastores - The list of datastore objects for which schemas
|
14
|
+
# will be managed.
|
15
|
+
# @option options [String] :schema_bath_path - The directory where schema definitions live. In a rails env,
|
16
|
+
# this defaults to <rails root>/cass_schema.
|
17
|
+
# @option options [#info|#error] :logger optional logger to use when creating schemas
|
18
|
+
# @option options [Boolean] :disallow_drops Defaults to false. If set to true,
|
19
|
+
# drop commands will raise an exception instead of executing the command.
|
20
|
+
def initialize(options = {})
|
21
|
+
options[:schema_base_path] ||= defined?(::Rails) ? File.join(::Rails.root, 'cass_schema') : nil
|
22
|
+
|
23
|
+
@datastores = options[:datastores]
|
24
|
+
@schema_base_path = options[:schema_base_path]
|
25
|
+
@logger = options[:logger]
|
26
|
+
@disallow_drops = options[:disallow_drops]
|
27
|
+
|
28
|
+
raise ":datastores is a required argument!" unless @datastores
|
29
|
+
|
30
|
+
@datastores.each { |ds| ds._setup(options) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create all schemas for all datastores
|
34
|
+
def create_all
|
35
|
+
datastores.each { |d| d.create }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Drop all schemas for all datastores
|
39
|
+
def drop_all
|
40
|
+
raise DropCommandsNotAllowed if disallow_drops
|
41
|
+
datastores.each { |d| d.drop }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create the schema for a particular datastore
|
45
|
+
# @param [String] datastore_name
|
46
|
+
def create(datastore_name)
|
47
|
+
datastore_lookup(datastore_name).create
|
48
|
+
end
|
49
|
+
|
50
|
+
# Drop the schema for a particular datastore
|
51
|
+
# @param [String] datastore_name
|
52
|
+
def drop(datastore_name)
|
53
|
+
raise DropCommandsNotAllowed if disallow_drops
|
54
|
+
datastore_lookup(datastore_name).drop
|
55
|
+
end
|
56
|
+
|
57
|
+
# Run a particular named migration for a datastore
|
58
|
+
# @param [String] datastore_name
|
59
|
+
# @param [String] migration_name
|
60
|
+
def migrate(datastore_name, migration_name)
|
61
|
+
datastore_lookup(datastore_name).migrate(migration_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find a datastore based on the datastore name
|
65
|
+
# @param datastore_name [String|Symbol] The datastore name
|
66
|
+
# @return [CassSchema::Datastore]
|
67
|
+
def datastore_lookup(datastore_name)
|
68
|
+
@datastore_lookup ||= Hash[datastores.map { |ds| [ds.name, ds] }]
|
69
|
+
@datastore_lookup[datastore_name.to_s] || (raise ArgumentError.new("CassSchema datastore #{datastore_name} not found"))
|
70
|
+
end
|
71
|
+
|
72
|
+
# The class methods for Runner are the same as the instance methods, which delegate to a singleton. To set up the
|
73
|
+
# singleton, call Runner#setup.
|
74
|
+
class << self
|
75
|
+
# (see Runner#initialize)
|
76
|
+
def setup(options = {})
|
77
|
+
@runner = Runner.new(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
(Runner.instance_methods - Object.instance_methods).each do |method|
|
81
|
+
define_method(method) do |*args|
|
82
|
+
@runner.send(method, *args)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CassSchema
|
2
|
+
class StatementLoader
|
3
|
+
class << self
|
4
|
+
def statements(*path_parts)
|
5
|
+
file_path = File.join(path_parts)
|
6
|
+
file = File.open(file_path).read
|
7
|
+
|
8
|
+
# Parse the individual CQL statements as a list from the file. To do so:
|
9
|
+
# - assume statements are separated by two new lines
|
10
|
+
# - strip comments and empty lines from each statement
|
11
|
+
# - remove statements that are empty
|
12
|
+
statements = file.split(/\n{2,}/).map do |statement|
|
13
|
+
statement
|
14
|
+
.split(/\n/)
|
15
|
+
.select { |l| l !~ /^\s*#/ }
|
16
|
+
.select { |l| l !~ /^\s*$/ }
|
17
|
+
.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
statements.select { |s| s.length > 0 }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
namespace :cass do
|
4
|
+
namespace :schema do
|
5
|
+
|
6
|
+
desc "Create the cassandra schema for all datastores"
|
7
|
+
task :create_all, [:datastore] => :environment do |t, args|
|
8
|
+
CassSchema::Runner.create_all
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Drop the cassandra schema for all datastores"
|
12
|
+
task :drop_all, [:datastore] => :environment do |t, args|
|
13
|
+
CassSchema::Runner.drop_all
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Create the cassandra schema for the specified datastore"
|
17
|
+
task :create, [:datastore] => :environment do |t, args|
|
18
|
+
raise ArgumentError.new('datastore argument required') unless args[:datastore]
|
19
|
+
CassSchema::Runner.create(args[:datastore])
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Drop the cassandra schema for the specified datastore"
|
23
|
+
task :drop, [:datastore] => :environment do |t, args|
|
24
|
+
raise ArgumentError.new('datastore argument required') unless args[:datastore]
|
25
|
+
CassSchema::Runner.drop(args[:datastore])
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Run the specified cassandra schema migration for the specified datastore"
|
29
|
+
task :migrate, [:datastore, :migration] => :environment do |t, args|
|
30
|
+
raise ArgumentError.new('datastore argument required') unless args[:datastore]
|
31
|
+
raise ArgumentError.new('migration argument required') unless args[:migration]
|
32
|
+
CassSchema::Runner.migrate(args[:datastore], args[:migration])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'datastore'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module CassSchema
|
5
|
+
class YamlHelper
|
6
|
+
def self.datastores(config_file)
|
7
|
+
config = YAML.load(File.read(config_file))
|
8
|
+
config['datastores'].map do |name, ds_config|
|
9
|
+
ds_config[:cluster] = Cluster.build(ds_config)
|
10
|
+
DataStore.build(name, ds_config)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/cass_schema.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'cass_schema/version'
|
2
|
+
require 'cass_schema/runner'
|
3
|
+
require 'cass_schema/datastore'
|
4
|
+
require 'cass_schema/yaml_helper'
|
5
|
+
|
6
|
+
module CassSchema; end
|
7
|
+
|
8
|
+
if defined?(::Rails)
|
9
|
+
module CassSchema
|
10
|
+
module Rails
|
11
|
+
class Railtie < ::Rails::Railtie
|
12
|
+
rake_tasks do
|
13
|
+
load "cass_schema/tasks/schema.rake"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
module CassSchema
|
4
|
+
class DataStoreTest < MiniTest::Should::TestCase
|
5
|
+
context '#build' do
|
6
|
+
should 'accept the schema base path as an option' do
|
7
|
+
schema_base_path = File.expand_path(__FILE__, '../')
|
8
|
+
ds = CassSchema::DataStore.build("hello", schema_base_path: schema_base_path)
|
9
|
+
assert_equal File.join(schema_base_path, 'hello', 'schema.cql'), ds.schema_path
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
module CassSchema
|
4
|
+
class RunnerTest < MiniTest::Should::TestCase
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@base_path = "#{File.dirname(__FILE__)}/../fixtures"
|
8
|
+
Runner.setup(datastores: YamlHelper.datastores(File.join(@base_path, 'test_config.yml')),
|
9
|
+
schema_base_path: @base_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
teardown do
|
13
|
+
Runner.drop_all unless @dont_drop_on_teardown
|
14
|
+
end
|
15
|
+
|
16
|
+
def tables_for_keyspace(keyspace)
|
17
|
+
create_client.execute('SELECT * FROM system.schema_columnfamilies').rows.to_a.select { |t| t["keyspace_name"] == keyspace}
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema_for_table(keyspace, table)
|
21
|
+
create_client.execute('SELECT * FROM system.schema_columns').rows.to_a.select do |t|
|
22
|
+
(t["keyspace_name"] == 'test_keyspace') && (t['columnfamily_name'] == table)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'creating a schema' do
|
27
|
+
should 'be able to create a datastore' do
|
28
|
+
Runner.create('test_datastore')
|
29
|
+
tables = tables_for_keyspace('test_keyspace')
|
30
|
+
assert_equal %w(test test2).to_set, tables.map { |t| t['columnfamily_name'] }.to_set
|
31
|
+
end
|
32
|
+
|
33
|
+
should 'raise a SchemaError and not execute subsequent schema commands when a schema contains errors' do
|
34
|
+
assert_raises(SchemaError) { Runner.create('invalid_datastore') }
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'raise a misisng file error if schema for a datastore does not exist' do
|
38
|
+
assert_raises(Errno::ENOENT) { Runner.create('missing_datastore') }
|
39
|
+
end
|
40
|
+
|
41
|
+
should 'raise an error if a datastore does not exist' do
|
42
|
+
assert_raises(ArgumentError) { Runner.create('nonexistent_datastore') }
|
43
|
+
end
|
44
|
+
|
45
|
+
should 'be able to create a schema for which the schema name differs from the datastore name' do
|
46
|
+
datastore = Runner.datastore_lookup('test_datastore')
|
47
|
+
datastore.schema = 'test2'
|
48
|
+
Runner.create('test_datastore')
|
49
|
+
tables = tables_for_keyspace('test_keyspace')
|
50
|
+
assert_equal %w(test_other test_other2).to_set, tables.map { |t| t['columnfamily_name'] }.to_set
|
51
|
+
|
52
|
+
datastore.schema = 'test_datastore'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'dropping a schema' do
|
57
|
+
should 'be able to drop a datastore' do
|
58
|
+
Runner.create('test_datastore')
|
59
|
+
Runner.drop('test_datastore')
|
60
|
+
tables = tables_for_keyspace('test_keyspace')
|
61
|
+
assert_equal [], tables.map(&:columnfamily_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
should 'raise an error if a datastore does not exist' do
|
65
|
+
assert_raises(ArgumentError) { Runner.drop('nonexistent_datastore') }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'running a migration' do
|
70
|
+
should 'be able to run a migration for a datastore' do
|
71
|
+
Runner.create('test_datastore')
|
72
|
+
Runner.migrate('test_datastore', 'migration')
|
73
|
+
schema = schema_for_table('test_datastore', 'test')
|
74
|
+
|
75
|
+
column = schema.find { |col| col['column_name'] == 'new_column'}
|
76
|
+
assert column
|
77
|
+
assert_equal 'org.apache.cassandra.db.marshal.Int32Type', column['validator']
|
78
|
+
end
|
79
|
+
|
80
|
+
should 'raise an error if a datastore does not exist' do
|
81
|
+
assert_raises(ArgumentError) { Runner.create('nonexistent_datastore') }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'custom setup' do
|
86
|
+
setup do
|
87
|
+
connection = Cassandra.cluster(hosts: %w(127.0.0.1), port: 9242)
|
88
|
+
cluster = Cluster.build(connection: connection)
|
89
|
+
datastores = [DataStore.build('test_datastore',
|
90
|
+
cluster: cluster,
|
91
|
+
keyspace: 'test_keyspace',
|
92
|
+
replication: "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }")]
|
93
|
+
@runner = Runner.new(datastores: datastores, schema_base_path: @base_path )
|
94
|
+
end
|
95
|
+
|
96
|
+
should 'be able to create a datastore' do
|
97
|
+
@runner.create('test_datastore')
|
98
|
+
tables = tables_for_keyspace('test_keyspace')
|
99
|
+
assert_equal %w(test test2).to_set, tables.map { |t| t['columnfamily_name'] }.to_set
|
100
|
+
end
|
101
|
+
|
102
|
+
should 'be able to run a migration' do
|
103
|
+
@runner.create('test_datastore')
|
104
|
+
@runner.migrate('test_datastore', 'migration')
|
105
|
+
schema = schema_for_table('test_datastore', 'test')
|
106
|
+
|
107
|
+
column = schema.find { |col| col['column_name'] == 'new_column'}
|
108
|
+
assert column
|
109
|
+
assert_equal 'org.apache.cassandra.db.marshal.Int32Type', column['validator']
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'disallowing drops' do
|
114
|
+
setup do
|
115
|
+
Runner.setup(datastores: YamlHelper.datastores(File.join(@base_path, 'test_config.yml')),
|
116
|
+
schema_base_path: @base_path,
|
117
|
+
disallow_drops: true
|
118
|
+
)
|
119
|
+
|
120
|
+
@dont_drop_on_teardown = true
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
should 'disallow drop_all if :disallow_drops is set' do
|
125
|
+
assert_raises(CassSchema::Runner::DropCommandsNotAllowed) { Runner.drop_all }
|
126
|
+
end
|
127
|
+
|
128
|
+
should 'disallow drop if :disallow_drops is set' do
|
129
|
+
assert_raises(CassSchema::Runner::DropCommandsNotAllowed) { Runner.drop('test_keyspace') }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
datastores:
|
2
|
+
test_datastore:
|
3
|
+
hosts: 127.0.0.1
|
4
|
+
port: 9242
|
5
|
+
keyspace: test_keyspace
|
6
|
+
replication: "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"
|
7
|
+
invalid_datastore:
|
8
|
+
hosts: 127.0.0.1
|
9
|
+
port: 9242
|
10
|
+
keyspace: test_keyspace_invalid
|
11
|
+
replication: "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"
|
12
|
+
missing_datastore:
|
13
|
+
hosts: 127.0.0.1
|
14
|
+
port: 9242
|
15
|
+
keyspace: test_keyspace_invalid
|
16
|
+
replication: "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"
|
@@ -0,0 +1 @@
|
|
1
|
+
ALTER TABLE test ADD new_column int;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# CREATE KEYSPACE test_keyspace with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }
|
2
|
+
|
3
|
+
CREATE TABLE test(
|
4
|
+
id text,
|
5
|
+
a text,
|
6
|
+
b text,
|
7
|
+
PRIMARY KEY(id, a))
|
8
|
+
|
9
|
+
# Comment
|
10
|
+
CREATE TABLE test2(
|
11
|
+
id text,
|
12
|
+
x text,
|
13
|
+
y text,
|
14
|
+
PRIMARY KEY(id, x))
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pry'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/should'
|
5
|
+
|
6
|
+
|
7
|
+
require 'cass_schema'
|
8
|
+
|
9
|
+
class Minitest::Should::TestCase
|
10
|
+
def self.xshould(*args)
|
11
|
+
puts "Disabled test: #{args}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_client
|
16
|
+
c = Cassandra.cluster(port: 9242)
|
17
|
+
c.connect
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cass_schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arron Norwell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-05-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: cassandra-driver
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: A gem for managing the schema multiple Cassandra instances
|
70
|
+
email:
|
71
|
+
- anorwell@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- cass_schema.gemspec
|
82
|
+
- lib/cass_schema.rb
|
83
|
+
- lib/cass_schema/cluster.rb
|
84
|
+
- lib/cass_schema/datastore.rb
|
85
|
+
- lib/cass_schema/errors.rb
|
86
|
+
- lib/cass_schema/runner.rb
|
87
|
+
- lib/cass_schema/statement_loader.rb
|
88
|
+
- lib/cass_schema/tasks/schema.rake
|
89
|
+
- lib/cass_schema/version.rb
|
90
|
+
- lib/cass_schema/yaml_helper.rb
|
91
|
+
- test/cass_schema/datastore_test.rb
|
92
|
+
- test/cass_schema/runner_test.rb
|
93
|
+
- test/fixtures/invalid_datastore/schema.cql
|
94
|
+
- test/fixtures/test2/schema.cql
|
95
|
+
- test/fixtures/test_config.yml
|
96
|
+
- test/fixtures/test_datastore/migrations/migration.cql
|
97
|
+
- test/fixtures/test_datastore/schema.cql
|
98
|
+
- test/test_helper.rb
|
99
|
+
homepage: ''
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
metadata: {}
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.4.8
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Manage Cassandra Schemas for multiple conceptual datastores.
|
123
|
+
test_files:
|
124
|
+
- test/cass_schema/datastore_test.rb
|
125
|
+
- test/cass_schema/runner_test.rb
|
126
|
+
- test/fixtures/invalid_datastore/schema.cql
|
127
|
+
- test/fixtures/test2/schema.cql
|
128
|
+
- test/fixtures/test_config.yml
|
129
|
+
- test/fixtures/test_datastore/migrations/migration.cql
|
130
|
+
- test/fixtures/test_datastore/schema.cql
|
131
|
+
- test/test_helper.rb
|