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.
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