virsandra 0.5.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.
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +18 -0
- data/lib/virsandra.rb +89 -0
- data/lib/virsandra/configuration.rb +61 -0
- data/lib/virsandra/connection.rb +39 -0
- data/lib/virsandra/cql_value.rb +38 -0
- data/lib/virsandra/errors.rb +3 -0
- data/lib/virsandra/model.rb +86 -0
- data/lib/virsandra/model_query.rb +52 -0
- data/lib/virsandra/queries/add_query.rb +25 -0
- data/lib/virsandra/queries/alter_query.rb +34 -0
- data/lib/virsandra/queries/delete_query.rb +25 -0
- data/lib/virsandra/queries/insert_query.rb +34 -0
- data/lib/virsandra/queries/limit_query.rb +17 -0
- data/lib/virsandra/queries/order_query.rb +36 -0
- data/lib/virsandra/queries/select_query.rb +72 -0
- data/lib/virsandra/queries/table_query.rb +13 -0
- data/lib/virsandra/queries/values_query.rb +41 -0
- data/lib/virsandra/queries/where_query.rb +69 -0
- data/lib/virsandra/query.rb +87 -0
- data/lib/virsandra/version.rb +3 -0
- data/spec/feature_helper.rb +62 -0
- data/spec/integration/virsandra_spec.rb +13 -0
- data/spec/lib/virsandra/configuration_spec.rb +66 -0
- data/spec/lib/virsandra/connection_spec.rb +47 -0
- data/spec/lib/virsandra/cql_value_spec.rb +25 -0
- data/spec/lib/virsandra/model_query_spec.rb +58 -0
- data/spec/lib/virsandra/model_spec.rb +173 -0
- data/spec/lib/virsandra/queries/add_query_spec.rb +26 -0
- data/spec/lib/virsandra/queries/alter_query_spec.rb +35 -0
- data/spec/lib/virsandra/queries/delete_query_spec.rb +34 -0
- data/spec/lib/virsandra/queries/insert_query_spec.rb +36 -0
- data/spec/lib/virsandra/queries/limit_query_spec.rb +20 -0
- data/spec/lib/virsandra/queries/order_query_spec.rb +33 -0
- data/spec/lib/virsandra/queries/select_query_spec.rb +108 -0
- data/spec/lib/virsandra/queries/table_query_spec.rb +13 -0
- data/spec/lib/virsandra/queries/values_query_spec.rb +41 -0
- data/spec/lib/virsandra/queries/where_query_spec.rb +76 -0
- data/spec/lib/virsandra/query_spec.rb +117 -0
- data/spec/lib/virsandra_spec.rb +108 -0
- data/spec/spec_helper.rb +19 -0
- metadata +207 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Robert Crim
|
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,93 @@
|
|
1
|
+
# Virsandra [](https://travis-ci.org/ottbot/virsandra) [](https://codeclimate.com/github/ottbot/virsandra) [](https://gemnasium.com/ottbot/virsandra) [](https://coveralls.io/r/ottbot/virsandra)
|
2
|
+
|
3
|
+
The Cassandra backed models with Virtus gem with a stupid name.
|
4
|
+
|
5
|
+
## Moving target
|
6
|
+
|
7
|
+
Virsandra is meant to make it easy to use cassandra for persistence
|
8
|
+
for models build with virtus.
|
9
|
+
|
10
|
+
The feature set will likely remain simple, the idea is to not block
|
11
|
+
development of other projects while the implementation of CQL changes
|
12
|
+
quickly.
|
13
|
+
|
14
|
+
## Schema yourself
|
15
|
+
|
16
|
+
At this stage, you're on your own in terms of schema management. The
|
17
|
+
gem expects you to maintain table <=> model attribute mappings
|
18
|
+
yourself.
|
19
|
+
|
20
|
+
## Example usage
|
21
|
+
|
22
|
+
````ruby
|
23
|
+
require 'virsandra'
|
24
|
+
|
25
|
+
Virsandra.configure |c|
|
26
|
+
c.servers = "127.0.0.1:9160"
|
27
|
+
c.keyspace = "example_keyspace"
|
28
|
+
end
|
29
|
+
````
|
30
|
+
|
31
|
+
To define a `Company` model backed by a table `companies` using a composite primary key of `name text, founder text`:
|
32
|
+
````ruby
|
33
|
+
class Company
|
34
|
+
include Virsandra::Model
|
35
|
+
|
36
|
+
attribute :name, String
|
37
|
+
attribute :founder, String
|
38
|
+
attribute :turnover, Fixnum
|
39
|
+
attribute :founded, Date
|
40
|
+
|
41
|
+
table :companies
|
42
|
+
key :name, :founder
|
43
|
+
end
|
44
|
+
````
|
45
|
+
|
46
|
+
Create a company:
|
47
|
+
````ruby
|
48
|
+
company = Company.new(name: "Gooble",
|
49
|
+
founder: "Larry Brin",
|
50
|
+
turnover: 2000000,
|
51
|
+
founded: 1884)
|
52
|
+
company.save
|
53
|
+
````
|
54
|
+
|
55
|
+
Find the company by key:
|
56
|
+
````ruby
|
57
|
+
company = Company.find(name: "Gooble", founder: "Larry Brin")
|
58
|
+
````
|
59
|
+
|
60
|
+
Find or initialize a company. If there is a row with the same primary
|
61
|
+
key, this will load missing attributes from cassandra and merge new
|
62
|
+
ones.
|
63
|
+
|
64
|
+
````ruby
|
65
|
+
company = Company.load(name: "Gooble", founder: "Larry Brin", foundec: 2012)
|
66
|
+
company.attributes
|
67
|
+
#=> {name: "Gooble", founder: "Larry Brin", turnover: 2000000, founded: 2012}
|
68
|
+
````
|
69
|
+
|
70
|
+
Search for companies:
|
71
|
+
````ruby
|
72
|
+
companies = Companies.all
|
73
|
+
|
74
|
+
googbles = Companies.where(name: 'Gooble')
|
75
|
+
|
76
|
+
company_names = Companies.all.map(&:name)
|
77
|
+
````
|
78
|
+
|
79
|
+
## TODO / Missing
|
80
|
+
1. Create / filter with index
|
81
|
+
2. Model attributes that are not Cassandra columns
|
82
|
+
3. Counters
|
83
|
+
4. Schema creation / migration
|
84
|
+
5. Support forSet, List, and Map column types
|
85
|
+
|
86
|
+
|
87
|
+
## Contributing
|
88
|
+
|
89
|
+
1. Fork it
|
90
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
91
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
92
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
93
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
|
4
|
+
require "rspec/core"
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
9
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
13
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
14
|
+
spec.rcov = true
|
15
|
+
spec.rcov_opts = "--exclude 'spec/*'"
|
16
|
+
end
|
17
|
+
|
18
|
+
task :default => :spec
|
data/lib/virsandra.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'cql'
|
3
|
+
require 'simple_uuid'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
|
7
|
+
require "virsandra/version"
|
8
|
+
require 'virsandra/errors'
|
9
|
+
require "virsandra/configuration"
|
10
|
+
|
11
|
+
module Virsandra
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def configuration
|
15
|
+
Thread.current[:configuration] ||= Virsandra::Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure
|
19
|
+
yield configuration
|
20
|
+
end
|
21
|
+
|
22
|
+
def connection
|
23
|
+
if dirty?
|
24
|
+
disconnect!
|
25
|
+
Thread.current[:connection] = Virsandra::Connection.new(configuration)
|
26
|
+
configuration.accept_changes
|
27
|
+
end
|
28
|
+
Thread.current[:connection]
|
29
|
+
end
|
30
|
+
|
31
|
+
def disconnect!
|
32
|
+
if Thread.current[:connection].respond_to?(:disconnect!)
|
33
|
+
Thread.current[:connection].disconnect!
|
34
|
+
end
|
35
|
+
Thread.current[:connection] = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset!
|
39
|
+
configuration.reset!
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset_configuration!
|
43
|
+
Thread.current[:configuration] = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def consistency
|
47
|
+
configuration.consistency
|
48
|
+
end
|
49
|
+
|
50
|
+
def keyspace
|
51
|
+
configuration.keyspace
|
52
|
+
end
|
53
|
+
|
54
|
+
def servers
|
55
|
+
configuration.servers
|
56
|
+
end
|
57
|
+
|
58
|
+
def consistency=(value)
|
59
|
+
configuration.consistency = value
|
60
|
+
end
|
61
|
+
|
62
|
+
def keyspace=(value)
|
63
|
+
configuration.keyspace = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def servers=(value)
|
67
|
+
configuration.servers = value
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute(query)
|
71
|
+
connection.execute(query)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def dirty?
|
77
|
+
Thread.current[:connection].nil? || configuration.changed?
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
require "virsandra/connection"
|
86
|
+
require "virsandra/cql_value"
|
87
|
+
require "virsandra/query"
|
88
|
+
require "virsandra/model_query"
|
89
|
+
require "virsandra/model"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Virsandra
|
2
|
+
class Configuration
|
3
|
+
OPTIONS = [
|
4
|
+
:consistency,
|
5
|
+
:keyspace,
|
6
|
+
:servers,
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
DEFAULT_OPTION_VALUES = {
|
10
|
+
servers: "127.0.0.1",
|
11
|
+
consistency: :quorum
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
attr_accessor *OPTIONS
|
15
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
reset!
|
18
|
+
use_options(options || {})
|
19
|
+
accept_changes
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset!
|
23
|
+
use_options(DEFAULT_OPTION_VALUES)
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate!
|
27
|
+
unless [servers, keyspace].all?
|
28
|
+
raise ConfigurationError.new("A keyspace and server must be defined")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def accept_changes
|
33
|
+
@old_hash = hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_hash
|
37
|
+
OPTIONS.each_with_object({}) do |attr, settings|
|
38
|
+
settings[attr] = send(attr)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def changed?
|
43
|
+
hash != @old_hash
|
44
|
+
end
|
45
|
+
|
46
|
+
def hash
|
47
|
+
to_hash.hash
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def use_options(options)
|
53
|
+
options.each do |key, value|
|
54
|
+
if OPTIONS.include?(key)
|
55
|
+
send(:"#{key}=", value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Virsandra
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :handle, :config
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
config.validate!
|
11
|
+
connect!
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect!
|
15
|
+
@handle = Cql::Client.connect(hosts: @config.servers)
|
16
|
+
@handle.use(@config.keyspace)
|
17
|
+
@handle
|
18
|
+
end
|
19
|
+
|
20
|
+
def disconnect!
|
21
|
+
@handle.close
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute(query, consistency = nil)
|
25
|
+
@handle.execute(query, consistency || config.consistency)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Delegate to CassandraCQL::Database handle
|
29
|
+
def method_missing(method, *args, &block)
|
30
|
+
return super unless @handle.respond_to?(method)
|
31
|
+
@handle.send(method, *args, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def respond_to?(method, include_private=false)
|
35
|
+
@handle.respond_to?(method, include_private) || super(method, include_private)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Virsandra
|
2
|
+
class CQLValue
|
3
|
+
|
4
|
+
def self.convert(val)
|
5
|
+
new(val).to_cql
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize(value)
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_cql
|
15
|
+
if value.respond_to?(:to_guid)
|
16
|
+
value.to_guid
|
17
|
+
elsif should_escape?(value)
|
18
|
+
"'#{escape(value)}'"
|
19
|
+
else
|
20
|
+
value.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def should_escape?(value)
|
27
|
+
!![String, Symbol, Time, Date].detect do |klass|
|
28
|
+
value.is_a?(klass)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def escape(str)
|
33
|
+
str = str.to_s.gsub(/'/,"''")
|
34
|
+
str.force_encoding('ASCII-8BIT')
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Virsandra
|
2
|
+
module Model
|
3
|
+
|
4
|
+
include Virtus
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
super
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.send :include, InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
def key
|
14
|
+
self.class.key.reduce({}) do |key, col|
|
15
|
+
key[col] = attributes[col]
|
16
|
+
key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def table
|
21
|
+
self.class.table
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
self.class.valid_key?(key)
|
26
|
+
end
|
27
|
+
|
28
|
+
def save
|
29
|
+
ModelQuery.new(self).save if valid?
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete
|
33
|
+
ModelQuery.new(self).delete
|
34
|
+
end
|
35
|
+
|
36
|
+
def ==(other)
|
37
|
+
other.is_a?(self.class) && attributes == other.attributes
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
def table(name = nil)
|
43
|
+
@table = name if name
|
44
|
+
@table
|
45
|
+
end
|
46
|
+
|
47
|
+
def key(*columns)
|
48
|
+
@key = columns unless columns.empty?
|
49
|
+
@key
|
50
|
+
end
|
51
|
+
|
52
|
+
def find(columns)
|
53
|
+
raise ArgumentError.new("Invalid key") unless valid_key?(columns)
|
54
|
+
load(columns)
|
55
|
+
end
|
56
|
+
|
57
|
+
def load(columns)
|
58
|
+
record = new(columns)
|
59
|
+
|
60
|
+
row = ModelQuery.new(record).find_by_key
|
61
|
+
record.attributes = row.merge(columns)
|
62
|
+
record
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid_key?(columns)
|
66
|
+
return false if columns.length != key.length
|
67
|
+
key.all? {|k| !columns[k].nil? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def attribute_names
|
71
|
+
attribute_set.map(&:name)
|
72
|
+
end
|
73
|
+
|
74
|
+
alias_method :column_names, :attribute_names
|
75
|
+
|
76
|
+
def all
|
77
|
+
where({})
|
78
|
+
end
|
79
|
+
|
80
|
+
def where(params)
|
81
|
+
ModelQuery.new(self).where(params)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|