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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/bin/cassie +59 -21
  3. data/lib/cassie/configuration/core.rb +1 -2
  4. data/lib/cassie/migration/README.md +141 -0
  5. data/lib/cassie/migration/configuration.rb +18 -0
  6. data/lib/cassie/migration/initialization.rb +70 -0
  7. data/lib/cassie/migration/queries/create_schema_keyspace_query.rb +17 -0
  8. data/lib/cassie/migration/queries/create_versions_table_query.rb +23 -0
  9. data/lib/cassie/migration/queries/insert_version_query.rb +23 -0
  10. data/lib/cassie/migration/queries/select_versions_query.rb +14 -0
  11. data/lib/cassie/migration/queries.rb +8 -0
  12. data/lib/cassie/migration/structure_dumper.rb +94 -0
  13. data/lib/cassie/migration/structure_loader.rb +24 -0
  14. data/lib/cassie/migration/version.rb +4 -0
  15. data/lib/cassie/migration.rb +30 -0
  16. data/lib/cassie/statements/README.md +23 -1
  17. data/lib/cassie/statements/execution/partition_linking/policy_methods.rb +3 -3
  18. data/lib/cassie/statements/execution/partition_linking.rb +2 -1
  19. data/lib/cassie/statements/execution/results/querying.rb +4 -0
  20. data/lib/cassie/statements/statement/assignment.rb +2 -2
  21. data/lib/cassie/statements/statement/conditions.rb +4 -2
  22. data/lib/cassie/statements/statement/limiting.rb +9 -1
  23. data/lib/cassie/statements/statement/relation.rb +2 -2
  24. data/lib/cassie/statements/statement.rb +12 -17
  25. data/lib/cassie/support/command_runner.rb +21 -29
  26. data/lib/cassie/support/statement_parser.rb +41 -0
  27. data/lib/cassie/support.rb +1 -0
  28. data/lib/cassie/testing/fake/prepared_statement.rb +15 -4
  29. data/lib/cassie/version.rb +1 -1
  30. data/lib/cassie.rb +1 -0
  31. metadata +27 -64
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 535c984598050fbd5ac382820afbc887b1fd57f5
4
- data.tar.gz: 7f1a97fc63a2a53b4947c5f2e0ef43bcdfb594a2
3
+ metadata.gz: 65b5763101f92f5797356a1081e0fee0219cd86a
4
+ data.tar.gz: ff6994cee1c3f6d04d4ff35f01bd2e59e4d50ac5
5
5
  SHA512:
6
- metadata.gz: e627d882bdd46544ccd753fd5b215c6fbb02d913311e3e982b3e4451039b209420d030fcd2fbd0de08bf1dd8da56a56edc74f7b867318381c1296fd392f87df9
7
- data.tar.gz: 7f3eed69154aad5ed58fb1463274977462335deaa605fdec9afda5a223f7b213072ac8f1faf76a17a545e5a0539bab46325604fe9155faa0402dc9bf1afa061c
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.finished?
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
- # | grep cassandra | grep -v grep | awk '{print $1}'`
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
- opts = {}
79
- opts[:destination_path] = Cassie.paths[:schema_structure]
79
+ dumper = Cassie::Migration::StructureDumper.new
80
+ dumper.dump
81
+ puts "[#{green("✓")}] Cassandra schema written to #{dumper.destination_path}"
80
82
 
81
- args = ["-e", "'DESCRIBE SCHEMA'"]
83
+ rescue => e
84
+ puts e.message
85
+ exit(1)
86
+ end
82
87
 
83
- runner = Cassie::Support::CommandRunner.new("cqlsh", args)
84
- runner.run!
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
- dir = File.dirname(opts[:destination_path])
87
- Dir.mkdir(dir) unless File.directory?(dir)
88
- File.open(opts[:destination_path], "w+") do |f|
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
- def load_structure
109
- opts = {}
110
- opts[:source_path] = Cassie.paths[:schema_structure]
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
- args = ["-f", opts[:source_path]]
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
- runner = Cassie::Support::CommandRunner.new("cqlsh", args)
115
- runner.run!
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
- puts "[✓] Cassandra schema loaded from #{opts[:source_path]}"
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:\n `#{self}.configuration` as been set explicitly. Setting `configurations` will have no effect."
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,4 @@
1
+ module Cassie::Migration
2
+ Version = Struct.new(:id, :version_number, :description, :migrator, :migrated_at) do
3
+ end
4
+ 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 also delegates to the `Cassandra::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("[WARN] linking to partition that is outside of ranges defined. #{_partition} outside of (#{first_key}..#{last_key}). This could result in unexpected records being returned.")
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, _source=source)
119
+ def eval_opt(value, src=source)
120
120
  case value
121
121
  when Symbol
122
- _source.send(value)
122
+ src.send(value)
123
123
  else
124
124
  value
125
125
  end
@@ -16,7 +16,8 @@ module Cassie::Statements::Execution
16
16
  end
17
17
 
18
18
  def partition_linker
19
- @partition_linker || SimplePolicy
19
+ return @partition_linker if defined?(@partition_linker)
20
+ SimplePolicy
20
21
  end
21
22
 
22
23
  def partition_linker_args=(val)
@@ -38,6 +38,10 @@ module Cassie::Statements::Results
38
38
  end
39
39
  end
40
40
 
41
+ def first!
42
+ first || (raise Cassie::Statements::RecordNotFound, 'CQL row does not exist')
43
+ end
44
+
41
45
  def success?
42
46
  # an empty query is still successful
43
47
  return true if __getobj__.empty?
@@ -39,10 +39,10 @@ module Cassie::Statements::Statement
39
39
 
40
40
  private
41
41
 
42
- def source_eval(value, _source=source)
42
+ def source_eval(value, src=source)
43
43
  case value
44
44
  when Symbol
45
- _source.send(value)
45
+ src.send(value)
46
46
  else
47
47
  value
48
48
  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 eval_if_opt?(opts[:if])
36
+ if !!source_eval(opts[:if])
35
37
  condition_strings << condition.to_s
36
- bindings << eval_value_opt(opts[:value]) if opts.has_key?(:value)
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
@@ -56,10 +56,10 @@ module Cassie::Statements::Statement
56
56
 
57
57
  private
58
58
 
59
- def source_eval(value, _source=source)
59
+ def source_eval(value, src=source)
60
60
  case value
61
61
  when Symbol
62
- _source.send(value)
62
+ src.send(value)
63
63
  else
64
64
  value
65
65
  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 eval_if_opt?(value)
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, :process, :duration, :output
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
- @process=Process.waitpid2(io.pid)[1]
24
+ @status=Process.waitpid2(io.pid)[1]
25
25
  end
26
26
 
27
27
  @duration=Time.now-t1
28
- exitcode == 0
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
- !!process
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
- ensure_run
48
- process.exitstatus
38
+ status.exitstatus
49
39
  end
50
40
 
51
- # Returns true if the command completed execution.
52
- # Will return false if the command hasn't been executed
53
- def finished?
54
- return false unless process
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
- protected
59
-
60
- def ensure_run
61
- run unless process
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 error_message
65
- msg = "\n"
54
+ def failure_message
55
+ msg = "---------------------\n"
66
56
  msg << red(output)
67
- msg << "\n---------------------"
68
- msg << "\n\nfailed to execute `#{command}`.\n"
69
- msg << "Please check the output above for any errors and make sure that `#{binary}` is installed in your PATH with proper permissions.\n"
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
@@ -1,5 +1,6 @@
1
1
  module Cassie
2
2
  module Support
3
3
  require_relative 'support/command_runner'
4
+ require_relative 'support/statement_parser'
4
5
  end
5
6
  end
@@ -1,13 +1,24 @@
1
1
  module Cassie::Testing::Fake
2
2
  class PreparedStatement
3
- attr_reader :statement
3
+ attr_reader :original_statement
4
4
 
5
5
  def initialize(statement)
6
- @statement = statement
6
+ @original_statement = statement
7
7
  end
8
8
 
9
- def bind(params)
10
- statement
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
@@ -1,3 +1,3 @@
1
1
  module Cassie
2
- VERSION = "1.0.0.beta.22"
2
+ VERSION = "1.0.0.beta.24"
3
3
  end
data/lib/cassie.rb CHANGED
@@ -20,6 +20,7 @@ module Cassie
20
20
  require_relative 'cassie/definition'
21
21
  require_relative 'cassie/query'
22
22
  require_relative 'cassie/modification'
23
+ require_relative 'cassie/migration'
23
24
 
24
25
  extend Configuration::Core
25
26
  extend ConnectionHandler
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.22
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-13 00:00:00.000000000 Z
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: rspec
48
+ name: terminal-table
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '3.4'
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: '0'
96
- type: :development
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: '0'
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: '0'
66
+ version: 1.4.0
117
67
  - !ruby/object:Gem::Dependency
118
- name: benchmark-ips
68
+ name: bundler
119
69
  requirement: !ruby/object:Gem::Requirement
120
70
  requirements:
121
- - - ">="
71
+ - - "~>"
122
72
  - !ruby/object:Gem::Version
123
- version: '0'
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: '0'
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