cassie 1.0.0.beta.22 → 1.0.0.beta.24
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/cassie +59 -21
- data/lib/cassie/configuration/core.rb +1 -2
- data/lib/cassie/migration/README.md +141 -0
- data/lib/cassie/migration/configuration.rb +18 -0
- data/lib/cassie/migration/initialization.rb +70 -0
- data/lib/cassie/migration/queries/create_schema_keyspace_query.rb +17 -0
- data/lib/cassie/migration/queries/create_versions_table_query.rb +23 -0
- data/lib/cassie/migration/queries/insert_version_query.rb +23 -0
- data/lib/cassie/migration/queries/select_versions_query.rb +14 -0
- data/lib/cassie/migration/queries.rb +8 -0
- data/lib/cassie/migration/structure_dumper.rb +94 -0
- data/lib/cassie/migration/structure_loader.rb +24 -0
- data/lib/cassie/migration/version.rb +4 -0
- data/lib/cassie/migration.rb +30 -0
- data/lib/cassie/statements/README.md +23 -1
- data/lib/cassie/statements/execution/partition_linking/policy_methods.rb +3 -3
- data/lib/cassie/statements/execution/partition_linking.rb +2 -1
- data/lib/cassie/statements/execution/results/querying.rb +4 -0
- data/lib/cassie/statements/statement/assignment.rb +2 -2
- data/lib/cassie/statements/statement/conditions.rb +4 -2
- data/lib/cassie/statements/statement/limiting.rb +9 -1
- data/lib/cassie/statements/statement/relation.rb +2 -2
- data/lib/cassie/statements/statement.rb +12 -17
- data/lib/cassie/support/command_runner.rb +21 -29
- data/lib/cassie/support/statement_parser.rb +41 -0
- data/lib/cassie/support.rb +1 -0
- data/lib/cassie/testing/fake/prepared_statement.rb +15 -4
- data/lib/cassie/version.rb +1 -1
- data/lib/cassie.rb +1 -0
- metadata +27 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65b5763101f92f5797356a1081e0fee0219cd86a
|
4
|
+
data.tar.gz: ff6994cee1c3f6d04d4ff35f01bd2e59e4d50ac5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77986c9aeb90d02ef1fdb107d168312390099ac0094ebef092985b737436f7ad5f23ce97f5068e4b0ca27579431fed9d7c95a426318b4008052dd5c9ebbe1b60
|
7
|
+
data.tar.gz: 961f2603659b2d3a770425fa571ea715ebe12314b602cd21539af579fcd48b0ff48a11aaf45d3304c3ca2211ed0a27303f6b36b76972d64782eac0637470baab
|
data/bin/cassie
CHANGED
@@ -5,7 +5,7 @@ def start
|
|
5
5
|
runner = Cassie::Support::CommandRunner.new("cassandra")
|
6
6
|
puts("Starting Cassandra...")
|
7
7
|
runner.run
|
8
|
-
runner.fail unless runner.
|
8
|
+
runner.fail unless runner.completed?
|
9
9
|
|
10
10
|
if runner.output =~ /state jump to NORMAL/
|
11
11
|
puts "[#{green('✓')}] Cassandra Running"
|
@@ -17,8 +17,9 @@ end
|
|
17
17
|
|
18
18
|
def stop(kill_all=false)
|
19
19
|
runner = Cassie::Support::CommandRunner.new("ps", ["-awx"])
|
20
|
-
runner.run
|
21
|
-
|
20
|
+
runner.run
|
21
|
+
fail runner.failure_message unless runner.success?
|
22
|
+
|
22
23
|
cassandra_awx = runner.output.split("\n").grep(/cassandra/)
|
23
24
|
pids = cassandra_awx.map{ |p| p.split(' ').first.to_i }
|
24
25
|
|
@@ -75,20 +76,23 @@ def generate_config
|
|
75
76
|
end
|
76
77
|
|
77
78
|
def dump_structure
|
78
|
-
|
79
|
-
|
79
|
+
dumper = Cassie::Migration::StructureDumper.new
|
80
|
+
dumper.dump
|
81
|
+
puts "[#{green("✓")}] Cassandra schema written to #{dumper.destination_path}"
|
80
82
|
|
81
|
-
|
83
|
+
rescue => e
|
84
|
+
puts e.message
|
85
|
+
exit(1)
|
86
|
+
end
|
82
87
|
|
83
|
-
|
84
|
-
|
88
|
+
def load_structure
|
89
|
+
loader = Cassie::Migration::StructureLoader.new
|
90
|
+
loader.load
|
91
|
+
puts "[#{green("✓")}] Cassandra schema loaded from #{loader.source_path}"
|
85
92
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
f.write(runner.output)
|
90
|
-
end
|
91
|
-
puts "[✓] Cassandra schema written to #{opts[:destination_path]}"
|
93
|
+
rescue => e
|
94
|
+
puts e.message
|
95
|
+
exit(1)
|
92
96
|
end
|
93
97
|
|
94
98
|
def tail_log
|
@@ -105,16 +109,44 @@ def tail_log
|
|
105
109
|
runner.run!
|
106
110
|
end
|
107
111
|
|
108
|
-
|
109
|
-
|
110
|
-
|
112
|
+
# Use current schema to initialize versioned migrations
|
113
|
+
# * import current schema as initial migration
|
114
|
+
# * initialize cassie_schema keyspace and version table
|
115
|
+
# * insert initial version
|
116
|
+
# * dump structure
|
117
|
+
def initialize_migrations
|
118
|
+
Cassie::Migration.initialize_versioning
|
119
|
+
puts "[#{green("✓")}] Versioned migrations initialized. Current version: #{Cassie::Migration.version_number}"
|
120
|
+
rescue Cassie::Migration::AlreadyInitiailizedError
|
121
|
+
puts "[#{red('✘')}] Versioned migration metatdata already exists. Current version: #{Cassie::Migration.version_number}"
|
122
|
+
rescue => e
|
123
|
+
puts e.message
|
124
|
+
exit(1)
|
125
|
+
end
|
111
126
|
|
112
|
-
|
127
|
+
def schema_history
|
128
|
+
print_versions(Cassie::Migration.versions)
|
129
|
+
rescue Cassie::Migration::UninitializedError => e
|
130
|
+
puts red(e.message)
|
131
|
+
end
|
113
132
|
|
114
|
-
|
115
|
-
|
133
|
+
def schema_version
|
134
|
+
print_versions([Cassie::Migration.version])
|
135
|
+
rescue Cassie::Migration::UninitializedError => e
|
136
|
+
puts red(e.message)
|
137
|
+
end
|
116
138
|
|
117
|
-
|
139
|
+
def print_versions(versions)
|
140
|
+
require 'terminal-table'
|
141
|
+
members = [:version_number, :description, :migrator, :migrated_at]
|
142
|
+
titles = ['Version', 'Description', 'Migrated by', 'Migrated at']
|
143
|
+
table = Terminal::Table.new(headings: titles)
|
144
|
+
versions.each.with_index do |v, i|
|
145
|
+
v.version_number = "* #{v.version_number}" if i == 0
|
146
|
+
table.add_row(v.to_h.values_at(*members))
|
147
|
+
end
|
148
|
+
table.align_column(0, :right)
|
149
|
+
puts table
|
118
150
|
end
|
119
151
|
|
120
152
|
def white(message)
|
@@ -142,6 +174,12 @@ when "structure:dump"
|
|
142
174
|
dump_structure
|
143
175
|
when "structure:load"
|
144
176
|
load_structure
|
177
|
+
when /migrations?\:initialize/
|
178
|
+
initialize_migrations
|
179
|
+
when "schema:history"
|
180
|
+
schema_history
|
181
|
+
when "schema:version"
|
182
|
+
schema_version
|
145
183
|
else
|
146
184
|
puts red("`#{ARGV[0]}` is not a supported command.")
|
147
185
|
end
|
@@ -10,7 +10,6 @@ module Cassie::Configuration
|
|
10
10
|
|
11
11
|
def self.extended(extender)
|
12
12
|
extender.paths["cluster_configurations"] = "config/cassandra.yml"
|
13
|
-
extender.paths["schema_structure"] = "db/structure.cql"
|
14
13
|
end
|
15
14
|
|
16
15
|
def env
|
@@ -31,7 +30,7 @@ module Cassie::Configuration
|
|
31
30
|
|
32
31
|
def configurations=(val)
|
33
32
|
if val && defined?(@configuration)
|
34
|
-
puts "WARNING
|
33
|
+
puts "WARNING: Setting `configurations` will have no effect on what config is used right now. `#{self}.configuration` has previously been set explicitly and will be used instead."
|
35
34
|
end
|
36
35
|
@configurations = val
|
37
36
|
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# Cassie Migrations
|
2
|
+
|
3
|
+
Cassie provides versioned migrations similar to many existing adapters and frameworks.
|
4
|
+
|
5
|
+
Practically speaking, this gives your cluster schmea its own "version" and simplifies upgrading or downgrading the schema.
|
6
|
+
|
7
|
+
As such, Cassie uses semantic versioning, where a defined version describes all non-system keyspaces.
|
8
|
+
|
9
|
+
Major, minor, and patch versions are used, without support for semantic extensions (prerelase or metadata, ex: 1.0.0.beta).
|
10
|
+
|
11
|
+
### Schema Migrations
|
12
|
+
#### Creating a migration
|
13
|
+
|
14
|
+
```
|
15
|
+
cassie migration:create that_killer_feature --bump minor
|
16
|
+
=> 0.2.0 - migrations/000_002_000_that_killer_feature.rb
|
17
|
+
```
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# migrations/000_002_000_that_killer_feature.rb
|
21
|
+
|
22
|
+
Uses the excellend DSL from `cassandra_migrations`.
|
23
|
+
```
|
24
|
+
|
25
|
+
#### Executing migrations
|
26
|
+
|
27
|
+
```
|
28
|
+
cassie migrate
|
29
|
+
=> roll up to latest version
|
30
|
+
```
|
31
|
+
|
32
|
+
```
|
33
|
+
cassie migrate 0.2.0
|
34
|
+
=> roll up to 0.2.0 and stop
|
35
|
+
```
|
36
|
+
|
37
|
+
```
|
38
|
+
cassie migrate 0.1.9
|
39
|
+
=> roll back to 0.1.9 and stop
|
40
|
+
```
|
41
|
+
|
42
|
+
#### Rolling back
|
43
|
+
|
44
|
+
```
|
45
|
+
cassie rollback
|
46
|
+
=> roll back 1 version and stop
|
47
|
+
```
|
48
|
+
|
49
|
+
```
|
50
|
+
cassie rollback 3
|
51
|
+
=> roll back 3 versions and stop
|
52
|
+
```
|
53
|
+
|
54
|
+
#### Getting the current version
|
55
|
+
```
|
56
|
+
cassie schema:version
|
57
|
+
+---------+----------------+-------------+---------------------------+
|
58
|
+
| Version | Description | Migrated by | Migrated at |
|
59
|
+
+---------+----------------+-------------+---------------------------+
|
60
|
+
| * 0.1.0 | initial schema | eprothro | 2016-09-08 09:23:54 -0500 |
|
61
|
+
+---------+----------------+-------------+---------------------------+
|
62
|
+
```
|
63
|
+
|
64
|
+
#### Getting the version history
|
65
|
+
```
|
66
|
+
cassie schema:history
|
67
|
+
+---------+----------------+-------------+---------------------------+
|
68
|
+
| Version | Description | Migrated by | Migrated at |
|
69
|
+
+---------+----------------+-------------+---------------------------+
|
70
|
+
| * 0.2.0 | create users | serverbot | 2016-09-08 10:23:54 -0500 |
|
71
|
+
| * 0.1.0 | initial schema | eprothro | 2016-09-08 09:23:54 -0500 |
|
72
|
+
+---------+----------------+-------------+---------------------------+
|
73
|
+
```
|
74
|
+
|
75
|
+
### Data Migrations
|
76
|
+
|
77
|
+
####TODO:
|
78
|
+
|
79
|
+
Best practice is calling data massaging from the migration, implemented with nice OO elsewhere...
|
80
|
+
|
81
|
+
### Schema overrides per environment
|
82
|
+
|
83
|
+
####TODO:
|
84
|
+
|
85
|
+
Migrations define production use case. Development and/or testing use cases may have slightly different needs.
|
86
|
+
|
87
|
+
Configure keyspace and/or table properties per env that should override migrations.
|
88
|
+
|
89
|
+
Example of keyspace replication settings.
|
90
|
+
|
91
|
+
Make sure it is clear what the schema dump is (with or without overrides)
|
92
|
+
|
93
|
+
### Multiple Keyspaces
|
94
|
+
|
95
|
+
####TODO:
|
96
|
+
|
97
|
+
Don't agree with different ENVs for managing multiple keyspaces. That assumes keyspaces align with domains.
|
98
|
+
|
99
|
+
Example of counter tables in a separate keyspace for higher replication to make read pressure lower.
|
100
|
+
|
101
|
+
Manage multiple keyspaces in configruation.
|
102
|
+
|
103
|
+
Configuration defaults to simple replication with durable writes.
|
104
|
+
|
105
|
+
Example of changing replication for production and how overrides keep dev working.
|
106
|
+
|
107
|
+
### Application Growth
|
108
|
+
|
109
|
+
####TODO:
|
110
|
+
|
111
|
+
This works for starting out, single app, single dev.
|
112
|
+
|
113
|
+
This also scales well to multiple apps or microservices, and multiple teams (ops, etc.), managing and depending on a central repository of migrations with a semantically versioned database schema.
|
114
|
+
|
115
|
+
### Transitioning from Other Tools
|
116
|
+
|
117
|
+
####TODO:
|
118
|
+
|
119
|
+
Support for sucking in `cassandra_migrations` migration files and changing to semantic versioning.
|
120
|
+
|
121
|
+
```
|
122
|
+
cassie migration:import cassandra_migrate
|
123
|
+
=> 0.0.1 - cassandra_migrate/20160818213805_create_users.rb -> migrations/000_000_001_create_users.rb
|
124
|
+
=> 0.0.2 - cassandra_migrate/20160818213811_create_widgets.rb -> migrations/000_000_002_create_widgets.rb
|
125
|
+
=> 0.0.3 - cassandra_migrate/20160818213843_create_sprockets.rb -> migrations/000_000_003_create_sprockets.rb
|
126
|
+
3 migrations imported
|
127
|
+
```
|
128
|
+
|
129
|
+
Support for a .cdl migration file to act as initial migration (dump defining initial version).
|
130
|
+
|
131
|
+
```
|
132
|
+
cassie migration:initialize
|
133
|
+
=> 0.1.0 - migrations/000_001_000_initial_schema.cdl
|
134
|
+
schema initialized at v0.1.0 from current database structure
|
135
|
+
```
|
136
|
+
|
137
|
+
```
|
138
|
+
cassie migration:initialize 0.15.3
|
139
|
+
=> 0.1.0 - migrations/000_015_003_initial_schema.cdl
|
140
|
+
schema initialized at v0.15.3 from current database structure
|
141
|
+
```
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
# Extend a module/class with Configuration to enable migration management
|
3
|
+
module Configuration
|
4
|
+
|
5
|
+
attr_accessor :schema_keyspace,
|
6
|
+
:versions_table
|
7
|
+
|
8
|
+
def self.extended(extender)
|
9
|
+
extender.paths["schema_structure"] = "db/structure.cql"
|
10
|
+
extender.schema_keyspace = "cassie_schema"
|
11
|
+
extender.versions_table = "versions"
|
12
|
+
end
|
13
|
+
|
14
|
+
def paths
|
15
|
+
@paths ||= {}.with_indifferent_access
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
|
3
|
+
class AlreadyInitiailizedError < StandardError; end
|
4
|
+
class UninitializedError < StandardError; end
|
5
|
+
|
6
|
+
module Initialization
|
7
|
+
require 'etc'
|
8
|
+
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
def initialize_versioning(version=default_version)
|
14
|
+
create_schema_keyspace unless keyspace_exists?
|
15
|
+
raise Cassie::Migration::AlreadyInitiailizedError if version_exists?
|
16
|
+
create_versions_table unless versions_table_exists?
|
17
|
+
insert_version(build_version(version, 'initial schema'))
|
18
|
+
end
|
19
|
+
|
20
|
+
def keyspace_exists?
|
21
|
+
Cassie.cluster.keyspaces.map(&:name).any?{|k| k == Cassie::Migration.schema_keyspace}
|
22
|
+
end
|
23
|
+
|
24
|
+
def version_exists?
|
25
|
+
!!Cassie::Migration.version
|
26
|
+
rescue Cassie::Migration::UninitializedError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def versions_table_exists?
|
31
|
+
!!SelectVersionsQuery.new(limit: 1).fetch
|
32
|
+
rescue Cassandra::Errors::InvalidError
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_version(version_number, description)
|
37
|
+
id = Cassandra::TimeUuid::Generator.new.now
|
38
|
+
migrator = Etc.getlogin rescue '<unknown>'
|
39
|
+
migrated_at = Time.now
|
40
|
+
Version.new(id, version_number, description, migrator, migrated_at)
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert_version(version)
|
44
|
+
InsertVersionQuery.new(version: version).execute
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def default_version
|
50
|
+
'0.1.0'
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_schema_keyspace
|
54
|
+
CreateSchemaKeyspaceQuery.new.execute
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_versions_table
|
58
|
+
CreateVersionsTableQuery.new.execute
|
59
|
+
end
|
60
|
+
|
61
|
+
def uninitialized_error
|
62
|
+
UninitializedError.new(uninitialized_message)
|
63
|
+
end
|
64
|
+
|
65
|
+
def uninitialized_message
|
66
|
+
"Cassie Schema Versions table not found at '#{schema_keyspace}.#{versions_table}'. Create a migration or initialize your schema to enable versioned migration support."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
class CreateSchemaKeyspaceQuery < Cassie::Definition
|
3
|
+
self.prepare = false
|
4
|
+
|
5
|
+
def statement
|
6
|
+
cql = %(
|
7
|
+
CREATE KEYSPACE #{Cassie::Migration.schema_keyspace}
|
8
|
+
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}
|
9
|
+
AND durable_writes = true;
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def keyspace
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
class CreateVersionsTableQuery < Cassie::Definition
|
3
|
+
self.prepare = false
|
4
|
+
|
5
|
+
def statement
|
6
|
+
%(
|
7
|
+
CREATE TABLE #{Cassie::Migration.versions_table} (
|
8
|
+
bucket int,
|
9
|
+
id timeuuid,
|
10
|
+
version_number text,
|
11
|
+
description text,
|
12
|
+
migrator text,
|
13
|
+
migrated_at timestamp,
|
14
|
+
PRIMARY KEY (bucket, id)
|
15
|
+
) WITH CLUSTERING ORDER BY (id DESC);
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def keyspace
|
20
|
+
Cassie::Migration.schema_keyspace
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
class InsertVersionQuery < Cassie::Modification
|
3
|
+
|
4
|
+
insert_into Cassie::Migration.versions_table
|
5
|
+
|
6
|
+
set :bucket
|
7
|
+
set :id
|
8
|
+
set :version_number
|
9
|
+
set :description
|
10
|
+
set :migrator
|
11
|
+
set :migrated_at
|
12
|
+
|
13
|
+
map_from :version
|
14
|
+
|
15
|
+
def bucket
|
16
|
+
0
|
17
|
+
end
|
18
|
+
|
19
|
+
def keyspace
|
20
|
+
Cassie::Migration.schema_keyspace
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
class SelectVersionsQuery < Cassie::Query
|
3
|
+
|
4
|
+
select_from Cassie::Migration.versions_table
|
5
|
+
|
6
|
+
def build_result(row)
|
7
|
+
Version.new(*row.values_at(*Version.members.map(&:to_s)))
|
8
|
+
end
|
9
|
+
|
10
|
+
def keyspace
|
11
|
+
Cassie::Migration.schema_keyspace
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
module Queries
|
3
|
+
require_relative 'queries/create_schema_keyspace_query'
|
4
|
+
require_relative 'queries/create_versions_table_query'
|
5
|
+
require_relative 'queries/insert_version_query'
|
6
|
+
require_relative 'queries/select_versions_query'
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
require_relative 'queries'
|
3
|
+
|
4
|
+
class StructureDumper
|
5
|
+
attr_reader :destination_path
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
@destination_path = opts[:destination_path] || default_destination_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def stream
|
12
|
+
@stream ||= begin
|
13
|
+
prepare_stream
|
14
|
+
File.open(destination_path, "w+")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def structure
|
19
|
+
@structure ||= begin
|
20
|
+
args = ["-e", "'DESCRIBE SCHEMA'"]
|
21
|
+
runner = Cassie::Support::CommandRunner.new("cqlsh", args)
|
22
|
+
runner.run
|
23
|
+
|
24
|
+
raise runner.failure_message unless runner.success?
|
25
|
+
|
26
|
+
runner.output
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def versions
|
31
|
+
@versions ||= begin
|
32
|
+
versions_query.fetch
|
33
|
+
rescue Cassandra::Errors::InvalidError => e
|
34
|
+
log_versions_not_found(e)
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def versions_insert_cql
|
40
|
+
inserts = versions.map do |v|
|
41
|
+
InsertVersionQuery.new(version: v).to_cql
|
42
|
+
end
|
43
|
+
inserts.join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def dump
|
47
|
+
stream << structure
|
48
|
+
stream << "\n\n"
|
49
|
+
stream << versions_insert_cql
|
50
|
+
stream << "\n"
|
51
|
+
|
52
|
+
close_stream
|
53
|
+
end
|
54
|
+
|
55
|
+
def versions_table_name
|
56
|
+
"cassie_schema.versions"
|
57
|
+
end
|
58
|
+
|
59
|
+
def destination_path
|
60
|
+
@destination_path || raise("Unconfigured schema structure path: `Cassie::Migration.paths[:schema_structure]` is empty")
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def versions_query
|
66
|
+
SelectVersionsQuery.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_destination_path
|
70
|
+
Cassie::Migration.paths[:schema_structure]
|
71
|
+
end
|
72
|
+
|
73
|
+
def prepare_stream
|
74
|
+
dir = File.dirname(destination_path)
|
75
|
+
Dir.mkdir(dir) unless File.directory?(dir)
|
76
|
+
end
|
77
|
+
|
78
|
+
def close_stream
|
79
|
+
stream.close
|
80
|
+
@stream = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_versions_not_found(error)
|
84
|
+
msg = "WARNING: Cassie Schema Versions table not found at '#{versions_table_name}'. Create a migration, or initialize your schema for versioned migration support."
|
85
|
+
msg << "\n\t"
|
86
|
+
msg << error.message.split("\n").join("\n\t")
|
87
|
+
logger.warn(msg)
|
88
|
+
end
|
89
|
+
|
90
|
+
def logger
|
91
|
+
Cassie.logger
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Cassie::Migration
|
2
|
+
class StructureLoader
|
3
|
+
attr_reader :source_path
|
4
|
+
|
5
|
+
|
6
|
+
def initialize(opts={})
|
7
|
+
@source_path = opts[:source_path] || default_source_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def load
|
11
|
+
args = ["-f", source_path]
|
12
|
+
runner = Cassie::Support::CommandRunner.new("cqlsh", args)
|
13
|
+
|
14
|
+
runner.run
|
15
|
+
raise runner.failure_message unless runner.success?
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def default_source_path
|
21
|
+
Cassie.paths[:schema_structure]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Cassie
|
2
|
+
require_relative 'migration/configuration'
|
3
|
+
require_relative 'migration/initialization'
|
4
|
+
|
5
|
+
module Migration
|
6
|
+
extend Configuration
|
7
|
+
include Initialization
|
8
|
+
|
9
|
+
require_relative 'migration/structure_dumper'
|
10
|
+
require_relative 'migration/structure_loader'
|
11
|
+
require_relative 'migration/version'
|
12
|
+
|
13
|
+
def self.version_number
|
14
|
+
return nil unless version
|
15
|
+
version.version_number
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.version
|
19
|
+
SelectVersionsQuery.new.fetch_first
|
20
|
+
rescue Cassandra::Errors::InvalidError
|
21
|
+
raise uninitialized_error
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.versions
|
25
|
+
SelectVersionsQuery.new.fetch
|
26
|
+
rescue Cassandra::Errors::InvalidError
|
27
|
+
raise uninitialized_error
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -445,7 +445,15 @@ query.result.each
|
|
445
445
|
#=> #<[#< Struct id=:123, username=:eprothro >]>
|
446
446
|
```
|
447
447
|
|
448
|
-
The result
|
448
|
+
The result has a `first!` method that raises if no result is available
|
449
|
+
```ruby
|
450
|
+
query.execute
|
451
|
+
#=> true
|
452
|
+
query.result.first!
|
453
|
+
Cassie::Statements::RecordNotFound: CQL row does not exist
|
454
|
+
```
|
455
|
+
|
456
|
+
The result delegates to the `Cassandra::Result`.
|
449
457
|
```ruby
|
450
458
|
query.result.execution_info
|
451
459
|
#=> #<Cassandra::Execution::Info:0x007fb404b51390 @payload=nil, @warnings=nil, @keyspace="cassie_test", @statement=#<Cassandra::Statements::Bound:0x3fda0258dee8 @cql="SELECT * FROM users_by_username LIMIT 500;" @params=[]>, @options=#<Cassandra::Execution::Options:0x007fb404b1b880 @consistency=:local_one, @page_size=10000, @trace=false, @timeout=12, @serial_consistency=nil, @arguments=[], @type_hints=[], @paging_state=nil, @idempotent=false, @payload=nil>, @hosts=[#<Cassandra::Host:0x3fda02541390 @ip=127.0.0.1>], @consistency=:local_one, @retries=0, @trace=nil>
|
@@ -734,6 +742,20 @@ end
|
|
734
742
|
|
735
743
|
> Note: unbound queries can be vulnerable to injection attacks. Be careful.
|
736
744
|
|
745
|
+
### Development and Debugging
|
746
|
+
|
747
|
+
#### `to_cql`
|
748
|
+
|
749
|
+
Cassie query objects have a `to_cql` method that handles positional argument interleaving and type conversion to provide CQL that is executable in `cqlsh`.
|
750
|
+
|
751
|
+
Keep your queries as perpared/bound statements, but easily copy executable CQL elsewhere.
|
752
|
+
|
753
|
+
```
|
754
|
+
query = UpdateUserQuery.new(user: user)
|
755
|
+
query.to_cql
|
756
|
+
=> "UPDATE users_by_id SET phone = '+15555555555', email = 'eprothro@example.com', username = 'eprothro' WHERE id = d331f6b8-8b05-11e6-b61f-2774b0185e07;"
|
757
|
+
```
|
758
|
+
|
737
759
|
#### Logging
|
738
760
|
|
739
761
|
Cassie Query objects use the Cassie logger unless overridden. This logs to STDOUT by default. Set any log stream you wish.
|
@@ -73,7 +73,7 @@ module Cassie::Statements::Execution::PartitionLinking
|
|
73
73
|
previous_key(current_key)
|
74
74
|
end
|
75
75
|
if _partition < first_key || _partition > last_key
|
76
|
-
logger.warn("
|
76
|
+
logger.warn("warning: linking to partition that is outside of ranges defined. #{_partition} outside of (#{first_key}..#{last_key}). This could result in unexpected records being returned.")
|
77
77
|
end
|
78
78
|
|
79
79
|
# define object singleton method to
|
@@ -116,10 +116,10 @@ module Cassie::Statements::Execution::PartitionLinking
|
|
116
116
|
Cassie::Statements.logger
|
117
117
|
end
|
118
118
|
|
119
|
-
def eval_opt(value,
|
119
|
+
def eval_opt(value, src=source)
|
120
120
|
case value
|
121
121
|
when Symbol
|
122
|
-
|
122
|
+
src.send(value)
|
123
123
|
else
|
124
124
|
value
|
125
125
|
end
|
@@ -6,6 +6,7 @@ module Cassie::Statements::Statement
|
|
6
6
|
def if_not_exists(opts={})
|
7
7
|
condition = "NOT EXISTS"
|
8
8
|
opts.delete(:value)
|
9
|
+
opts[:if] = true unless opts.has_key?(:if)
|
9
10
|
|
10
11
|
conditions[condition] = opts
|
11
12
|
end
|
@@ -13,6 +14,7 @@ module Cassie::Statements::Statement
|
|
13
14
|
def if_exists(opts={})
|
14
15
|
condition = "EXISTS"
|
15
16
|
opts.delete(:value)
|
17
|
+
opts[:if] = true unless opts.has_key?(:if)
|
16
18
|
|
17
19
|
conditions[condition] = opts
|
18
20
|
end
|
@@ -31,9 +33,9 @@ module Cassie::Statements::Statement
|
|
31
33
|
bindings = []
|
32
34
|
|
33
35
|
conditions.each do |condition, opts|
|
34
|
-
if
|
36
|
+
if !!source_eval(opts[:if])
|
35
37
|
condition_strings << condition.to_s
|
36
|
-
bindings <<
|
38
|
+
bindings << source_eval(opts[:value]) if opts.has_key?(:value)
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
@@ -62,12 +62,14 @@ module Cassie::Statements
|
|
62
62
|
|
63
63
|
def initialize_copy(other)
|
64
64
|
super
|
65
|
+
@jruby_issue_4228 = false
|
65
66
|
remove_limit_singleton
|
66
67
|
end
|
67
68
|
|
68
69
|
def define_limit_singleton(temp_limit)
|
69
70
|
assert_no_limit_singleton
|
70
71
|
define_singleton_method(:limit) do
|
72
|
+
return super() if jruby_issue_4228?
|
71
73
|
temp_limit
|
72
74
|
end
|
73
75
|
end
|
@@ -78,12 +80,18 @@ module Cassie::Statements
|
|
78
80
|
remove_method :limit
|
79
81
|
end
|
80
82
|
end
|
83
|
+
rescue NameError
|
84
|
+
@jruby_issue_4228 = true
|
81
85
|
end
|
82
86
|
|
83
87
|
def assert_no_limit_singleton
|
84
88
|
if singleton_methods.include?(:limit)
|
85
|
-
raise NameError.new("A singleton method has already been defined for `limit`. `with_limit` can't be implemented.", :limit)
|
89
|
+
raise NameError.new("A singleton method has already been defined for `limit`. `with_limit` can't be implemented.", :limit) unless jruby_issue_4228?
|
86
90
|
end
|
87
91
|
end
|
92
|
+
|
93
|
+
def jruby_issue_4228?
|
94
|
+
defined?(@jruby_issue_4228) && @jruby_issue_4228
|
95
|
+
end
|
88
96
|
end
|
89
97
|
end
|
@@ -35,6 +35,16 @@ module Cassie::Statements
|
|
35
35
|
Cassandra::Statements::Simple.new(*build_cql_and_bindings)
|
36
36
|
end
|
37
37
|
|
38
|
+
# returns a CQL string with inline parameters, that
|
39
|
+
# is representative of what would be executed in a CQL shell
|
40
|
+
def to_cql
|
41
|
+
if statement.respond_to?(:cql) && statement.respond_to?(:params)
|
42
|
+
Cassie::Support::StatementParser.new(statement).to_cql
|
43
|
+
else
|
44
|
+
statement.to_s
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
38
48
|
protected
|
39
49
|
|
40
50
|
def build_cql_and_bindings
|
@@ -47,25 +57,10 @@ module Cassie::Statements
|
|
47
57
|
|
48
58
|
private
|
49
59
|
|
50
|
-
def
|
51
|
-
case value
|
52
|
-
when nil
|
53
|
-
true # if is true by default
|
54
|
-
when Symbol
|
55
|
-
!!send(value)
|
56
|
-
when String
|
57
|
-
!!eval(value)
|
58
|
-
else
|
59
|
-
!!value
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def eval_value_opt(value)
|
60
|
+
def source_eval(value, src=self)
|
64
61
|
case value
|
65
62
|
when Symbol
|
66
|
-
send(value)
|
67
|
-
when String
|
68
|
-
eval(value)
|
63
|
+
src.send(value)
|
69
64
|
else
|
70
65
|
value
|
71
66
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Cassie::Support
|
2
2
|
class CommandRunner
|
3
|
-
attr_reader :binary, :args, :command, :
|
3
|
+
attr_reader :binary, :args, :command, :status, :duration, :output
|
4
4
|
|
5
5
|
# When a block is given, the command runs before yielding
|
6
6
|
def initialize(binary, args=[])
|
@@ -21,55 +21,47 @@ module Cassie::Support
|
|
21
21
|
|
22
22
|
IO.popen(command) do |io|
|
23
23
|
@output=io.read
|
24
|
-
@
|
24
|
+
@status=Process.waitpid2(io.pid)[1]
|
25
25
|
end
|
26
26
|
|
27
27
|
@duration=Time.now-t1
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
# Runs the command and raises if doesn't exit 0
|
32
|
-
def run!
|
33
|
-
fail unless exitcode == 0
|
34
|
-
end
|
35
|
-
|
36
|
-
def fail
|
37
|
-
Kernel.fail error_message
|
28
|
+
completed?
|
38
29
|
end
|
39
30
|
|
40
31
|
# Returns false if the command hasn't been executed yet
|
41
32
|
def run?
|
42
|
-
|
33
|
+
!!@duration
|
43
34
|
end
|
44
35
|
|
45
36
|
# Returns the exit code for the command. Runs the command if it hasn't run yet.
|
46
37
|
def exitcode
|
47
|
-
|
48
|
-
process.exitstatus
|
38
|
+
status.exitstatus
|
49
39
|
end
|
50
40
|
|
51
|
-
# Returns true if
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
process.success?
|
41
|
+
# Returns true if exited 0
|
42
|
+
def success?
|
43
|
+
return false unless run?
|
44
|
+
exitcode == 0
|
56
45
|
end
|
57
46
|
|
58
|
-
|
59
|
-
|
60
|
-
def
|
61
|
-
|
47
|
+
# Returns true if the command completed execution and success bits were set.
|
48
|
+
# Will return false if the command hasn't been executed
|
49
|
+
def completed?
|
50
|
+
return false unless run?
|
51
|
+
status.exited? && @status.success?
|
62
52
|
end
|
63
53
|
|
64
|
-
def
|
65
|
-
msg = "
|
54
|
+
def failure_message
|
55
|
+
msg = "---------------------\n"
|
66
56
|
msg << red(output)
|
67
|
-
msg << "
|
68
|
-
msg << "
|
69
|
-
msg << "
|
57
|
+
msg << "---------------------\n"
|
58
|
+
msg << "Failed to execute `#{command}`:\n"
|
59
|
+
msg << "\tPlease check the output above for any errors and make sure that `#{binary}` is installed in your PATH with proper permissions."
|
70
60
|
msg
|
71
61
|
end
|
72
62
|
|
63
|
+
protected
|
64
|
+
|
73
65
|
def red(message)
|
74
66
|
"\e[1m\e[31m#{message}\e[0m\e[22m"
|
75
67
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Cassie::Support
|
2
|
+
class StatementParser
|
3
|
+
attr_reader :statement
|
4
|
+
|
5
|
+
def initialize(statement)
|
6
|
+
@statement = statement
|
7
|
+
end
|
8
|
+
|
9
|
+
def bound_cql
|
10
|
+
statement.cql
|
11
|
+
end
|
12
|
+
|
13
|
+
def params
|
14
|
+
statement.params
|
15
|
+
end
|
16
|
+
|
17
|
+
def params_types
|
18
|
+
statement.params_types
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_cql
|
22
|
+
cql = bound_cql.dup
|
23
|
+
|
24
|
+
params_types.map.with_index do |type, i|
|
25
|
+
cassandra_param = type.new(params[i])
|
26
|
+
quoted_val = if QUOTED_TYPES.include? type.kind
|
27
|
+
"'#{cassandra_param.to_s}'"
|
28
|
+
else
|
29
|
+
cassandra_param.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
cql.sub!("?", quoted_val)
|
33
|
+
end
|
34
|
+
cql
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
QUOTED_TYPES = [:date, :time, :text, :timestamp, :inet, :ascii].freeze
|
40
|
+
end
|
41
|
+
end
|
data/lib/cassie/support.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
module Cassie::Testing::Fake
|
2
2
|
class PreparedStatement
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :original_statement
|
4
4
|
|
5
5
|
def initialize(statement)
|
6
|
-
@
|
6
|
+
@original_statement = statement
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
# Creates a statement bound with specific arguments
|
10
|
+
#
|
11
|
+
# Cassandra does type checking against connection version
|
12
|
+
# which requires a connection.
|
13
|
+
#
|
14
|
+
# Note: this fake implementation returns a simple statement
|
15
|
+
# not a bound statement. Implementing the latter would require
|
16
|
+
# faking the metadata on the prepared statement object
|
17
|
+
# which I don't have reason to do yet
|
18
|
+
def bind(new_params)
|
19
|
+
fake_bound = original_statement.clone
|
20
|
+
fake_bound.instance_variable_set :@params, new_params
|
21
|
+
fake_bound
|
11
22
|
end
|
12
23
|
end
|
13
24
|
end
|
data/lib/cassie/version.rb
CHANGED
data/lib/cassie.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cassie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.beta.
|
4
|
+
version: 1.0.0.beta.24
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Prothro
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cassandra-driver
|
@@ -45,89 +45,39 @@ dependencies:
|
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '4.2'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: terminal-table
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
54
|
-
type: :development
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - "~>"
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '3.4'
|
61
|
-
- !ruby/object:Gem::Dependency
|
62
|
-
name: rake
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
64
|
-
requirements:
|
65
|
-
- - "~>"
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version: '11.3'
|
68
|
-
type: :development
|
69
|
-
prerelease: false
|
70
|
-
version_requirements: !ruby/object:Gem::Requirement
|
71
|
-
requirements:
|
72
|
-
- - "~>"
|
73
|
-
- !ruby/object:Gem::Version
|
74
|
-
version: '11.3'
|
75
|
-
- !ruby/object:Gem::Dependency
|
76
|
-
name: gem-release
|
77
|
-
requirement: !ruby/object:Gem::Requirement
|
78
|
-
requirements:
|
79
|
-
- - "~>"
|
80
|
-
- !ruby/object:Gem::Version
|
81
|
-
version: 0.7.4
|
82
|
-
type: :development
|
83
|
-
prerelease: false
|
84
|
-
version_requirements: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - "~>"
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: 0.7.4
|
89
|
-
- !ruby/object:Gem::Dependency
|
90
|
-
name: byebug
|
91
|
-
requirement: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
53
|
+
version: '1.4'
|
93
54
|
- - ">="
|
94
55
|
- !ruby/object:Gem::Version
|
95
|
-
version:
|
96
|
-
type: :
|
56
|
+
version: 1.4.0
|
57
|
+
type: :runtime
|
97
58
|
prerelease: false
|
98
59
|
version_requirements: !ruby/object:Gem::Requirement
|
99
60
|
requirements:
|
100
|
-
- - "
|
101
|
-
- !ruby/object:Gem::Version
|
102
|
-
version: '0'
|
103
|
-
- !ruby/object:Gem::Dependency
|
104
|
-
name: pry
|
105
|
-
requirement: !ruby/object:Gem::Requirement
|
106
|
-
requirements:
|
107
|
-
- - ">="
|
61
|
+
- - "~>"
|
108
62
|
- !ruby/object:Gem::Version
|
109
|
-
version: '
|
110
|
-
type: :development
|
111
|
-
prerelease: false
|
112
|
-
version_requirements: !ruby/object:Gem::Requirement
|
113
|
-
requirements:
|
63
|
+
version: '1.4'
|
114
64
|
- - ">="
|
115
65
|
- !ruby/object:Gem::Version
|
116
|
-
version:
|
66
|
+
version: 1.4.0
|
117
67
|
- !ruby/object:Gem::Dependency
|
118
|
-
name:
|
68
|
+
name: bundler
|
119
69
|
requirement: !ruby/object:Gem::Requirement
|
120
70
|
requirements:
|
121
|
-
- - "
|
71
|
+
- - "~>"
|
122
72
|
- !ruby/object:Gem::Version
|
123
|
-
version: '
|
73
|
+
version: '1.9'
|
124
74
|
type: :development
|
125
75
|
prerelease: false
|
126
76
|
version_requirements: !ruby/object:Gem::Requirement
|
127
77
|
requirements:
|
128
|
-
- - "
|
78
|
+
- - "~>"
|
129
79
|
- !ruby/object:Gem::Version
|
130
|
-
version: '
|
80
|
+
version: '1.9'
|
131
81
|
description: Cassie provides database configration, versioned migrations, efficient
|
132
82
|
session management, and query classes. This allows applications to use the functionality
|
133
83
|
provided by the official `cassandra-driver` through lightweight and easy to use
|
@@ -153,6 +103,18 @@ files:
|
|
153
103
|
- lib/cassie/connection_handler/sessions.rb
|
154
104
|
- lib/cassie/definition.rb
|
155
105
|
- lib/cassie/logger.rb
|
106
|
+
- lib/cassie/migration.rb
|
107
|
+
- lib/cassie/migration/README.md
|
108
|
+
- lib/cassie/migration/configuration.rb
|
109
|
+
- lib/cassie/migration/initialization.rb
|
110
|
+
- lib/cassie/migration/queries.rb
|
111
|
+
- lib/cassie/migration/queries/create_schema_keyspace_query.rb
|
112
|
+
- lib/cassie/migration/queries/create_versions_table_query.rb
|
113
|
+
- lib/cassie/migration/queries/insert_version_query.rb
|
114
|
+
- lib/cassie/migration/queries/select_versions_query.rb
|
115
|
+
- lib/cassie/migration/structure_dumper.rb
|
116
|
+
- lib/cassie/migration/structure_loader.rb
|
117
|
+
- lib/cassie/migration/version.rb
|
156
118
|
- lib/cassie/modification.rb
|
157
119
|
- lib/cassie/query.rb
|
158
120
|
- lib/cassie/statements.rb
|
@@ -208,6 +170,7 @@ files:
|
|
208
170
|
- lib/cassie/statements/statement/updating.rb
|
209
171
|
- lib/cassie/support.rb
|
210
172
|
- lib/cassie/support/command_runner.rb
|
173
|
+
- lib/cassie/support/statement_parser.rb
|
211
174
|
- lib/cassie/testing.rb
|
212
175
|
- lib/cassie/testing/README.md
|
213
176
|
- lib/cassie/testing/fake/definition.rb
|