cequel 1.0.4 → 1.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 +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
|