cassandro 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gems +1 -0
- data/.gems-test +1 -0
- data/.gitignore +1 -0
- data/LICENSE +8 -0
- data/README.md +187 -0
- data/cassandro.gemspec +17 -0
- data/lib/cassandro.rb +4 -0
- data/lib/cassandro/core.rb +66 -0
- data/lib/cassandro/ext/soft_delete.rb +24 -0
- data/lib/cassandro/model.rb +318 -0
- data/lib/cassandro/support/hash.rb +29 -0
- data/rakefile +10 -0
- data/test/cassandro_model_test.rb +137 -0
- data/test/cassandro_test.rb +19 -0
- data/test/helper.rb +24 -0
- data/test/support/tables.rb +20 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NmU5NmI3YWNjYjMwYTYyZjQ2YjNlOGM1NTc4NTRkMmZkNDIwNjNlOA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NDZkYjJiMmZlOWU1ZTRjZDk4MmI2N2IzODFjYzJiYzY2OWI3M2Q3Yg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDJiNDg1MTkyYWRmNzVjYzc0N2FkZjEzOGZmYThjMjRhZDViMmMwNjU4Yjgy
|
10
|
+
ZDJjYzM4NTliYTdhNmNlZjI3MDRmMDgwZjc1ZTQ3ODQ1ZjQ4ZTNhYjEwOTcz
|
11
|
+
ZmMxNjM4NjRjYjlhNjJiMWZiNTY4MjljZDNlODJjYjZkM2NmY2I=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MGU1MjY0ZTE5NzlkNDIyMWE0N2QzODllZmRhZDZiMzBlNWI3NmI5NmZjY2E4
|
14
|
+
ZjQ5YTI1OWQwYTZhMjM0OTEyY2VmMDYxYzdjY2RmNjViMDVmZDJlYTVkMTgx
|
15
|
+
YmRjYjAyYzM2OWRiMjI0YmIxZGZkM2UxYmRkYTc0MDE5MDQ5ZDI=
|
data/.gems
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
cassandra-driver -v 1.0.0.beta.3
|
data/.gems-test
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
protest -v 0.5.2
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/LICENSE
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Copyright (C) 2014 Lautaro Orazi and Leonardo Mateo
|
2
|
+
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
5
|
+
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
7
|
+
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# Cassandro
|
2
|
+
|
3
|
+
Cassandro is a small Ruby ORM for Apache Cassandra 2.0 and CQL 3.0. Cassandro uses the new Datastax Ruby Driver (official driver).
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
`gem install cassandro`
|
8
|
+
|
9
|
+
## Basic Cassandro
|
10
|
+
|
11
|
+
Connecting to Cassandra DB: `Cassandro.connect(Hash options)`. For full list of options visit [Ruby Driver Documentation](http://datastax.github.io/ruby-driver/api/#cluster-class_method)
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
Cassandro.connect(
|
15
|
+
hosts: ['127.0.0.1'],
|
16
|
+
keyspace: 'some_keyspace'
|
17
|
+
)
|
18
|
+
```
|
19
|
+
|
20
|
+
Creating a new keyspace. For full details of keyspace creation visit [CLI keyspace](http://www.datastax.com/documentation/cassandra/2.0/cassandra/reference/referenceStorage_r.html)
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
Cassandro.create_keyspace('new_keyspace', 'SimpleStrategy', 1)
|
24
|
+
```
|
25
|
+
|
26
|
+
Select keyspace outside `#connect`
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Cassandro.use('keyspace_name')
|
30
|
+
```
|
31
|
+
|
32
|
+
Create table.
|
33
|
+
```ruby
|
34
|
+
table = <<-TABLEDEF
|
35
|
+
CREATE TABLE IF NOT EXISTS table_name (
|
36
|
+
id UUID,
|
37
|
+
username VARCHAR,
|
38
|
+
crypted_password VARCHAR,
|
39
|
+
created_at TIMESTAMP,
|
40
|
+
updated_at TIMESTAMP,
|
41
|
+
PRIMARY KEY(id,username)
|
42
|
+
)
|
43
|
+
TABLEDEF
|
44
|
+
|
45
|
+
Cassandro.execute(table)
|
46
|
+
```
|
47
|
+
|
48
|
+
Execute queries.
|
49
|
+
```ruby
|
50
|
+
result = Cassandro.execute("SELECT * FROM table_name;")
|
51
|
+
```
|
52
|
+
|
53
|
+
Using Driver directly.
|
54
|
+
```ruby
|
55
|
+
statement = Cassandro.client.prepare("SELECT * FROM table_name WHERE colname = ?;")
|
56
|
+
result = Cassandro.client.execute(statement, id)
|
57
|
+
```
|
58
|
+
|
59
|
+
## Cassandro::Model
|
60
|
+
|
61
|
+
### Creating model
|
62
|
+
Creating new model: make you class inherits form `Cassandro::Model`
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class SomeModel < Cassandro::Model
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
Specifying table name using the method `table(table_name)`:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class SomeModel < Cassandro::Model
|
73
|
+
|
74
|
+
table 'some_models'
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
Adding attributes using the method `attribute(name, type, options)`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class SomeModel < Cassandro::Model
|
82
|
+
|
83
|
+
attribute :id, :uuid
|
84
|
+
attribute :name, :text
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
types: :uuid, :text, :integer, :float, :timestamp, :datetime
|
89
|
+
|
90
|
+
Setting the primary key using the method `primary_key(pk_name | Array)`:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class SomeModel < Cassandro::Model
|
94
|
+
|
95
|
+
attribute :id, :uuid
|
96
|
+
attribute :name, :text
|
97
|
+
|
98
|
+
primary_key :id
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
class SomeModel < Cassandro::Model
|
103
|
+
|
104
|
+
attribute :id, :uuid
|
105
|
+
attribute :name, :text
|
106
|
+
|
107
|
+
primary_key [:id,:name]
|
108
|
+
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
Setting unique field using the method `unique(field | Array)`:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class SomeModel < Cassandro::Model
|
116
|
+
|
117
|
+
unique :name
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
__A complete example__
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class SomeModel < Cassandro::Model
|
125
|
+
|
126
|
+
table 'some_models'
|
127
|
+
|
128
|
+
attribute :id, :uuid
|
129
|
+
attribute :name, :text
|
130
|
+
|
131
|
+
primary_key [:id, :name]
|
132
|
+
|
133
|
+
unique :name
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
### Interacting
|
138
|
+
|
139
|
+
Creating a new row:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
somemodel = SomeModel.create(name: 'DaModel')
|
143
|
+
=> #<SomeModel:0x00000001e7a0a8
|
144
|
+
@attributes={:name=>"DaModel", :id=>"1534214c-0e0b-455c-95e8-13677f56d6e5"},
|
145
|
+
@errors={},
|
146
|
+
@persisted=true>
|
147
|
+
|
148
|
+
```
|
149
|
+
|
150
|
+
Find row:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
SomeModel[name: 'DaModel']
|
154
|
+
=> #<SomeModel:0x00000001d27930
|
155
|
+
@attributes={:id=>1534214c-0e0b-455c-95e8-13677f56d6e5, :name=>"DaModel"},
|
156
|
+
@errors={},
|
157
|
+
@persisted=true>
|
158
|
+
```
|
159
|
+
|
160
|
+
Checking errors:
|
161
|
+
```ruby
|
162
|
+
somemodel = SomeModel.create(name: 'DaModel')
|
163
|
+
=> #<SomeModel:0x00000001d68160
|
164
|
+
@attributes={:name=>"DaModel", :id=>"a723301b-b94b-4a4b-8d36-872055734ab5"},
|
165
|
+
@errors={:unique=>"somemodel_not_unique"},
|
166
|
+
@persisted=false>
|
167
|
+
|
168
|
+
somemodel.persisted?
|
169
|
+
=> false
|
170
|
+
|
171
|
+
somemodel.errors
|
172
|
+
=> {:unique=>"somemodel_not_unique"}
|
173
|
+
```
|
174
|
+
|
175
|
+
## TODO
|
176
|
+
|
177
|
+
* Migrations
|
178
|
+
* Support Index
|
179
|
+
* Better queries
|
180
|
+
* Better documentation
|
181
|
+
|
182
|
+
## How to collaborate
|
183
|
+
|
184
|
+
If you find a bug or want to collaborate with the code, you can:
|
185
|
+
|
186
|
+
* Report issues trhough the issue tracker
|
187
|
+
* Fork the repository into your own account and submit a Pull Request
|
data/cassandro.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "cassandro"
|
5
|
+
s.version = "0.1.0"
|
6
|
+
s.summary = "Ruby ORM for Apache Cassandra"
|
7
|
+
s.license = "MIT"
|
8
|
+
s.description = "Lightweight Apache Cassandra ORM for Ruby"
|
9
|
+
s.authors = ["Lautaro Orazi", "Leonardo Mateo"]
|
10
|
+
s.email = ["orazile@gmail.com", "leonardomateo@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/tarolandia/cassandro"
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.add_dependency "cassandra-driver", '>= 1.0.0.beta.3'
|
14
|
+
s.add_development_dependency "protest", '~> 0.5.3'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
end
|
data/lib/cassandro.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'cassandra'
|
2
|
+
|
3
|
+
module Cassandro
|
4
|
+
@@cluster = nil
|
5
|
+
@@session = nil
|
6
|
+
@@tables = []
|
7
|
+
|
8
|
+
def self.cluster
|
9
|
+
@@cluster
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.client
|
13
|
+
@@session
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.tables
|
17
|
+
@@tables
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.connect(options = {})
|
21
|
+
keyspace = options.delete(:keyspace)
|
22
|
+
@@cluster = Cassandra.connect(options)
|
23
|
+
@@session = @@cluster.connect(keyspace || nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.use(keyspace)
|
27
|
+
@@session.execute("USE #{keyspace}") if @session
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.disconnect
|
31
|
+
@@cluster.close if @cluster
|
32
|
+
@@session = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.execute(cql_command)
|
36
|
+
@@session.execute(cql_command)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.create_keyspace(name, strategy = 'SimpleStrategy', replication_factor = 1)
|
40
|
+
keyspace_definition = <<-KSDEF
|
41
|
+
CREATE KEYSPACE IF NOT EXISTS #{name}
|
42
|
+
WITH replication = {
|
43
|
+
'class': '#{strategy}',
|
44
|
+
'replication_factor': #{replication_factor}
|
45
|
+
}
|
46
|
+
KSDEF
|
47
|
+
@@session.execute(keyspace_definition)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.truncate_table(table_name)
|
51
|
+
@@session.execute("TRUNCATE #{table_name}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.register_table(table_def)
|
55
|
+
@@tables << table_def
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.load_tables
|
59
|
+
@@tables.each do |table_definition|
|
60
|
+
queries = table_definition.split(";").map(&:strip)
|
61
|
+
queries.each do |query|
|
62
|
+
@@session.execute(query) unless query.empty?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Cassandro
|
2
|
+
module SoftDelete
|
3
|
+
def self.included(model)
|
4
|
+
model.attribute :deleted, :boolean
|
5
|
+
end
|
6
|
+
|
7
|
+
def destroy
|
8
|
+
update_attributes(deleted: true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def restore
|
12
|
+
update_attributes(deleted: false)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deleted?
|
16
|
+
!!deleted
|
17
|
+
end
|
18
|
+
|
19
|
+
def exists?
|
20
|
+
!deleted
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,318 @@
|
|
1
|
+
module Cassandro
|
2
|
+
class Model
|
3
|
+
class ModelException < StandardError; end
|
4
|
+
|
5
|
+
attr_reader :attributes
|
6
|
+
|
7
|
+
def initialize(attrs = {}, persisted = false)
|
8
|
+
@attributes = {}
|
9
|
+
@errors = {}
|
10
|
+
@persisted = persisted
|
11
|
+
|
12
|
+
attrs.each do |att, val|
|
13
|
+
case val.class.name
|
14
|
+
when "Set"
|
15
|
+
send(:"#{att}=", val.to_a)
|
16
|
+
when "Cassandra::Uuid"
|
17
|
+
send(:"#{att}=", val.to_s)
|
18
|
+
else
|
19
|
+
send(:"#{att}=", val)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def persisted?
|
25
|
+
@persisted
|
26
|
+
end
|
27
|
+
|
28
|
+
def errors
|
29
|
+
@errors
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear_errors
|
33
|
+
@errors = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid?
|
37
|
+
self.class.pk.any? && @errors.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
def cast(attribute)
|
41
|
+
self.class.cast_as(attribute, @attributes[attribute])
|
42
|
+
end
|
43
|
+
|
44
|
+
def unique?
|
45
|
+
self.class.uniqueness_defined? ? self.class[@attributes.slice(*self.class.uniques)].nil? : true
|
46
|
+
end
|
47
|
+
|
48
|
+
def update_attributes(attrs = {})
|
49
|
+
attrs = attrs.inject({}) do |memo, (k,v)|
|
50
|
+
memo[k.to_sym] = (v.nil? || v.to_s.empty?) ? nil : v #TODO: fix for Set, Map
|
51
|
+
memo
|
52
|
+
end#symbolize keys for later merge
|
53
|
+
|
54
|
+
p_keys = []
|
55
|
+
fields = []
|
56
|
+
|
57
|
+
self.class.pk.flatten.each do |k|
|
58
|
+
p_keys << "#{k} = #{cast(k)}"
|
59
|
+
end
|
60
|
+
|
61
|
+
attrs.keys.each do |field|
|
62
|
+
fields << "#{field} = ?"
|
63
|
+
end
|
64
|
+
|
65
|
+
query = "UPDATE #{self.class.table_name} SET #{fields.join(", ")} "
|
66
|
+
query += "WHERE #{p_keys.join(" AND ")}"
|
67
|
+
|
68
|
+
begin
|
69
|
+
st = Cassandro.client.prepare(query)
|
70
|
+
Cassandro.client.execute(st, *native_attributes(attrs))
|
71
|
+
@attributes.merge!(attrs)
|
72
|
+
true
|
73
|
+
rescue Exception => e
|
74
|
+
@errors[:update_error] = e.message
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def save(insert_check = false)
|
80
|
+
clear_errors
|
81
|
+
|
82
|
+
if self.class.casts[:id] == :uuid
|
83
|
+
@attributes[:id] ||= SecureRandom.uuid
|
84
|
+
end
|
85
|
+
|
86
|
+
self.class.pk.flatten.each do |key|
|
87
|
+
if @attributes[key].nil?
|
88
|
+
@errors[:primary_key] = "#{key.to_s}_cant_be_nil"
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if !persisted? && self.class.uniqueness_defined? && !unique?
|
94
|
+
@errors[:unique] = "#{self.class.to_s.downcase}_not_unique"
|
95
|
+
return false
|
96
|
+
end
|
97
|
+
|
98
|
+
st = self.statement_for(:insert, :insert_check => insert_check)
|
99
|
+
|
100
|
+
begin
|
101
|
+
r = Cassandro.client.execute(st, *self.native_attributes)
|
102
|
+
raise ModelException.new('not_applied') unless !insert_check || (insert_check && r.first["[applied]"])
|
103
|
+
@persisted = true
|
104
|
+
rescue => e
|
105
|
+
@attributes[:id] = nil if !persisted? && @attributes.has_key?(:id)
|
106
|
+
@errors[:save] = e.message
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def destroy
|
112
|
+
query = <<-QUERY
|
113
|
+
DELETE FROM #{self.class.table_name}
|
114
|
+
WHERE #{self.class.pk.flatten.map { |k| "#{k.to_s} = #{self.class.cast_as(k, @attributes[k])}" }.join(' AND ')}
|
115
|
+
QUERY
|
116
|
+
Cassandro.execute(query)
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.table(name)
|
120
|
+
self.table_name = name.to_s
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.attribute(name, type = String, options = {})
|
124
|
+
attributes << name
|
125
|
+
casts[name] = type
|
126
|
+
|
127
|
+
define_method(name) do
|
128
|
+
@attributes[name]
|
129
|
+
end
|
130
|
+
|
131
|
+
define_method(:"#{name}=") do |value|
|
132
|
+
@attributes[name] = value
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.primary_key(keys)
|
137
|
+
if keys.is_a?(Array)
|
138
|
+
pk.push(*keys)
|
139
|
+
else
|
140
|
+
pk << keys
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.unique(keys)
|
145
|
+
if keys.is_a?(Array)
|
146
|
+
uniques.push(*keys)
|
147
|
+
else
|
148
|
+
uniques << keys
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.[](value)
|
153
|
+
if value.is_a?(Hash)
|
154
|
+
where = "#{value.map { |k,v| "#{k.to_s} = #{cast_as(k, v)}" }.join(' AND ')} ALLOW FILTERING"
|
155
|
+
else
|
156
|
+
where = "#{partition_key} = #{cast_as(partition_key, value)}"
|
157
|
+
end
|
158
|
+
|
159
|
+
query = <<-QUERY
|
160
|
+
SELECT *
|
161
|
+
FROM #{table_name}
|
162
|
+
WHERE #{where}
|
163
|
+
QUERY
|
164
|
+
|
165
|
+
result = Cassandro.execute(query)
|
166
|
+
return nil unless result.any?
|
167
|
+
|
168
|
+
self.new(result.first, true)
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.create(attrs = {})
|
172
|
+
model = new(attrs)
|
173
|
+
model.save(true)
|
174
|
+
|
175
|
+
model
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.all
|
179
|
+
query = "SELECT * FROM #{self.table_name}"
|
180
|
+
|
181
|
+
rows = Cassandro.execute(query)
|
182
|
+
all = []
|
183
|
+
rows.each do |row|
|
184
|
+
all << new(row)
|
185
|
+
end
|
186
|
+
all
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.where(key, value)
|
190
|
+
key = key.to_sym
|
191
|
+
results = []
|
192
|
+
|
193
|
+
query = "SELECT * FROM #{table_name} WHERE #{key} = ? ALLOW FILTERING"
|
194
|
+
|
195
|
+
st = Cassandro.client.prepare(query)
|
196
|
+
rows = Cassandro.client.execute(st, value)
|
197
|
+
|
198
|
+
rows.each do |result|
|
199
|
+
results << new(result)
|
200
|
+
end
|
201
|
+
|
202
|
+
results
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.count(key, value)
|
206
|
+
key = key.to_sym
|
207
|
+
query = "SELECT count(*) FROM #{table_name} WHERE #{key} = ? ALLOW FILTERING"
|
208
|
+
|
209
|
+
st = Cassandro.client.prepare(query)
|
210
|
+
results = Cassandro.client.execute(st, value)
|
211
|
+
|
212
|
+
results.first["count"]
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.destroy_all
|
216
|
+
begin
|
217
|
+
query = "TRUNCATE #{table_name}"
|
218
|
+
st = Cassandro.execute(query)
|
219
|
+
st.is_a? Cassandra::Client::VoidResult
|
220
|
+
rescue e
|
221
|
+
false
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.query(where, *values)
|
226
|
+
query = "SELECT * FROM #{table_name} WHERE #{where} ALLOW FILTERING"
|
227
|
+
st = Cassandro.client.prepare(query)
|
228
|
+
Cassandro.client.execute(st, *values)
|
229
|
+
end
|
230
|
+
|
231
|
+
protected
|
232
|
+
def self.attributes
|
233
|
+
@attributes ||= []
|
234
|
+
end
|
235
|
+
|
236
|
+
def self.pk
|
237
|
+
@pk ||= []
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.table_name
|
241
|
+
@table_name ||= name.downcase
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.table_name=(name)
|
245
|
+
@table_name = name
|
246
|
+
end
|
247
|
+
|
248
|
+
def self.casts
|
249
|
+
@cast ||= {}
|
250
|
+
end
|
251
|
+
|
252
|
+
def self.uniques
|
253
|
+
@unique ||= []
|
254
|
+
end
|
255
|
+
|
256
|
+
def self.uniqueness_defined?
|
257
|
+
uniques.any?
|
258
|
+
end
|
259
|
+
|
260
|
+
def self.cast_as(key, value)
|
261
|
+
return "NULL" if value.nil?
|
262
|
+
|
263
|
+
case casts[key]
|
264
|
+
when :text
|
265
|
+
"'#{value}'"
|
266
|
+
when :int, :integer
|
267
|
+
value.to_i
|
268
|
+
when :datetime
|
269
|
+
value.to_time.to_i * 1000
|
270
|
+
else
|
271
|
+
"#{value}"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def self.partition_key
|
276
|
+
pk.first
|
277
|
+
end
|
278
|
+
|
279
|
+
def statement_for(operation, options = {})
|
280
|
+
case operation
|
281
|
+
when :insert
|
282
|
+
#INSERT will update if the record already exists http://www.datastax.com/documentation/cql/3.0/cql/cql_reference/insert_r.html
|
283
|
+
query = <<-QUERY
|
284
|
+
INSERT INTO #{self.class.table_name}(#{@attributes.keys.map { |x| x.to_s}.join(',')})
|
285
|
+
VALUES(#{@attributes.keys.map { |x| '?' }.join(",")})
|
286
|
+
#{options[:insert_check] ? 'IF NOT EXISTS' : ''}
|
287
|
+
QUERY
|
288
|
+
if @insert_statement.nil? ||
|
289
|
+
@insert_statement.metadata.count != @attributes.count
|
290
|
+
@insert_statement = Cassandro.client.prepare(query)
|
291
|
+
else
|
292
|
+
@insert_statement
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def native_attributes(attrs = nil)
|
298
|
+
n_attrs = []
|
299
|
+
attrs ||= @attributes
|
300
|
+
|
301
|
+
attrs.each do |k, v|
|
302
|
+
case self.class.casts[k]
|
303
|
+
when :uuid
|
304
|
+
n_attrs << Cassandra::Uuid.new(attrs[k])
|
305
|
+
when :integer
|
306
|
+
n_attrs << attrs[k].to_i
|
307
|
+
when :float
|
308
|
+
n_attrs << attrs[k].to_f
|
309
|
+
when :datetime
|
310
|
+
n_attrs << attrs[k].to_time.to_i
|
311
|
+
else
|
312
|
+
n_attrs << attrs[k]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
n_attrs
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Taken from i18n/core_ext/hash.rb ; https://github.com/svenfuchs/i18n/blob/master/lib/i18n/core_ext/hash.rb
|
2
|
+
class Hash
|
3
|
+
def slice(*keep_keys)
|
4
|
+
h = {}
|
5
|
+
keep_keys.each { |key| h[key] = fetch(key) }
|
6
|
+
h
|
7
|
+
end unless Hash.method_defined?(:slice)
|
8
|
+
|
9
|
+
def except(*less_keys)
|
10
|
+
slice(*keys - less_keys)
|
11
|
+
end unless Hash.method_defined?(:except)
|
12
|
+
|
13
|
+
def deep_symbolize_keys
|
14
|
+
inject({}) { |result, (key, value)|
|
15
|
+
value = value.deep_symbolize_keys if value.is_a?(Hash)
|
16
|
+
result[(key.to_sym rescue key) || key] = value
|
17
|
+
result
|
18
|
+
}
|
19
|
+
end unless Hash.method_defined?(:deep_symbolize_keys)
|
20
|
+
|
21
|
+
# deep_merge_hash! by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
|
22
|
+
HASH_MERGER = proc do |key, v1, v2|
|
23
|
+
Hash === v1 && Hash === v2 ? v1.merge(v2, &HASH_MERGER) : v2
|
24
|
+
end
|
25
|
+
|
26
|
+
def deep_merge!(data)
|
27
|
+
merge!(data, &HASH_MERGER)
|
28
|
+
end unless Hash.method_defined?(:deep_merge!)
|
29
|
+
end
|
data/rakefile
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
require_relative 'support/tables'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
Protest.describe "Cassandro Model" do
|
6
|
+
setup do
|
7
|
+
Cassandro.connect(hosts: ['127.0.0.1'], keyspace: 'cassandro_test')
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'Modeling' do
|
11
|
+
setup do
|
12
|
+
class Test < Cassandro::Model
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
test "adds table name" do
|
17
|
+
class Test < Cassandro::Model
|
18
|
+
table 'tests'
|
19
|
+
end
|
20
|
+
assert_equal 'tests', Test.table_name
|
21
|
+
end
|
22
|
+
|
23
|
+
test "adds attribute" do
|
24
|
+
class Test < Cassandro::Model
|
25
|
+
attribute :test_col_1, :uuid
|
26
|
+
attribute :test_col_2, :text
|
27
|
+
end
|
28
|
+
assert Test.attributes.include?(:test_col_1)
|
29
|
+
assert Test.attributes.include?(:test_col_2)
|
30
|
+
end
|
31
|
+
|
32
|
+
test "adds primary key" do
|
33
|
+
class Test < Cassandro::Model
|
34
|
+
primary_key :test_col_1
|
35
|
+
end
|
36
|
+
assert Test.pk.include?(:test_col_1)
|
37
|
+
end
|
38
|
+
|
39
|
+
test "adds unique field" do
|
40
|
+
class Test < Cassandro::Model
|
41
|
+
unique :test_col_1
|
42
|
+
end
|
43
|
+
assert Test.uniques.include?(:test_col_1)
|
44
|
+
assert Test.uniqueness_defined?
|
45
|
+
end
|
46
|
+
|
47
|
+
test "allows setting and getting attributes" do
|
48
|
+
uuid = SecureRandom.uuid
|
49
|
+
test = Test.new(test_col_1: uuid, test_col_2: 'test_value_2')
|
50
|
+
assert_equal uuid, test.test_col_1
|
51
|
+
assert_equal 'test_value_2', test.test_col_2
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'Creating' do
|
56
|
+
setup do
|
57
|
+
class Test < Cassandro::Model
|
58
|
+
table 'tests'
|
59
|
+
|
60
|
+
attribute :test_col_1, :uuid
|
61
|
+
attribute :test_col_2, :text
|
62
|
+
|
63
|
+
primary_key :test_col_1
|
64
|
+
unique :test_col_1
|
65
|
+
end
|
66
|
+
|
67
|
+
Cassandro.truncate_table('tests')
|
68
|
+
end
|
69
|
+
|
70
|
+
test "creates a row" do
|
71
|
+
test = Test.create(test_col_1: SecureRandom.uuid, test_col_2: 'test_value_2')
|
72
|
+
assert test.persisted?
|
73
|
+
end
|
74
|
+
|
75
|
+
test "fails creating dup row" do
|
76
|
+
uuid = SecureRandom.uuid
|
77
|
+
test_1 = Test.create(test_col_1: uuid, test_col_2: 'test_value_2')
|
78
|
+
test_2 = Test.create(test_col_1: uuid, test_col_2: 'test_value_2')
|
79
|
+
assert !test_2.persisted?
|
80
|
+
assert_equal "test_not_unique", test_2.errors[:unique]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'Querying' do
|
85
|
+
setup do
|
86
|
+
class Test < Cassandro::Model
|
87
|
+
table 'tests'
|
88
|
+
|
89
|
+
attribute :test_col_1, :uuid
|
90
|
+
attribute :test_col_2, :text
|
91
|
+
|
92
|
+
primary_key :test_col_1
|
93
|
+
unique :test_col_1
|
94
|
+
end
|
95
|
+
|
96
|
+
Cassandro.truncate_table('tests')
|
97
|
+
end
|
98
|
+
|
99
|
+
test "gets row" do
|
100
|
+
uuid = SecureRandom.uuid
|
101
|
+
Test.create(test_col_1: uuid, test_col_2: 'test_value_2')
|
102
|
+
test = Test[uuid]
|
103
|
+
assert_equal uuid, test.test_col_1.to_s
|
104
|
+
assert_equal "test_value_2", test.test_col_2
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'Updating' do
|
109
|
+
setup do
|
110
|
+
class Patient < Cassandro::Model
|
111
|
+
table :patients
|
112
|
+
attribute :name, :text
|
113
|
+
attribute :address, :text
|
114
|
+
attribute :age, :int
|
115
|
+
|
116
|
+
primary_key :name
|
117
|
+
end
|
118
|
+
Cassandro.truncate_table('patients')
|
119
|
+
end
|
120
|
+
|
121
|
+
test "updates attributes" do
|
122
|
+
patient = Patient.create(:name => "John Doe", :address => "Somewhere", :age => 30)
|
123
|
+
assert_equal 1, Patient.all.count
|
124
|
+
|
125
|
+
assert patient.update_attributes(:address => "Somewhere Else")
|
126
|
+
assert_equal 1, Patient.all.count
|
127
|
+
assert_equal "Somewhere Else", patient.address
|
128
|
+
end
|
129
|
+
|
130
|
+
test "won't update primary keys" do
|
131
|
+
patient = Patient.create(:name => "John Doe", :address => "Somewhere", :age => 30)
|
132
|
+
|
133
|
+
assert !patient.update_attributes(:name => "Jane Doe")
|
134
|
+
assert_equal "PRIMARY KEY part name found in SET part", patient.errors[:update_error]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
Protest.describe "Cassandro Module" do
|
4
|
+
setup do
|
5
|
+
SESSION.execute("DROP KEYSPACE IF EXISTS test_keyspace")
|
6
|
+
end
|
7
|
+
|
8
|
+
test "connects to database" do
|
9
|
+
client = Cassandro.connect(hosts: ["127.0.0.1"])
|
10
|
+
assert_equal Cassandra::Session, client.class
|
11
|
+
end
|
12
|
+
|
13
|
+
test "creates new keyspace" do
|
14
|
+
Cassandro.connect(hosts: ["127.0.0.1"])
|
15
|
+
assert_equal Cassandra::Results::Void, Cassandro.create_keyspace("test_keyspace").class
|
16
|
+
assert_equal NilClass, Cassandro.use("test_keyspace").class
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
require 'protest'
|
3
|
+
|
4
|
+
require_relative '../lib/cassandro'
|
5
|
+
|
6
|
+
CASSANDRA = Cassandra.connect(hosts: ['127.0.0.1'])
|
7
|
+
SESSION = CASSANDRA.connect
|
8
|
+
|
9
|
+
keyspace_definition = <<-KSDEF
|
10
|
+
CREATE KEYSPACE IF NOT EXISTS cassandro_test
|
11
|
+
WITH replication = {
|
12
|
+
'class': 'SimpleStrategy',
|
13
|
+
'replication_factor': 1
|
14
|
+
}
|
15
|
+
KSDEF
|
16
|
+
|
17
|
+
SESSION.execute(keyspace_definition)
|
18
|
+
SESSION.execute("USE cassandro_test")
|
19
|
+
|
20
|
+
class Protest::TestCase
|
21
|
+
include Rack::Test::Methods
|
22
|
+
end
|
23
|
+
|
24
|
+
Protest.report_with(:turn)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
SESSION.execute("DROP TABLE IF EXISTS tests")
|
2
|
+
table = <<-TABLEDEF
|
3
|
+
CREATE TABLE IF NOT EXISTS tests (
|
4
|
+
test_col_1 UUID,
|
5
|
+
test_col_2 VARCHAR,
|
6
|
+
PRIMARY KEY(test_col_1)
|
7
|
+
)
|
8
|
+
TABLEDEF
|
9
|
+
SESSION.execute(table)
|
10
|
+
|
11
|
+
SESSION.execute("DROP TABLE IF EXISTS patients")
|
12
|
+
table = <<-TABLEDEF
|
13
|
+
CREATE TABLE IF NOT EXISTS patients (
|
14
|
+
name VARCHAR,
|
15
|
+
address VARCHAR,
|
16
|
+
age INT,
|
17
|
+
PRIMARY KEY(name)
|
18
|
+
)
|
19
|
+
TABLEDEF
|
20
|
+
SESSION.execute(table)
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cassandro
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lautaro Orazi
|
8
|
+
- Leonardo Mateo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-10-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: cassandra-driver
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 1.0.0.beta.3
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 1.0.0.beta.3
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: protest
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.5.3
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.5.3
|
42
|
+
description: Lightweight Apache Cassandra ORM for Ruby
|
43
|
+
email:
|
44
|
+
- orazile@gmail.com
|
45
|
+
- leonardomateo@gmail.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- .gems
|
51
|
+
- .gems-test
|
52
|
+
- .gitignore
|
53
|
+
- LICENSE
|
54
|
+
- README.md
|
55
|
+
- cassandro.gemspec
|
56
|
+
- lib/cassandro.rb
|
57
|
+
- lib/cassandro/core.rb
|
58
|
+
- lib/cassandro/ext/soft_delete.rb
|
59
|
+
- lib/cassandro/model.rb
|
60
|
+
- lib/cassandro/support/hash.rb
|
61
|
+
- rakefile
|
62
|
+
- test/cassandro_model_test.rb
|
63
|
+
- test/cassandro_test.rb
|
64
|
+
- test/helper.rb
|
65
|
+
- test/support/tables.rb
|
66
|
+
homepage: https://github.com/tarolandia/cassandro
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.4.2
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Ruby ORM for Apache Cassandra
|
90
|
+
test_files: []
|
91
|
+
has_rdoc:
|