cassava_rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +22 -0
- data/README.md +112 -0
- data/Rakefile +18 -0
- data/cassava_rb.gemspec +26 -0
- data/lib/cassava.rb +5 -0
- data/lib/cassava/client.rb +264 -0
- data/lib/cassava/version.rb +3 -0
- data/test/cassava/client_test.rb +195 -0
- data/test/test_helper.rb +25 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 608975429084fed43ea970713fc2ba91497d061b
|
4
|
+
data.tar.gz: 8c782e434e9172dd8d9a23ac286186de9ba155b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2116e9edaff4ac4ef6d91f105a41028b1d217b57f689f26ee96594792dff87c68ec201f28f4fe062d2819fa719d60726a19011c0e1a3de2b06e2a761ae18c8ba
|
7
|
+
data.tar.gz: b027b99912c044a853d0d47bc76308df2714dfba43da863ff866f63d7d603f47c16b14a6251993da834c41e8cb49d8142166e0e359bde53eac84fd4d4924b85c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in cassava.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem "pry"
|
8
|
+
gem "awesome_print"
|
9
|
+
gem 'm', :git => 'git@github.com:ANorwell/m.git', :branch => 'minitest_5'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'minitest_should', :git => 'git@github.com:citrus/minitest_should.git'
|
14
|
+
gem "mocha"
|
15
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Datto
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Cassava
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/backupify/cassava)
|
4
|
+
|
5
|
+
An unopinionated Cassandra client built on top of the Datastax Cassandra Driver. Cassava provides a higher-level statement execution interface while still supporting asynchronous queries and the ability to connect to multiple clusters.
|
6
|
+
|
7
|
+
_If prepared incorrectly, the cassava plant can produce cyanide, a deadly compound when consumed._
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'cassava_rb', github: 'backupify/cassava'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install cassava_rb
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Creating a client requires a `Cassandra::Session` object:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'cassava'
|
31
|
+
cluster = Cassandra.cluster
|
32
|
+
session = cluster.connect('a_keyspace')
|
33
|
+
client = Cassava::Client.new(session)
|
34
|
+
```
|
35
|
+
|
36
|
+
### Insert
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
client.insert(:table, :id => 'id', :a => 1, :b => 'b')
|
40
|
+
|
41
|
+
```
|
42
|
+
|
43
|
+
### Select
|
44
|
+
|
45
|
+
A select statement is built and then executed. To create a statement that will
|
46
|
+
select all columns:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
statement = client.select(:table)
|
50
|
+
```
|
51
|
+
|
52
|
+
This statement can then be further refined:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
new_statement = statement.where(:id => 'id').limit(2)
|
56
|
+
```
|
57
|
+
|
58
|
+
and then executed:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
result = new_statement.execute
|
62
|
+
```
|
63
|
+
|
64
|
+
or executed asynchronously:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
promise = new_statement.execute_async
|
68
|
+
```
|
69
|
+
|
70
|
+
To select only certain rows, provide those rows to the select method:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
client.select(:table, [:id, :a, :b]).execute
|
74
|
+
```
|
75
|
+
|
76
|
+
Ordering can be specified using the order method:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
client.select(:table).where('id = ? AND a > ?', 1, 'b').order(:a, :desc).execute
|
80
|
+
```
|
81
|
+
|
82
|
+
Filtering is permitting with the `allow_filtering` method.
|
83
|
+
|
84
|
+
Multiple records can be specified by passing an array of values, but this will generate an CQL IN query and should be used with caution:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
client.select(:table).where(:id => 1, :a => [1, 2])
|
88
|
+
```
|
89
|
+
|
90
|
+
### Delete
|
91
|
+
|
92
|
+
To delete an entire record:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
client.delete(table).where(:id => 1, :a => 1).execute
|
96
|
+
```
|
97
|
+
|
98
|
+
To delete only certain columns:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
client.delete(table, [:c, :d]).where(:id => 1, :a => 1).execute
|
102
|
+
```
|
103
|
+
|
104
|
+
Note here that :c and :d must not be part of the primary key.
|
105
|
+
|
106
|
+
## Contributing
|
107
|
+
|
108
|
+
1. Fork it ( https://github.com/backupify/cassava/fork )
|
109
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
110
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
111
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
112
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
7
|
+
t.libs.push 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
task :setup do
|
11
|
+
require 'bundler/setup'
|
12
|
+
Bundler.require(:default, :development)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
task :console => [:setup] do
|
17
|
+
Pry.start
|
18
|
+
end
|
data/cassava_rb.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cassava/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "cassava_rb"
|
8
|
+
spec.version = Cassava::VERSION
|
9
|
+
spec.authors = ["Arron Norwell"]
|
10
|
+
spec.email = ["anorwell@datto.com"]
|
11
|
+
spec.summary = %q{An unopinionated wrapper for the datastax cassandra gem.}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
spec.add_development_dependency "minitest"
|
23
|
+
spec.add_development_dependency "minitest-reporters"
|
24
|
+
|
25
|
+
spec.add_dependency 'cassandra-driver'
|
26
|
+
end
|
data/lib/cassava.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
module Cassava
|
2
|
+
class Client
|
3
|
+
attr_reader :session, :executor
|
4
|
+
|
5
|
+
# @param session [Cassandra::Session] The session object
|
6
|
+
# @option opts [Object] :logger responding to :debug, :info, :warn, :error, :fatal
|
7
|
+
def initialize(session, opts = {})
|
8
|
+
@session = session
|
9
|
+
logger = opts[:logger] || NullLogger.new
|
10
|
+
@executor = Executor.new(session, logger)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @see #insert
|
14
|
+
def insert_async(table, data)
|
15
|
+
executor.execute_async(insert_statement(table, data), :arguments => data.values)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param table [Symbol] the table name
|
19
|
+
# @param data [Hash] A hash of column names to data, which will be inserted into the table
|
20
|
+
def insert(table, data)
|
21
|
+
statement = insert_statement(table, data)
|
22
|
+
executor.execute(statement, :arguments => data.values)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param table [Symbol] the table name
|
26
|
+
# @param columns [Array<Symbol>] An optional list of column names (as symbols), to only select those columns
|
27
|
+
# @return [StatementBuilder] A statement builder representing the partially completed statement.
|
28
|
+
def select(table, columns = nil)
|
29
|
+
StatementBuilder.new(executor).select(table, columns)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param table [Symbol] the table name
|
33
|
+
# @param columns [Array<String] A list of columns that will be deleted. If nil, all columns will be deleted.
|
34
|
+
# @return [StatementBuilder] A statement builder representing the partially completed statement.
|
35
|
+
def delete(table, columns = nil)
|
36
|
+
StatementBuilder.new(executor).delete(table, columns)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Pass a raw query to execute synchronously to the underlying session object.
|
40
|
+
# @param statement[String] The statment to execute
|
41
|
+
# @param opts [Hash] options accepted by Cassandra::Session
|
42
|
+
def execute_async(statement, opts = {})
|
43
|
+
executor.execute_async(statement, opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Pass a raw query to execute asynchronously to the underlying session object.
|
47
|
+
# @param statement [String] The statment to execute
|
48
|
+
# @param opts [Hash] options accepted by Cassandra::Session
|
49
|
+
def execute(statement, opts = {})
|
50
|
+
executor.execute(statement, opts)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def insert_statement(table, data)
|
56
|
+
column_names = data.keys
|
57
|
+
statement_cql = "INSERT INTO #{table} (#{column_names.join(', ')}) VALUES (#{column_names.map { |x| '?' }.join(',')})"
|
58
|
+
executor.prepare(statement_cql)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class StatementBuilder
|
63
|
+
attr_reader :executor, :table, :clauses
|
64
|
+
|
65
|
+
CLAUSE_ORDERING = {
|
66
|
+
:main => 0,
|
67
|
+
:from => 1,
|
68
|
+
:where => 2,
|
69
|
+
:order => 3,
|
70
|
+
:limit => 4,
|
71
|
+
:allow_filtering => 5
|
72
|
+
}
|
73
|
+
|
74
|
+
def initialize(executor, clauses = {})
|
75
|
+
@executor = executor
|
76
|
+
@table = table
|
77
|
+
@clauses = clauses
|
78
|
+
end
|
79
|
+
|
80
|
+
# Execute the statement synchronously
|
81
|
+
# @param opts [Hash] options accepted by Cassandra::Session
|
82
|
+
def execute(opts = {})
|
83
|
+
options = opts.dup.merge(:arguments => prepared_arguments)
|
84
|
+
executor.execute(prepared_statement, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Execute the statement asynchronously
|
88
|
+
# @param opts [Hash] options accepted by Cassandra::Session
|
89
|
+
def execute_async(opts = {})
|
90
|
+
options = opts.dup.merge(:arguments => prepared_arguments)
|
91
|
+
executor.execute_async(prepared_statement, options)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param table [Symbol] table to select data from
|
95
|
+
# @param columns [Array<Symbol>] Columns to select -- defaults to all.
|
96
|
+
# @return [StatementBuilder]
|
97
|
+
def select(table, columns = nil)
|
98
|
+
add_clause(SelectClause.new(table, columns), :main)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param table [Symbol] table to delete data from
|
102
|
+
# @param columns [Array<Symbol>] Columns to delete -- defaults to all.
|
103
|
+
# @return [StatementBuilder]
|
104
|
+
def delete(table, columns = nil)
|
105
|
+
add_clause(DeleteClause.new(table, columns), :main)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Condition the query based on a condition
|
109
|
+
# Provide either a String and a list of arguments, or a hash.
|
110
|
+
# @example
|
111
|
+
# statement.where('id = ? and field > ?', 1, 'a')
|
112
|
+
# @example
|
113
|
+
# statement.where(:id => 1, :field => 'x')
|
114
|
+
# @param args [Array] arguments representing the where condition
|
115
|
+
# @return [StatementBuilder]
|
116
|
+
def where(*args)
|
117
|
+
clause = clauses[:where] || WhereClause.new([], [])
|
118
|
+
add_clause(clause.where(*args), :where)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Allow filtering for this query
|
122
|
+
# @return [StatementBuilder]
|
123
|
+
def allow_filtering
|
124
|
+
add_clause('ALLOW FILTERING', :allow_filtering)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param clustering_column [Symbol] clustering_column to order by
|
128
|
+
# @param direction [:asc|:desc] the direction to order by, defaults to :asc
|
129
|
+
# @return [StatementBuilder]
|
130
|
+
def order(clustering_column, direction = :asc)
|
131
|
+
add_clause("ORDER BY #{clustering_column.to_s} #{direction.to_s}", :order)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param n [Integer] maximum number of results to return
|
135
|
+
# @return [StatementBuilder]
|
136
|
+
def limit(n)
|
137
|
+
add_clause("LIMIT #{n.to_i}", :limit)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return the count of objects rather than the objects themselves
|
141
|
+
# @return [StatementBuilder]
|
142
|
+
def count
|
143
|
+
add_clause(clauses[:main].count, :main)
|
144
|
+
end
|
145
|
+
|
146
|
+
# @return [String] the CQL statement that this StatementBuilder represents
|
147
|
+
def statement
|
148
|
+
clauses.sort_by { |s| CLAUSE_ORDERING[s[0]] }.map { |s| s[1] }.join(' ')
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def prepared_statement
|
154
|
+
executor.prepare(statement)
|
155
|
+
end
|
156
|
+
|
157
|
+
def prepared_arguments
|
158
|
+
clauses[:where] ? clauses[:where].arguments : []
|
159
|
+
end
|
160
|
+
|
161
|
+
# Adds a clause of a given type.
|
162
|
+
# @return [StatementBuilder] A new StatementBuilder with the added clause
|
163
|
+
def add_clause(clause, type)
|
164
|
+
clauses_copy = clauses.dup
|
165
|
+
clauses_copy[type] = clause
|
166
|
+
self.class.new(executor, clauses_copy)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
SelectClause = Struct.new(:table, :columns, :count_boolean) do
|
171
|
+
def count
|
172
|
+
self.class.new(table, _columns = nil, _count_boolean = true)
|
173
|
+
end
|
174
|
+
|
175
|
+
def to_s
|
176
|
+
if count_boolean
|
177
|
+
"SELECT COUNT(*) FROM #{table}"
|
178
|
+
else
|
179
|
+
self.columns ||= ['*']
|
180
|
+
"SELECT #{columns.join(', ')} from #{table}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
DeleteClause = Struct.new(:table, :columns) do
|
186
|
+
def to_s
|
187
|
+
if columns
|
188
|
+
"DELETE #{columns.join(', ')} from #{table}"
|
189
|
+
else
|
190
|
+
"DELETE from #{table}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
WhereClause = Struct.new(:parts, :arguments) do
|
196
|
+
def where(*args)
|
197
|
+
new_parts = self.parts.dup || []
|
198
|
+
new_arguments = self.arguments.dup || []
|
199
|
+
|
200
|
+
case args[0]
|
201
|
+
when String
|
202
|
+
new_parts << args[0]
|
203
|
+
new_arguments += args[1..-1]
|
204
|
+
when Hash
|
205
|
+
new_parts += args[0].map { |key, value| "#{key} #{where_string(value)}" }
|
206
|
+
new_arguments += args[0].values.flatten
|
207
|
+
end
|
208
|
+
self.class.new(new_parts, new_arguments)
|
209
|
+
end
|
210
|
+
|
211
|
+
def to_s
|
212
|
+
"WHERE #{parts.join(' AND ')}"
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
def where_string(value)
|
218
|
+
case value
|
219
|
+
when Array
|
220
|
+
quoted_values = value.map { |v| type_quote(v) }
|
221
|
+
"IN(#{quoted_values.map { |_| '?' }.join(', ')})"
|
222
|
+
else "= ?"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def type_quote(value)
|
227
|
+
case value
|
228
|
+
when Numeric then value.to_s
|
229
|
+
when String then "'#{value}'"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
Executor = Struct.new(:session, :logger) do
|
235
|
+
def execute(statement, opts = {})
|
236
|
+
log_statement(statement, opts)
|
237
|
+
session.execute(statement, opts)
|
238
|
+
rescue => e
|
239
|
+
log_error(e, statement, opts)
|
240
|
+
raise e
|
241
|
+
end
|
242
|
+
|
243
|
+
def execute_async(statement, opts = {})
|
244
|
+
log_statement(statement, opts)
|
245
|
+
session.execute_async(statement, opts)
|
246
|
+
end
|
247
|
+
|
248
|
+
def prepare(*args)
|
249
|
+
session.prepare(*args)
|
250
|
+
end
|
251
|
+
|
252
|
+
def log_statement(statement, opts)
|
253
|
+
logger.debug("Executing Cassandra request #{statement.to_s} with options #{opts}")
|
254
|
+
end
|
255
|
+
|
256
|
+
def log_error(e, statement, opts)
|
257
|
+
logger.debug("Error #{e} executing Cassandra request #{statement.to_s} with options #{opts}")
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class NullLogger
|
262
|
+
def method_missing(*); end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
module Cassava
|
4
|
+
class ClientTest < Minitest::Should::TestCase
|
5
|
+
setup do
|
6
|
+
initialize_test_table
|
7
|
+
@session = session_for_keyspace
|
8
|
+
@client = Cassava::Client.new(@session)
|
9
|
+
end
|
10
|
+
|
11
|
+
def string_keys(hash)
|
12
|
+
Hash[hash.map { |k, v| [k.to_s, v] }]
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'insert' do
|
16
|
+
should 'be able to insert an item' do
|
17
|
+
item = { :id => 'i', :a => 1, :b => 'b', :c => 'c', :d => 1}
|
18
|
+
@client.insert(:test, item)
|
19
|
+
assert_equal string_keys(item), @client.select(:test).execute.first
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'insert an item with some fields missing' do
|
23
|
+
item = { :id => 'i', :a => 1, :b => 'b', :c => 'c'}
|
24
|
+
expected = item.merge(:d => nil)
|
25
|
+
@client.insert(:test, item)
|
26
|
+
assert_equal string_keys(expected), @client.select(:test).execute.first
|
27
|
+
end
|
28
|
+
|
29
|
+
should 'raise an error if the row key is missing' do
|
30
|
+
item = { :a => 1, :b => 'b', :c => 'c'}
|
31
|
+
assert_raises(Cassandra::Errors::InvalidError) { @client.insert(:test, item) }
|
32
|
+
end
|
33
|
+
|
34
|
+
should 'raise an error if a primary key part is missing' do
|
35
|
+
item = { :id => 'i', :a => 1, :c => 'c'}
|
36
|
+
assert_raises(Cassandra::Errors::InvalidError) { @client.insert(:test, item) }
|
37
|
+
end
|
38
|
+
|
39
|
+
should 'allow the insertion of columns with mismatched quotes' do
|
40
|
+
item = { :id => 'i', :a => 1, :b => 'b', :c => "'\"item(", :d => 1}
|
41
|
+
@client.insert(:test, item)
|
42
|
+
assert_equal string_keys(item), @client.select(:test).execute.first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'select' do
|
47
|
+
|
48
|
+
setup do
|
49
|
+
@client.insert(:test, :id => 'i', :a => 1, :b => 'b', :c => '1')
|
50
|
+
@client.insert(:test, :id => 'i', :a => 2, :b => 'a', :c => '1')
|
51
|
+
@client.insert(:test, :id => 'i', :a => 3, :b => 'c', :c => '1')
|
52
|
+
@client.insert(:test, :id => 'i2', :a => 4, :b => 'b', :c => '1')
|
53
|
+
end
|
54
|
+
|
55
|
+
should 'select all columns for all items' do
|
56
|
+
items = @client.select(:test).execute
|
57
|
+
assert_equal 4, items.count
|
58
|
+
assert_equal [1,2,3,4].to_set, items.map { |x| x['a'] }.to_set
|
59
|
+
assert_equal %w(id a b c d).to_set, items.first.keys.to_set
|
60
|
+
end
|
61
|
+
|
62
|
+
should 'select certain columns for all items' do
|
63
|
+
items = @client.select(:test, %w(id a c)).execute
|
64
|
+
assert_equal 4, items.count
|
65
|
+
assert_equal [1,2,3,4].to_set, items.map { |x| x['a'] }.to_set
|
66
|
+
assert_equal %w(id a c).to_set, items.first.keys.to_set
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'where' do
|
70
|
+
should 'allow where clause' do
|
71
|
+
items = @client.select(:test).where(:id => 'i').execute
|
72
|
+
assert_equal [1,2,3].to_set, items.map { |x| x['a'] }.to_set
|
73
|
+
end
|
74
|
+
|
75
|
+
should 'allow string-based where clauses' do
|
76
|
+
items = @client.select(:test).where("id = 'i' and a > 1").execute
|
77
|
+
assert_equal [2,3].to_set, items.map { |x| x['a'] }.to_set
|
78
|
+
end
|
79
|
+
|
80
|
+
should 'allow string-based where clauses with arguments' do
|
81
|
+
items = @client.select(:test).where("id = ? and a > ?", 'i', 1).execute
|
82
|
+
assert_equal [2,3].to_set, items.map { |x| x['a'] }.to_set
|
83
|
+
end
|
84
|
+
|
85
|
+
should 'allow multiple where clauses to be chained' do
|
86
|
+
items = @client.select(:test).where(:id => 'i').where('a > 1').execute
|
87
|
+
assert_equal [2,3].to_set, items.map { |x| x['a'] }.to_set
|
88
|
+
end
|
89
|
+
|
90
|
+
should 'create an IN clause when a list of values is passed' do
|
91
|
+
items = @client.select(:test).where(:id => 'i', :a => 1, :b => %w(a b)).execute
|
92
|
+
assert_equal [1], items.map { |x| x['a'] }
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'hash arguments' do
|
96
|
+
should 'allow single and double quotes in the value' do
|
97
|
+
items = @client.select(:test).where(:id => "'\"abc").execute
|
98
|
+
assert_equal [], items.to_a
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'string arguments' do
|
103
|
+
should 'allow single and double quotes in the value' do
|
104
|
+
items = @client.select(:test).where('id = ?', "'\"abc").execute
|
105
|
+
assert_equal [], items.to_a
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
should 'order by ascending primary key by default' do
|
111
|
+
items = @client.select(:test).where(:id => 'i').execute
|
112
|
+
assert_equal [1,2,3], items.map { |x| x['a'] }
|
113
|
+
end
|
114
|
+
|
115
|
+
should 'allow order to specify ordering' do
|
116
|
+
items = @client.select(:test).where(:id => 'i').order(:a, :desc).execute
|
117
|
+
assert_equal [3, 2, 1], items.map { |x| x['a'] }
|
118
|
+
end
|
119
|
+
|
120
|
+
should 'allow limiting the result count' do
|
121
|
+
items = @client.select(:test).where(:id => 'i').limit(2).execute
|
122
|
+
assert_equal [1, 2], items.map { |x| x['a'] }
|
123
|
+
end
|
124
|
+
|
125
|
+
should 'not allow queries across multiple rows if allow_filtering is not set' do
|
126
|
+
assert_raises(Cassandra::Errors::InvalidError) { @client.select(:test).where(:a => 1).execute }
|
127
|
+
end
|
128
|
+
|
129
|
+
should 'allow queries across multiple rows if allow_filtering is set' do
|
130
|
+
items = @client.select(:test).where('a >= 3').allow_filtering.execute
|
131
|
+
assert_equal [3, 4].to_set, items.map { |x| x['a'] }.to_set
|
132
|
+
end
|
133
|
+
|
134
|
+
should 'allow clauses to be chained in any order' do
|
135
|
+
items = @client.select(:test).limit(2).allow_filtering.where('a >= 2').execute
|
136
|
+
assert_equal [2, 4].to_set, items.map { |x| x['a'] }.to_set
|
137
|
+
end
|
138
|
+
|
139
|
+
should 'allow select statements to be modified without affecting the original statement' do
|
140
|
+
partial_query = @client.select(:test).allow_filtering.where(:a => 1)
|
141
|
+
|
142
|
+
items = partial_query.where(:b => 'missing').execute
|
143
|
+
assert_equal 0, items.count
|
144
|
+
|
145
|
+
original_items = partial_query.execute
|
146
|
+
assert_equal 1, original_items.count
|
147
|
+
end
|
148
|
+
|
149
|
+
should 'support count queries' do
|
150
|
+
count = @client.select(:test).where("id = ? and a > ?", 'i', 1).count.execute
|
151
|
+
assert_equal 2, count.first["count"]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'delete' do
|
156
|
+
setup do
|
157
|
+
@client.insert(:test, :id => 'i', :a => 2, :b => 'a', :c => '1', :d => 1)
|
158
|
+
@client.insert(:test, :id => 'i', :a => 3, :b => 'c', :c => '1', :d => 1)
|
159
|
+
end
|
160
|
+
|
161
|
+
should 'delete entire rows' do
|
162
|
+
@client.delete(:test).where(:id => 'i', :a => 2).execute
|
163
|
+
items = @client.select(:test).where(:id => 'i', :a => 2).execute
|
164
|
+
assert_equal 0, items.count
|
165
|
+
end
|
166
|
+
|
167
|
+
should 'delete multiple elements in a partition' do
|
168
|
+
@client.delete(:test).where(:id => 'i').execute
|
169
|
+
items = @client.select(:test).where(:id => 'i').execute
|
170
|
+
assert_equal 0, items.count
|
171
|
+
end
|
172
|
+
|
173
|
+
should 'delete individual columns' do
|
174
|
+
@client.delete(:test, [:c, :d]).where(:id => 'i', :a => 2, :b => 'a').execute
|
175
|
+
items = @client.select(:test).where(:id => 'i', :a => 2).execute
|
176
|
+
assert_equal nil, items.first['c']
|
177
|
+
assert_equal nil, items.first['d']
|
178
|
+
end
|
179
|
+
|
180
|
+
context 'hash arguments' do
|
181
|
+
should 'allow single and double quotes in the value' do
|
182
|
+
# no error raised
|
183
|
+
@client.delete(:test).where(:id => "'\"abc").execute
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'string arguments' do
|
188
|
+
should 'allow single and double quotes in the value' do
|
189
|
+
# no error raised
|
190
|
+
@client.delete(:test).where('id = ?', "'\"abc").execute
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pry'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/should'
|
5
|
+
|
6
|
+
|
7
|
+
require 'cassava'
|
8
|
+
|
9
|
+
class Minitest::Should::TestCase
|
10
|
+
def self.xshould(*args)
|
11
|
+
puts "Disabled test: #{args}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def session_for_keyspace(keyspace = 'test_cassava')
|
16
|
+
c = Cassandra.cluster(port: 9242)
|
17
|
+
c.connect(keyspace)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_test_table
|
21
|
+
sess = session_for_keyspace(nil)
|
22
|
+
sess.execute('DROP KEYSPACE test_cassava') rescue nil
|
23
|
+
sess.execute("CREATE KEYSPACE test_cassava with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }")
|
24
|
+
session_for_keyspace.execute('CREATE TABLE test(id text, a int, b text, c text, d int, primary key ((id), a, b))')
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cassava_rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arron Norwell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-reporters
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: cassandra-driver
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- anorwell@datto.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- cassava_rb.gemspec
|
96
|
+
- lib/cassava.rb
|
97
|
+
- lib/cassava/client.rb
|
98
|
+
- lib/cassava/version.rb
|
99
|
+
- test/cassava/client_test.rb
|
100
|
+
- test/test_helper.rb
|
101
|
+
homepage: ''
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.2.2
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: An unopinionated wrapper for the datastax cassandra gem.
|
125
|
+
test_files:
|
126
|
+
- test/cassava/client_test.rb
|
127
|
+
- test/test_helper.rb
|
128
|
+
has_rdoc:
|