cassandra-mapper 0.1
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 +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +8 -0
- data/Rakefile +4 -0
- data/cassandra-mapper.gemspec +19 -0
- data/lib/cassandra/mapper.rb +41 -0
- data/lib/cassandra/mapper/convert.rb +95 -0
- data/lib/cassandra/mapper/data/insert.rb +28 -0
- data/lib/cassandra/mapper/data/remove.rb +18 -0
- data/lib/cassandra/mapper/data/request.rb +49 -0
- data/lib/cassandra/mapper/data/response.rb +40 -0
- data/lib/cassandra/mapper/extend/migrate.rb +31 -0
- data/lib/cassandra/mapper/extend/queries.rb +56 -0
- data/lib/cassandra/mapper/extend/schema.rb +21 -0
- data/lib/cassandra/mapper/utility/config.rb +56 -0
- data/lib/cassandra/mapper/utility/delegate_keys.rb +13 -0
- data/lib/cassandra/mapper/utility/hash.rb +22 -0
- data/lib/cassandra/mapper/utility/store_instances.rb +13 -0
- data/spec/cassandra/mapper/convert_spec.rb +44 -0
- data/spec/cassandra/mapper/utility/config_spec.rb +15 -0
- data/spec/cassandra/mapper/utility/store_instances_spec.rb +16 -0
- data/spec/cassandra/mapper_spec.rb +231 -0
- data/spec/spec_helper.rb +11 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f2057b9a950ab60c08bff745c6a8cf5fc2492d98
|
4
|
+
data.tar.gz: 729c987811f2ab5caa1ebff77ea538977ff2a4e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 25cc8d2cc84656054045c228ef11e49e93a407acf514ed5bf5ddf8b55a9f970c3d471f2f5f23cf18fc94396ada1b710e2082955122d08bb3e81aaed3d42264c7
|
7
|
+
data.tar.gz: c37205509f6be2ba0cf25faf0573f74fa0fe0810a3680cbe57029a1360750f627db801789b294f293ef71060047cd4556391e8a95387ad15a3dc88d39563073a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9.3
|
4
|
+
- 2.0.0
|
5
|
+
- jruby-19mode
|
6
|
+
- rbx-19mode
|
7
|
+
services:
|
8
|
+
- cassandra
|
9
|
+
matrix:
|
10
|
+
allow_failures:
|
11
|
+
- rvm: rbx-19mode
|
12
|
+
before_install:
|
13
|
+
- sudo sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Djava.net.preferIPv4Stack=false\"' >> /usr/local/cassandra/conf/cassandra-env.sh"
|
14
|
+
- sudo service cassandra start
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 brainopia
|
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,8 @@
|
|
1
|
+
# Cassandra::Mapper
|
2
|
+
|
3
|
+
Abstractions to work with cassandra. Currently under development.
|
4
|
+
|
5
|
+
[](http://travis-ci.org/brainopia/cassandra-mapper)
|
7
|
+
[](https://codeclimate.com/github/brainopia/cassandra-mapper)
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = 'cassandra-mapper'
|
3
|
+
gem.version = '0.1'
|
4
|
+
gem.authors = 'brainopia'
|
5
|
+
gem.email = 'brainopia@evilmartians.com'
|
6
|
+
gem.homepage = 'https://github.com/brainopia/cassandra-mapper'
|
7
|
+
gem.summary = 'Cassandra mapper'
|
8
|
+
gem.description = <<-DESCRIPTION
|
9
|
+
Work with cassandra in datamapper style.
|
10
|
+
DESCRIPTION
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($/)
|
13
|
+
gem.test_files = gem.files.grep %r{^spec/}
|
14
|
+
gem.require_paths = %w(lib)
|
15
|
+
|
16
|
+
gem.add_dependency 'cassandra', '~> 0.18.0'
|
17
|
+
gem.add_dependency 'multi_json', '~> 1.0'
|
18
|
+
gem.add_development_dependency 'rspec'
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'time'
|
3
|
+
require 'cassandra'
|
4
|
+
|
5
|
+
Cassandra::THRIFT_DEFAULTS.merge! \
|
6
|
+
connect_timeout: 1, timeout: 1
|
7
|
+
|
8
|
+
class Cassandra::Mapper
|
9
|
+
require_relative 'mapper/convert'
|
10
|
+
require_relative 'mapper/data/request'
|
11
|
+
require_relative 'mapper/data/insert'
|
12
|
+
require_relative 'mapper/data/remove'
|
13
|
+
require_relative 'mapper/data/response'
|
14
|
+
|
15
|
+
require_relative 'mapper/extend/schema'
|
16
|
+
require_relative 'mapper/extend/migrate'
|
17
|
+
require_relative 'mapper/extend/queries'
|
18
|
+
|
19
|
+
require_relative 'mapper/utility/hash'
|
20
|
+
require_relative 'mapper/utility/delegate_keys'
|
21
|
+
require_relative 'mapper/utility/config'
|
22
|
+
require_relative 'mapper/utility/store_instances'
|
23
|
+
|
24
|
+
extend Utility::StoreInstances
|
25
|
+
|
26
|
+
attr_reader :table, :config
|
27
|
+
|
28
|
+
def initialize(keyspace, table, &block)
|
29
|
+
@keyspace = keyspace.to_s
|
30
|
+
@table = table.to_s
|
31
|
+
@config = Utility::Config.new(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def keyspace
|
35
|
+
Thread.current["keyspace_#{keyspace_name}"] ||= Cassandra.new keyspace_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def keyspace_name
|
39
|
+
"#{@keyspace}_#{env}"
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Cassandra::Mapper::Convert
|
2
|
+
extend self
|
3
|
+
|
4
|
+
TEXT_TYPE = 'UTF8Type'
|
5
|
+
TYPES = {
|
6
|
+
uuid: 'TimeUUIDType',
|
7
|
+
integer: 'Int32Type',
|
8
|
+
time: 'DateType'
|
9
|
+
}
|
10
|
+
|
11
|
+
def type(symbol)
|
12
|
+
TYPES.fetch symbol, TEXT_TYPE
|
13
|
+
end
|
14
|
+
|
15
|
+
def to(type, value)
|
16
|
+
send "to_#{type}", value
|
17
|
+
end
|
18
|
+
|
19
|
+
def from(type, value)
|
20
|
+
send "from_#{type}", value
|
21
|
+
end
|
22
|
+
|
23
|
+
def round(type, value)
|
24
|
+
from type, to(type, value)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def to_(value)
|
30
|
+
value.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def from_(value)
|
34
|
+
value.force_encoding Encoding::UTF_8
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_uuid(value)
|
38
|
+
value = Time.parse value if value.is_a? String
|
39
|
+
SimpleUUID::UUID.new(value).to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def from_uuid(value)
|
43
|
+
SimpleUUID::UUID.new(value).to_time
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_integer(value)
|
47
|
+
[value.to_i].pack('N')
|
48
|
+
end
|
49
|
+
|
50
|
+
def from_integer(value)
|
51
|
+
value.unpack('N').first
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_boolean(value)
|
55
|
+
value ? 'on' : 'off'
|
56
|
+
end
|
57
|
+
|
58
|
+
def from_boolean(value)
|
59
|
+
value == 'on' ? true : false
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_time(value)
|
63
|
+
value = Time.parse value if value.is_a? String
|
64
|
+
value = value.to_time if value.is_a? Date
|
65
|
+
[(value.to_f * 1000).to_i].pack('Q>')
|
66
|
+
end
|
67
|
+
|
68
|
+
def from_time(value)
|
69
|
+
Time.at(value.unpack('Q>').first.to_f / 1000)
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_decimal(value)
|
73
|
+
value.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
def from_decimal(value)
|
77
|
+
BigDecimal value
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_json(value)
|
81
|
+
MultiJson.dump value
|
82
|
+
end
|
83
|
+
|
84
|
+
def from_json(value)
|
85
|
+
MultiJson.load value
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_float(value)
|
89
|
+
value.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
def from_float(value)
|
93
|
+
value.to_f
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Cassandra::Mapper::Data
|
2
|
+
class Insert < Request
|
3
|
+
def initialize(_config, data)
|
4
|
+
@request = data.dup
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def convert!(data)
|
9
|
+
config.before_insert.each {|it| it.call data }
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def return!
|
14
|
+
converted.tap do |data|
|
15
|
+
config.after_insert.each {|it| it.call data }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def converted
|
22
|
+
@request.each_with_object({}) do |(field, value), converted|
|
23
|
+
next unless value
|
24
|
+
converted[field] = Cassandra::Mapper::Convert.round config.types[field], value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Cassandra::Mapper::Data
|
2
|
+
class Remove < Insert
|
3
|
+
def convert!(data)
|
4
|
+
config.before_remove.each {|it| it.call data }
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def columns
|
9
|
+
super.keys
|
10
|
+
end
|
11
|
+
|
12
|
+
def return!
|
13
|
+
converted.tap do |data|
|
14
|
+
config.after_remove.each {|it| it.call data }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Cassandra::Mapper::Data
|
2
|
+
class Request
|
3
|
+
KEY_SEPARATOR = '##'
|
4
|
+
|
5
|
+
attr_reader :keys, :subkeys, :data, :config
|
6
|
+
|
7
|
+
def initialize(config, data)
|
8
|
+
@config = config
|
9
|
+
@data = convert! data.dup
|
10
|
+
@keys = extract! :key
|
11
|
+
@subkeys = extract! :subkey
|
12
|
+
end
|
13
|
+
|
14
|
+
def packed_keys
|
15
|
+
keys.join(KEY_SEPARATOR).force_encoding('binary')
|
16
|
+
end
|
17
|
+
|
18
|
+
def columns
|
19
|
+
fields = data.empty? ? { '' => '' } : data
|
20
|
+
fields.each_with_object({}) do |(field, value), columns|
|
21
|
+
columns[composite *subkeys, field.to_s] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def query(offset=nil)
|
26
|
+
return { start: offset } if offset
|
27
|
+
return if subkeys.all? &:empty?
|
28
|
+
{ start: composite(*subkeys),
|
29
|
+
finish: composite(*subkeys, slice: :after) }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def composite(*args)
|
35
|
+
Cassandra::Composite.new *args
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract!(option)
|
39
|
+
config.send(option).map {|it| data.delete(it).to_s }
|
40
|
+
end
|
41
|
+
|
42
|
+
def convert!(data)
|
43
|
+
data.delete_if {|_, value| value.nil? }
|
44
|
+
data.each do |field, value|
|
45
|
+
data[field] = Cassandra::Mapper::Convert.to config.types[field], value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Cassandra::Mapper::Data
|
2
|
+
class Response
|
3
|
+
attr_reader :config, :key_values, :columns
|
4
|
+
|
5
|
+
def initialize(config, key_values, columns)
|
6
|
+
@config = config
|
7
|
+
@key_values = key_values
|
8
|
+
@columns = columns
|
9
|
+
end
|
10
|
+
|
11
|
+
def keys
|
12
|
+
Hash[config.key.zip key_values]
|
13
|
+
end
|
14
|
+
|
15
|
+
def subkeys(values)
|
16
|
+
Hash[config.subkey.zip values]
|
17
|
+
end
|
18
|
+
|
19
|
+
def unpack
|
20
|
+
return [] if columns.empty?
|
21
|
+
records = columns.group_by {|composite, _| composite[0..-2] }
|
22
|
+
records.map do |subkey_values, fields|
|
23
|
+
record = keys.merge subkeys(subkey_values)
|
24
|
+
fields.each do |composite, value|
|
25
|
+
field = composite[-1]
|
26
|
+
record[field.to_sym] = value unless field.empty?
|
27
|
+
end
|
28
|
+
convert! record
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def convert!(data)
|
35
|
+
data.each do |field, value|
|
36
|
+
data[field] = Cassandra::Mapper::Convert.from config.types[field], value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Cassandra::Mapper
|
2
|
+
def self.migrate
|
3
|
+
cassandra = Cassandra.new('system')
|
4
|
+
schema[:keyspaces].each do |name|
|
5
|
+
options = schema.fetch(env, {}).fetch(name, {})
|
6
|
+
options = Utility::Hash.stringify_keys options
|
7
|
+
strategy = options.delete('strategy') || 'SimpleStrategy'
|
8
|
+
options['replication_factor'] = options.fetch('replication_factor', 1).to_s
|
9
|
+
|
10
|
+
cassandra.add_keyspace Cassandra::Keyspace.new \
|
11
|
+
name: "#{name}_#{env}",
|
12
|
+
strategy_class: strategy,
|
13
|
+
strategy_options: options,
|
14
|
+
cf_defs: []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def migrate
|
19
|
+
subkey_types = config.subkey.map do |it|
|
20
|
+
Convert.type config.types[it]
|
21
|
+
end
|
22
|
+
|
23
|
+
# field subkey
|
24
|
+
subkey_types.push Convert::TEXT_TYPE
|
25
|
+
|
26
|
+
keyspace.add_column_family Cassandra::ColumnFamily.new \
|
27
|
+
keyspace: keyspace.keyspace,
|
28
|
+
name: table,
|
29
|
+
comparator_type: "CompositeType(#{subkey_types.join ','})"
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class Cassandra::Mapper
|
2
|
+
BATCH_SIZE = 500
|
3
|
+
|
4
|
+
def insert(hash)
|
5
|
+
data = Data::Insert.new config, hash
|
6
|
+
keyspace.insert table, data.packed_keys, data.columns
|
7
|
+
data.return!
|
8
|
+
end
|
9
|
+
|
10
|
+
def remove(hash)
|
11
|
+
data = Data::Remove.new config, hash
|
12
|
+
keyspace.remove table, data.packed_keys, data.columns
|
13
|
+
data.return!
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(query)
|
17
|
+
request = Data::Request.new config, query
|
18
|
+
columns = columns_for request
|
19
|
+
response = Data::Response.new config, request.keys, columns
|
20
|
+
response.unpack
|
21
|
+
end
|
22
|
+
|
23
|
+
def one(keys)
|
24
|
+
get(keys).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(&block)
|
28
|
+
keyspace.each_key table do |packed_keys|
|
29
|
+
keys = unpack_keys packed_keys
|
30
|
+
get(keys).each &block
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def all
|
35
|
+
to_enum.to_a
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def columns_for(request, offset=nil)
|
41
|
+
columns = keyspace.get table, request.packed_keys, request.query(offset)
|
42
|
+
columns ||= {}
|
43
|
+
if columns.size == BATCH_SIZE
|
44
|
+
columns.merge! columns_for(request, columns.keys.last)
|
45
|
+
end
|
46
|
+
columns
|
47
|
+
end
|
48
|
+
|
49
|
+
def unpack_keys(packed_keys)
|
50
|
+
keys = packed_keys.split Data::Request::KEY_SEPARATOR
|
51
|
+
keys = Hash[config.key.zip(keys)]
|
52
|
+
keys.each do |field, value|
|
53
|
+
keys[field] = Convert.from config.types[field], value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Cassandra::Mapper
|
2
|
+
def self.schema
|
3
|
+
@@schema
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.schema=(schema)
|
7
|
+
@@schema = Utility::Hash.symbolize_keys schema
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.env
|
11
|
+
@@env
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.env=(env)
|
15
|
+
@@env = env.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
def env
|
19
|
+
@@env
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Cassandra::Mapper::Utility
|
2
|
+
class Config
|
3
|
+
extend DelegateKeys
|
4
|
+
delegate_keys 'dsl.options', :key, :subkey, :types, :before_insert,
|
5
|
+
:after_insert, :after_remove, :before_remove
|
6
|
+
|
7
|
+
attr_reader :dsl
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
@dsl = DSL.new &block
|
11
|
+
end
|
12
|
+
|
13
|
+
class DSL
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
def initialize(&block)
|
17
|
+
@options = {
|
18
|
+
types: {},
|
19
|
+
before_insert: [],
|
20
|
+
after_insert: [],
|
21
|
+
after_remove: [],
|
22
|
+
before_remove: []
|
23
|
+
}
|
24
|
+
instance_eval &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def key(*fields)
|
28
|
+
@options[:key] = fields
|
29
|
+
end
|
30
|
+
|
31
|
+
def subkey(*fields)
|
32
|
+
@options[:subkey] = fields
|
33
|
+
end
|
34
|
+
|
35
|
+
def type(field, type)
|
36
|
+
@options[:types][field] = type
|
37
|
+
end
|
38
|
+
|
39
|
+
def before_insert(&block)
|
40
|
+
@options[:before_insert].push block
|
41
|
+
end
|
42
|
+
|
43
|
+
def before_remove(&block)
|
44
|
+
@options[:before_remove].push block
|
45
|
+
end
|
46
|
+
|
47
|
+
def after_insert(&block)
|
48
|
+
@options[:after_insert].push block
|
49
|
+
end
|
50
|
+
|
51
|
+
def after_remove(&block)
|
52
|
+
@options[:after_remove].push block
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Cassandra::Mapper::Utility
|
2
|
+
module Hash
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def symbolize_keys(hash)
|
6
|
+
map_hash_key hash, &:to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
def stringify_keys(hash)
|
10
|
+
map_hash_key hash, &:to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def map_hash_key(hash)
|
16
|
+
hash.keys.each do |key|
|
17
|
+
hash[yield key] = hash.delete key
|
18
|
+
end
|
19
|
+
hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cassandra::Mapper::Convert do
|
4
|
+
context 'uuid' do
|
5
|
+
let(:time) { Time.now.round }
|
6
|
+
|
7
|
+
it '#to' do
|
8
|
+
subject.to(:uuid, time).should have(16).bytes
|
9
|
+
end
|
10
|
+
|
11
|
+
it '#from' do
|
12
|
+
uuid = subject.to(:uuid, time)
|
13
|
+
subject.from(:uuid, uuid).should == time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'integer' do
|
18
|
+
let(:number) { 17 }
|
19
|
+
let(:cassandra_format) { "\x00\x00\x00\x11" }
|
20
|
+
|
21
|
+
it '#to' do
|
22
|
+
subject.to(:integer, number).should == cassandra_format
|
23
|
+
end
|
24
|
+
|
25
|
+
it '#from' do
|
26
|
+
subject.from(:integer, cassandra_format) == number
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'time' do
|
31
|
+
context 'empty' do
|
32
|
+
it '#to' do
|
33
|
+
expect { subject.to(:time, '') }.to raise_error(ArgumentError)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:time) { Time.at((Time.now.to_f * 1000).to_i / 1000) }
|
38
|
+
|
39
|
+
it '#from' do
|
40
|
+
formatted = subject.to(:time, time)
|
41
|
+
subject.from(:time, formatted).should == time
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cassandra::Mapper::Utility::Config do
|
4
|
+
subject do
|
5
|
+
described_class.new do
|
6
|
+
key :key1, :key2
|
7
|
+
subkey :subkey1, :subkey2
|
8
|
+
type :field, :integer
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
its(:key) { should == [:key1, :key2] }
|
13
|
+
its(:subkey) { should == [:subkey1, :subkey2] }
|
14
|
+
its(:types) { should == { field: :integer }}
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cassandra::Mapper::Utility::StoreInstances do
|
4
|
+
let(:subject) { Class.new.tap {|it| it.extend described_class }}
|
5
|
+
|
6
|
+
it 'should initialize instances with array' do
|
7
|
+
subject.instances.should == []
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should track new instances' do
|
11
|
+
object1 = subject.new
|
12
|
+
object2 = subject.new
|
13
|
+
|
14
|
+
subject.instances.should == [object1, object2]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cassandra::Mapper do
|
4
|
+
subject do
|
5
|
+
described_class.new :mapper, table, &definition
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
subject.keyspace.drop_column_family table.to_s rescue nil
|
10
|
+
subject.migrate
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:table) { :common }
|
14
|
+
|
15
|
+
context 'one subkey' do
|
16
|
+
let :definition do
|
17
|
+
proc do
|
18
|
+
key :field1
|
19
|
+
subkey :field2
|
20
|
+
type :field1, :integer
|
21
|
+
type :field2, :integer
|
22
|
+
type :field3, :integer
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context '.insert/.get' do
|
27
|
+
let(:field1) { 1 }
|
28
|
+
let(:field2) { 2 }
|
29
|
+
let(:field3) { 3 }
|
30
|
+
let(:keys) {{ field1: field1, field2: field2 }}
|
31
|
+
|
32
|
+
it 'only keys' do
|
33
|
+
subject.insert keys
|
34
|
+
subject.one(keys).should == keys
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'with data' do
|
38
|
+
payload = keys.merge(field3: field3)
|
39
|
+
subject.insert(payload).should == payload
|
40
|
+
subject.one(keys).should == payload
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'nil data field' do
|
44
|
+
payload = keys.merge(field3: nil, field4: nil)
|
45
|
+
converted_payload = subject.insert(payload)
|
46
|
+
converted_payload.should == keys
|
47
|
+
subject.one(keys).should == keys
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'various commands' do
|
52
|
+
let(:first) {{ field1: 1, field2: 1, data: 'payload1' }}
|
53
|
+
let(:second) {{ field1: 1, field2: 2, data: 'payload2' }}
|
54
|
+
let(:data) {[ first, second ]}
|
55
|
+
|
56
|
+
before { data.each &subject.method(:insert) }
|
57
|
+
|
58
|
+
it '.each' do
|
59
|
+
subject.to_enum.to_a.should == data
|
60
|
+
end
|
61
|
+
|
62
|
+
it '.delete' do
|
63
|
+
subject.get(field1: 1).should have(2).items
|
64
|
+
subject.remove(first)
|
65
|
+
subject.get(field1: 1).should == [second]
|
66
|
+
subject.remove(second)
|
67
|
+
subject.get(field1: 1).should be_empty
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'callbacks' do
|
73
|
+
let :definition do
|
74
|
+
proc do
|
75
|
+
key :field1
|
76
|
+
subkey :field2
|
77
|
+
type :field2, :integer
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it '.before_insert' do
|
82
|
+
subject.config.dsl.before_insert do |data|
|
83
|
+
data[:field1] = data[:field2]
|
84
|
+
end
|
85
|
+
subject.insert field2: 2
|
86
|
+
subject.one(field1: '2').should == { field1: '2', field2: 2 }
|
87
|
+
end
|
88
|
+
|
89
|
+
it '.after_insert' do
|
90
|
+
subject.config.dsl.after_insert do |data|
|
91
|
+
data.should == { field1: '1', field2: 2 }
|
92
|
+
end
|
93
|
+
subject.insert field1: 1, field2: '2'
|
94
|
+
end
|
95
|
+
|
96
|
+
it '.after_remove' do
|
97
|
+
subject.config.dsl.after_remove do |data|
|
98
|
+
data.should == { field1: '1', field2: 2 }
|
99
|
+
end
|
100
|
+
subject.insert field1: 1, field2: '2'
|
101
|
+
subject.remove field1: 1, field2: '2'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'conversions' do
|
106
|
+
let :definition do
|
107
|
+
scope = self
|
108
|
+
proc do
|
109
|
+
key *scope.key
|
110
|
+
subkey *scope.subkey
|
111
|
+
type :field, scope.type
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.converts(type, original, expected=original, &block)
|
116
|
+
context "converts #{type}" do
|
117
|
+
let(:original) { original }
|
118
|
+
let(:expected) { expected }
|
119
|
+
let(:compare) { block or -> it { it }}
|
120
|
+
it_behaves_like :type
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
shared_examples_for :convertable do
|
125
|
+
context 'default text type' do
|
126
|
+
let(:type) { nil }
|
127
|
+
converts 'integer', 2, '2'
|
128
|
+
converts 'string', 'string'
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'integer type' do
|
132
|
+
let(:type) { :integer }
|
133
|
+
converts 'integer', 2
|
134
|
+
converts 'big integer', 1_000_000
|
135
|
+
converts 'string', '32', 32
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'boolean type' do
|
139
|
+
let(:type) { :boolean }
|
140
|
+
converts 'true', true
|
141
|
+
converts 'false', false
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'time' do
|
145
|
+
let(:type) { :time }
|
146
|
+
converts 'time', Time.now.round
|
147
|
+
converts('date', Date.today) {|time| time.to_date }
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'decimal' do
|
151
|
+
let(:type) { :decimal }
|
152
|
+
converts 'integer', 20
|
153
|
+
converts 'float', 30.442
|
154
|
+
converts 'decimal', BigDecimal('42.42')
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'float' do
|
158
|
+
let(:type) { :float }
|
159
|
+
converts 'integer', 20
|
160
|
+
converts 'float', 30.30
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
shared_examples_for :uuid_convertable do
|
165
|
+
context 'uuid' do
|
166
|
+
let(:type) { :uuid }
|
167
|
+
|
168
|
+
uuid = SimpleUUID::UUID.new
|
169
|
+
converts 'uuid', uuid, uuid.to_time
|
170
|
+
converts 'time', Time.now.round
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
shared_examples_for :empty_string_convertable do
|
175
|
+
context 'empty string for text type' do
|
176
|
+
let(:type) { nil }
|
177
|
+
converts '', ''
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
shared_examples_for :type do
|
182
|
+
before { subject.insert data }
|
183
|
+
|
184
|
+
it 'should be correctly restored' do
|
185
|
+
restored = subject.one(query)[:field]
|
186
|
+
compare.(restored).should == expected
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'key' do
|
191
|
+
let(:key) { :field }
|
192
|
+
let(:subkey) {}
|
193
|
+
let(:query) {{ field: original }}
|
194
|
+
let(:data) {{ field: original, data: :dummy }}
|
195
|
+
|
196
|
+
it_behaves_like :convertable
|
197
|
+
end
|
198
|
+
|
199
|
+
context 'subkeys' do
|
200
|
+
let(:key) { :key }
|
201
|
+
let(:subkey) { :field }
|
202
|
+
let(:query) {{ key: 42 }}
|
203
|
+
let(:data) {{ key: 42, field: original }}
|
204
|
+
|
205
|
+
it_behaves_like :convertable
|
206
|
+
it_behaves_like :uuid_convertable
|
207
|
+
it_behaves_like :empty_string_convertable
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'data' do
|
211
|
+
let(:key) { :key }
|
212
|
+
let(:subkey) {}
|
213
|
+
let(:query) {{ key: 42 }}
|
214
|
+
let(:data) {{ key: 42, field: original }}
|
215
|
+
|
216
|
+
it_behaves_like :convertable
|
217
|
+
it_behaves_like :uuid_convertable
|
218
|
+
it_behaves_like :empty_string_convertable
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'complex key' do
|
222
|
+
let(:key) {[ :key, :field ]}
|
223
|
+
let(:subkey) {}
|
224
|
+
let(:query) {{ key: 42, field: original }}
|
225
|
+
let(:data) {{ key: 42, field: original, data: :dummy }}
|
226
|
+
|
227
|
+
it_behaves_like :convertable
|
228
|
+
it_behaves_like :empty_string_convertable
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'cassandra/mapper'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
Cassandra::Mapper.schema = { keyspaces: [ :mapper ]}
|
5
|
+
Cassandra::Mapper.env = :test
|
6
|
+
|
7
|
+
begin
|
8
|
+
Cassandra::Mapper.migrate
|
9
|
+
rescue CassandraThrift::InvalidRequestException
|
10
|
+
puts 'Using existing keyspace'
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cassandra-mapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- brainopia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: cassandra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.18.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.18.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: multi_json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
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
|
+
description: |2
|
56
|
+
Work with cassandra in datamapper style.
|
57
|
+
email: brainopia@evilmartians.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- .travis.yml
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- cassandra-mapper.gemspec
|
69
|
+
- lib/cassandra/mapper.rb
|
70
|
+
- lib/cassandra/mapper/convert.rb
|
71
|
+
- lib/cassandra/mapper/data/insert.rb
|
72
|
+
- lib/cassandra/mapper/data/remove.rb
|
73
|
+
- lib/cassandra/mapper/data/request.rb
|
74
|
+
- lib/cassandra/mapper/data/response.rb
|
75
|
+
- lib/cassandra/mapper/extend/migrate.rb
|
76
|
+
- lib/cassandra/mapper/extend/queries.rb
|
77
|
+
- lib/cassandra/mapper/extend/schema.rb
|
78
|
+
- lib/cassandra/mapper/utility/config.rb
|
79
|
+
- lib/cassandra/mapper/utility/delegate_keys.rb
|
80
|
+
- lib/cassandra/mapper/utility/hash.rb
|
81
|
+
- lib/cassandra/mapper/utility/store_instances.rb
|
82
|
+
- spec/cassandra/mapper/convert_spec.rb
|
83
|
+
- spec/cassandra/mapper/utility/config_spec.rb
|
84
|
+
- spec/cassandra/mapper/utility/store_instances_spec.rb
|
85
|
+
- spec/cassandra/mapper_spec.rb
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
homepage: https://github.com/brainopia/cassandra-mapper
|
88
|
+
licenses: []
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.0.3
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Cassandra mapper
|
110
|
+
test_files:
|
111
|
+
- spec/cassandra/mapper/convert_spec.rb
|
112
|
+
- spec/cassandra/mapper/utility/config_spec.rb
|
113
|
+
- spec/cassandra/mapper/utility/store_instances_spec.rb
|
114
|
+
- spec/cassandra/mapper_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
has_rdoc:
|