cass_schema 0.3.1
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/.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
|