cequel 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Appraisals +3 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +5 -4
- data/Gemfile.lock +215 -22
- data/README.md +19 -6
- data/Vagrantfile +6 -1
- data/lib/cequel.rb +3 -2
- data/lib/cequel/metal/batch.rb +16 -1
- data/lib/cequel/metal/data_set.rb +63 -39
- data/lib/cequel/metal/deleter.rb +2 -2
- data/lib/cequel/metal/incrementer.rb +2 -2
- data/lib/cequel/metal/inserter.rb +8 -6
- data/lib/cequel/metal/keyspace.rb +127 -34
- data/lib/cequel/metal/logger.rb +1 -1
- data/lib/cequel/metal/row.rb +3 -4
- data/lib/cequel/metal/updater.rb +2 -2
- data/lib/cequel/metal/writer.rb +18 -12
- data/lib/cequel/record/associations.rb +7 -1
- data/lib/cequel/record/bound.rb +1 -1
- data/lib/cequel/record/callbacks.rb +10 -6
- data/lib/cequel/record/data_set_builder.rb +13 -2
- data/lib/cequel/record/persistence.rb +14 -12
- data/lib/cequel/record/properties.rb +3 -3
- data/lib/cequel/record/railtie.rb +1 -1
- data/lib/cequel/record/record_set.rb +12 -12
- data/lib/cequel/record/schema.rb +5 -1
- data/lib/cequel/schema/keyspace.rb +1 -1
- data/lib/cequel/schema/table_property.rb +1 -1
- data/lib/cequel/type.rb +44 -10
- data/lib/cequel/uuids.rb +46 -0
- data/lib/cequel/version.rb +1 -1
- data/spec/examples/metal/data_set_spec.rb +93 -0
- data/spec/examples/metal/keyspace_spec.rb +74 -0
- data/spec/examples/record/associations_spec.rb +84 -0
- data/spec/examples/record/persistence_spec.rb +23 -0
- data/spec/examples/record/properties_spec.rb +1 -1
- data/spec/examples/record/record_set_spec.rb +12 -3
- data/spec/examples/record/schema_spec.rb +1 -1
- data/spec/examples/record/secondary_index_spec.rb +1 -1
- data/spec/examples/schema/table_reader_spec.rb +2 -3
- data/spec/examples/spec_helper.rb +8 -0
- data/spec/examples/type_spec.rb +7 -5
- data/spec/examples/uuids_spec.rb +22 -0
- data/spec/support/helpers.rb +25 -19
- data/templates/config/cequel.yml +5 -5
- metadata +40 -33
data/lib/cequel/metal/logger.rb
CHANGED
data/lib/cequel/metal/row.rb
CHANGED
@@ -9,9 +9,9 @@ module Cequel
|
|
9
9
|
#
|
10
10
|
class Row < DelegateClass(ActiveSupport::HashWithIndifferentAccess)
|
11
11
|
#
|
12
|
-
# Encapsulate a row from
|
12
|
+
# Encapsulate a result row from the driver
|
13
13
|
#
|
14
|
-
# @param result_row [
|
14
|
+
# @param result_row [Hash] row from underlying driver
|
15
15
|
# @return [Row] encapsulated row
|
16
16
|
#
|
17
17
|
# @api private
|
@@ -19,8 +19,7 @@ module Cequel
|
|
19
19
|
def self.from_result_row(result_row)
|
20
20
|
if result_row
|
21
21
|
new.tap do |row|
|
22
|
-
|
23
|
-
names.zip(values) do |name, value|
|
22
|
+
result_row.each_pair do |name, value|
|
24
23
|
if name =~ /^(ttl|writetime)\((.+)\)$/
|
25
24
|
if $1 == 'ttl' then row.set_ttl($2, value)
|
26
25
|
else row.set_writetime($2, value)
|
data/lib/cequel/metal/updater.rb
CHANGED
@@ -143,10 +143,10 @@ module Cequel
|
|
143
143
|
super && column_updates.empty?
|
144
144
|
end
|
145
145
|
|
146
|
-
def write_to_statement(statement)
|
146
|
+
def write_to_statement(statement, options)
|
147
147
|
prepare_column_updates
|
148
148
|
statement.append("UPDATE #{table_name}")
|
149
|
-
.append(generate_upsert_options)
|
149
|
+
.append(generate_upsert_options(options))
|
150
150
|
.append(" SET ")
|
151
151
|
.append(statements.join(', '), *bind_vars)
|
152
152
|
end
|
data/lib/cequel/metal/writer.rb
CHANGED
@@ -15,13 +15,8 @@ module Cequel
|
|
15
15
|
|
16
16
|
#
|
17
17
|
# @param data_set [DataSet] data set to write to
|
18
|
-
# @param options [Options] options
|
19
|
-
# @option options [Integer] :ttl time-to-live in seconds for the written
|
20
|
-
# data
|
21
|
-
# @option options [Time,Integer] :timestamp the timestamp associated with
|
22
|
-
# the column values
|
23
18
|
#
|
24
|
-
def initialize(data_set,
|
19
|
+
def initialize(data_set, &block)
|
25
20
|
@data_set, @options, @block = data_set, options, block
|
26
21
|
@statements, @bind_vars = [], []
|
27
22
|
SimpleDelegator.new(self).instance_eval(&block) if block
|
@@ -30,14 +25,24 @@ module Cequel
|
|
30
25
|
#
|
31
26
|
# Execute the statement as a write operation
|
32
27
|
#
|
28
|
+
# @param options [Options] options
|
29
|
+
# @opiton options [Symbol] :consistency what consistency level to use for
|
30
|
+
# the operation
|
31
|
+
# @option options [Integer] :ttl time-to-live in seconds for the written
|
32
|
+
# data
|
33
|
+
# @option options [Time,Integer] :timestamp the timestamp associated with
|
34
|
+
# the column values
|
33
35
|
# @return [void]
|
34
36
|
#
|
35
|
-
def execute
|
37
|
+
def execute(options = {})
|
38
|
+
options.assert_valid_keys(:timestamp, :ttl, :consistency)
|
36
39
|
return if empty?
|
37
40
|
statement = Statement.new
|
38
|
-
|
41
|
+
consistency = options.fetch(:consistency, data_set.query_consistency)
|
42
|
+
write_to_statement(statement, options)
|
39
43
|
statement.append(*data_set.row_specifications_cql)
|
40
|
-
data_set.
|
44
|
+
data_set.write_with_consistency(
|
45
|
+
statement.cql, statement.bind_vars, consistency)
|
41
46
|
end
|
42
47
|
|
43
48
|
private
|
@@ -63,12 +68,13 @@ module Cequel
|
|
63
68
|
#
|
64
69
|
# Generate CQL option statement for inserts and updates
|
65
70
|
#
|
66
|
-
def generate_upsert_options
|
67
|
-
|
71
|
+
def generate_upsert_options(options)
|
72
|
+
upsert_options = options.slice(:timestamp, :ttl)
|
73
|
+
if upsert_options.empty?
|
68
74
|
''
|
69
75
|
else
|
70
76
|
' USING ' <<
|
71
|
-
|
77
|
+
upsert_options.map do |key, value|
|
72
78
|
serialized_value =
|
73
79
|
case key
|
74
80
|
when :timestamp then (value.to_f * 1_000_000).to_i
|
@@ -82,6 +82,10 @@ module Cequel
|
|
82
82
|
# has a primary key `(subdomain)`, this will declare a key column
|
83
83
|
# `blog_subdomain` of the same type.
|
84
84
|
#
|
85
|
+
# If the parent class has multiple keys, e.g. it belongs to a parent
|
86
|
+
# class, defining a `partition: true` option will declare all of the
|
87
|
+
# parent's keys as partition key columns for this class.
|
88
|
+
#
|
85
89
|
# Parent associations are read/write, so declaring `belongs_to :blog`
|
86
90
|
# will define a `blog` getter and `blog=` setter, which will update the
|
87
91
|
# underlying key column. Note that a record's parent cannot be changed
|
@@ -104,12 +108,14 @@ module Cequel
|
|
104
108
|
"belongs_to association must be declared before declaring " \
|
105
109
|
"key(s)"
|
106
110
|
end
|
111
|
+
|
112
|
+
key_options = options.extract!(:partition)
|
107
113
|
|
108
114
|
self.parent_association =
|
109
115
|
BelongsToAssociation.new(self, name.to_sym, options)
|
110
116
|
|
111
117
|
parent_association.association_key_columns.each do |column|
|
112
|
-
key :"#{name}_#{column.name}", column.type
|
118
|
+
key :"#{name}_#{column.name}", column.type, key_options
|
113
119
|
end
|
114
120
|
def_parent_association_accessors
|
115
121
|
end
|
data/lib/cequel/record/bound.rb
CHANGED
@@ -31,21 +31,25 @@ module Cequel
|
|
31
31
|
|
32
32
|
# (see Persistence#save)
|
33
33
|
def save(options = {})
|
34
|
-
connection.batch
|
34
|
+
connection.batch(options.slice(:consistency)) do
|
35
|
+
run_callbacks(:save) { super }
|
36
|
+
end
|
35
37
|
end
|
36
38
|
|
37
|
-
# (see Persistence#
|
38
|
-
def destroy
|
39
|
-
connection.batch
|
39
|
+
# (see Persistence#destroy)
|
40
|
+
def destroy(options = {})
|
41
|
+
connection.batch(options.slice(:consistency)) do
|
42
|
+
run_callbacks(:destroy) { super }
|
43
|
+
end
|
40
44
|
end
|
41
45
|
|
42
46
|
protected
|
43
47
|
|
44
|
-
def create
|
48
|
+
def create(*)
|
45
49
|
run_callbacks(:create) { super }
|
46
50
|
end
|
47
51
|
|
48
|
-
def update
|
52
|
+
def update(*)
|
49
53
|
run_callbacks(:update) { super }
|
50
54
|
end
|
51
55
|
end
|
@@ -14,7 +14,7 @@ module Cequel
|
|
14
14
|
# Build a data set for the given record set
|
15
15
|
#
|
16
16
|
# @param (see #initialize)
|
17
|
-
# @return
|
17
|
+
# @return (see #build)
|
18
18
|
#
|
19
19
|
def self.build_for(record_set)
|
20
20
|
new(record_set).build
|
@@ -30,12 +30,16 @@ module Cequel
|
|
30
30
|
end
|
31
31
|
private_class_method :new
|
32
32
|
|
33
|
+
#
|
34
|
+
# @return [Metal::DataSet] a DataSet exposing the rows for the record set
|
35
|
+
#
|
33
36
|
def build
|
34
37
|
add_limit
|
35
38
|
add_select_columns
|
36
39
|
add_where_statement
|
37
40
|
add_bounds
|
38
41
|
add_order
|
42
|
+
set_consistency
|
39
43
|
data_set
|
40
44
|
end
|
41
45
|
|
@@ -46,7 +50,8 @@ module Cequel
|
|
46
50
|
def_delegators :record_set, :row_limit, :select_columns,
|
47
51
|
:scoped_key_names, :scoped_key_values,
|
48
52
|
:scoped_indexed_column, :lower_bound,
|
49
|
-
:upper_bound, :reversed?, :order_by_column
|
53
|
+
:upper_bound, :reversed?, :order_by_column,
|
54
|
+
:query_consistency
|
50
55
|
|
51
56
|
private
|
52
57
|
|
@@ -82,6 +87,12 @@ module Cequel
|
|
82
87
|
def add_order
|
83
88
|
self.data_set = data_set.order(order_by_column => :desc) if reversed?
|
84
89
|
end
|
90
|
+
|
91
|
+
def set_consistency
|
92
|
+
if query_consistency
|
93
|
+
self.data_set = data_set.consistency(query_consistency)
|
94
|
+
end
|
95
|
+
end
|
85
96
|
end
|
86
97
|
end
|
87
98
|
end
|
@@ -171,9 +171,9 @@ module Cequel
|
|
171
171
|
# @see Validations#save!
|
172
172
|
#
|
173
173
|
def save(options = {})
|
174
|
-
options.assert_valid_keys
|
175
|
-
if new_record? then create
|
176
|
-
else update
|
174
|
+
options.assert_valid_keys(:consistency)
|
175
|
+
if new_record? then create(options)
|
176
|
+
else update(options)
|
177
177
|
end
|
178
178
|
@new_record = false
|
179
179
|
true
|
@@ -199,9 +199,10 @@ module Cequel
|
|
199
199
|
#
|
200
200
|
# @return [Record] self
|
201
201
|
#
|
202
|
-
def destroy
|
202
|
+
def destroy(options = {})
|
203
|
+
options.assert_valid_keys(:consistency)
|
203
204
|
assert_keys_present!
|
204
|
-
metal_scope.delete
|
205
|
+
metal_scope.delete(options)
|
205
206
|
transient!
|
206
207
|
self
|
207
208
|
end
|
@@ -252,28 +253,29 @@ module Cequel
|
|
252
253
|
self
|
253
254
|
end
|
254
255
|
|
255
|
-
def create
|
256
|
+
def create(options = {})
|
256
257
|
assert_keys_present!
|
257
|
-
metal_scope
|
258
|
+
metal_scope
|
259
|
+
.insert(attributes.reject { |attr, value| value.nil? }, options)
|
258
260
|
loaded!
|
259
261
|
persisted!
|
260
262
|
end
|
261
263
|
|
262
|
-
def update
|
264
|
+
def update(options = {})
|
263
265
|
assert_keys_present!
|
264
266
|
connection.batch do
|
265
|
-
updater.execute
|
266
|
-
deleter.execute
|
267
|
+
updater.execute(options)
|
268
|
+
deleter.execute(options)
|
267
269
|
@updater, @deleter = nil
|
268
270
|
end
|
269
271
|
end
|
270
272
|
|
271
273
|
def updater
|
272
|
-
@updater ||= metal_scope
|
274
|
+
@updater ||= Metal::Updater.new(metal_scope)
|
273
275
|
end
|
274
276
|
|
275
277
|
def deleter
|
276
|
-
@deleter ||= metal_scope
|
278
|
+
@deleter ||= Metal::Deleter.new(metal_scope)
|
277
279
|
end
|
278
280
|
|
279
281
|
private
|
@@ -101,7 +101,7 @@ module Cequel
|
|
101
101
|
unless Type[type].is_a?(Cequel::Type::Uuid)
|
102
102
|
fail ArgumentError, ":auto option only valid for UUID columns"
|
103
103
|
end
|
104
|
-
default = -> {
|
104
|
+
default = -> { Cequel.uuid } if options[:auto]
|
105
105
|
end
|
106
106
|
set_attribute_default(name, default)
|
107
107
|
end
|
@@ -300,8 +300,8 @@ module Cequel
|
|
300
300
|
#
|
301
301
|
def inspect
|
302
302
|
inspected_attributes = attributes.each_pair.map do |attr, value|
|
303
|
-
inspected_value =
|
304
|
-
value.
|
303
|
+
inspected_value = Cequel.uuid?(value) ?
|
304
|
+
value.to_s :
|
305
305
|
value.inspect
|
306
306
|
"#{attr}: #{inspected_value}"
|
307
307
|
end
|
@@ -20,7 +20,7 @@ module Cequel
|
|
20
20
|
config = YAML.load(ERB.new(IO.read(config_path)).result)[Rails.env]
|
21
21
|
.deep_symbolize_keys
|
22
22
|
else
|
23
|
-
config = {host: '127.0.0.1:
|
23
|
+
config = {host: '127.0.0.1:9042'}
|
24
24
|
end
|
25
25
|
config.reverse_merge!(keyspace: "#{Railtie.app_name}_#{Rails.env}")
|
26
26
|
connection = Cequel.connect(config)
|
@@ -454,6 +454,16 @@ module Cequel
|
|
454
454
|
scoped(reversed: !reversed?)
|
455
455
|
end
|
456
456
|
|
457
|
+
#
|
458
|
+
# Set the consistency at which to read records into the record set.
|
459
|
+
#
|
460
|
+
# @param consistency [Symbol] consistency for reads
|
461
|
+
# @return [RecordSet] record set tuned to given consistency
|
462
|
+
#
|
463
|
+
def consistency(consistency)
|
464
|
+
scoped(query_consistency: consistency)
|
465
|
+
end
|
466
|
+
|
457
467
|
#
|
458
468
|
# @overload first
|
459
469
|
# @return [Record] the first record in this record set
|
@@ -634,9 +644,9 @@ module Cequel
|
|
634
644
|
attr_reader :attributes
|
635
645
|
hattr_reader :attributes, :select_columns, :scoped_key_values,
|
636
646
|
:row_limit, :lower_bound, :upper_bound,
|
637
|
-
:scoped_indexed_column
|
647
|
+
:scoped_indexed_column, :query_consistency
|
638
648
|
protected :select_columns, :scoped_key_values, :row_limit, :lower_bound,
|
639
|
-
:upper_bound, :scoped_indexed_column
|
649
|
+
:upper_bound, :scoped_indexed_column, :query_consistency
|
640
650
|
hattr_inquirer :attributes, :reversed
|
641
651
|
protected :reversed?
|
642
652
|
|
@@ -790,16 +800,6 @@ module Cequel
|
|
790
800
|
Bound.create(range_key_column, gt, inclusive, value)
|
791
801
|
end
|
792
802
|
|
793
|
-
def cast_range_key_for_bound(value)
|
794
|
-
if range_key_column.type?(Type::Timeuuid) &&
|
795
|
-
!value.is_a?(CassandraCQL::UUID)
|
796
|
-
|
797
|
-
Type::Timestamp.instance.cast(value)
|
798
|
-
else
|
799
|
-
cast_range_key(value)
|
800
|
-
end
|
801
|
-
end
|
802
|
-
|
803
803
|
def load!
|
804
804
|
fail ArgumentError, "Not all primary key columns have specified values"
|
805
805
|
end
|
data/lib/cequel/record/schema.rb
CHANGED
@@ -48,6 +48,9 @@ module Cequel
|
|
48
48
|
# @!attribute [r] partition_key_columns
|
49
49
|
# (see Cequel::Schema::Table#partition_key_columns)
|
50
50
|
#
|
51
|
+
# @!attribute [r] partition_key_column_names
|
52
|
+
# (see Cequel::Schema::Table#partition_key_column_names)
|
53
|
+
#
|
51
54
|
# @!attribute [r] clustering_columns
|
52
55
|
# (see Cequel::Schema::Table#clustering_columns)
|
53
56
|
#
|
@@ -56,7 +59,8 @@ module Cequel
|
|
56
59
|
#
|
57
60
|
def_delegators :table_schema, :columns, :key_columns,
|
58
61
|
:key_column_names, :partition_key_columns,
|
59
|
-
:
|
62
|
+
:partition_key_column_names, :clustering_columns,
|
63
|
+
:compact_storage?
|
60
64
|
#
|
61
65
|
# @!method reflect_on_column(name)
|
62
66
|
# (see Cequel::Schema::Table#column)
|
data/lib/cequel/type.rb
CHANGED
@@ -73,12 +73,39 @@ module Cequel
|
|
73
73
|
raise UnknownType, "Unrecognized internal type #{internal_name.inspect}"
|
74
74
|
end
|
75
75
|
|
76
|
+
#
|
77
|
+
# Quote an arbitrary value for use in a CQL statement by inferring the
|
78
|
+
# equivalent CQL type to the value's Ruby type
|
79
|
+
#
|
80
|
+
# @return [String] quoted value
|
81
|
+
#
|
82
|
+
def self.quote(value)
|
83
|
+
if value.is_a?(Array)
|
84
|
+
return value.map { |element| quote(element) }.join(',')
|
85
|
+
end
|
86
|
+
case value
|
87
|
+
when ::String
|
88
|
+
if value.encoding == Encoding::ASCII_8BIT
|
89
|
+
"0x#{value}"
|
90
|
+
else
|
91
|
+
"'#{value.gsub("'", "''")}'"
|
92
|
+
end
|
93
|
+
when Date, Time, ActiveSupport::TimeWithZone
|
94
|
+
quote(value.to_i * 1000 + value.usec / 1000)
|
95
|
+
when Numeric, true, false, Cql::Uuid
|
96
|
+
value.to_s
|
97
|
+
else
|
98
|
+
quote(value.to_s)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
76
102
|
#
|
77
103
|
# The base class for all type objects. Types are singletons.
|
78
104
|
#
|
79
105
|
# @abstract Subclasses should implement {#cast}, and may implement
|
80
|
-
# {#internal_names} if it cannot be inferred from the class name.
|
81
|
-
# name of the type class should be the camel-cased CQL name of the
|
106
|
+
# {#internal_names} if it cannot be inferred from the class name.
|
107
|
+
# The name of the type class should be the camel-cased CQL name of the
|
108
|
+
# type
|
82
109
|
#
|
83
110
|
class Base
|
84
111
|
include Singleton
|
@@ -367,10 +394,9 @@ module Cequel
|
|
367
394
|
register Timestamp.instance
|
368
395
|
|
369
396
|
#
|
370
|
-
# `uuid` columns store type 1 and type 4 UUIDs.
|
371
|
-
#
|
372
|
-
#
|
373
|
-
# supported as inputs.
|
397
|
+
# `uuid` columns store type 1 and type 4 UUIDs. New UUID instances can be
|
398
|
+
# created using the {Cequel.uuid} method, and a value can be checked to see
|
399
|
+
# if it is a UUID recognized by Cequel using the {Cequel.uuid?} method.
|
374
400
|
#
|
375
401
|
# @see http://cassandra.apache.org/doc/cql3/CQL.html#types
|
376
402
|
# CQL3 data type documentation
|
@@ -381,10 +407,14 @@ module Cequel
|
|
381
407
|
end
|
382
408
|
|
383
409
|
def cast(value)
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
410
|
+
if value.is_a? Cql::Uuid then value
|
411
|
+
elsif defined?(SimpleUUID::UUID) && value.is_a?(SimpleUUID::UUID)
|
412
|
+
Cql::Uuid.new(value.to_i)
|
413
|
+
elsif value.is_a?(::Integer) || value.is_a?(::String)
|
414
|
+
Cql::Uuid.new(value)
|
415
|
+
else
|
416
|
+
fail ArgumentError,
|
417
|
+
"Don't know how to cast #{value.inspect} to a UUID"
|
388
418
|
end
|
389
419
|
end
|
390
420
|
end
|
@@ -401,6 +431,10 @@ module Cequel
|
|
401
431
|
# CQL3 data type documentation
|
402
432
|
#
|
403
433
|
class Timeuuid < Uuid
|
434
|
+
def cast(value)
|
435
|
+
Cql::TimeUuid.new(super.value)
|
436
|
+
end
|
437
|
+
|
404
438
|
def internal_names
|
405
439
|
['org.apache.cassandra.db.marshal.TimeUUIDType']
|
406
440
|
end
|