cassie 1.0.0.beta.22 → 1.0.0.beta.24
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 +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
|