cassandra-mapper 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build
|
6
|
+
Status](https://secure.travis-ci.org/brainopia/cassandra-mapper.png)](http://travis-ci.org/brainopia/cassandra-mapper)
|
7
|
+
[![Code
|
8
|
+
Climate](https://codeclimate.com/github/brainopia/cassandra-mapper.png)](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:
|