virsandra 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/LICENSE.txt +22 -0
  2. data/README.md +93 -0
  3. data/Rakefile +18 -0
  4. data/lib/virsandra.rb +89 -0
  5. data/lib/virsandra/configuration.rb +61 -0
  6. data/lib/virsandra/connection.rb +39 -0
  7. data/lib/virsandra/cql_value.rb +38 -0
  8. data/lib/virsandra/errors.rb +3 -0
  9. data/lib/virsandra/model.rb +86 -0
  10. data/lib/virsandra/model_query.rb +52 -0
  11. data/lib/virsandra/queries/add_query.rb +25 -0
  12. data/lib/virsandra/queries/alter_query.rb +34 -0
  13. data/lib/virsandra/queries/delete_query.rb +25 -0
  14. data/lib/virsandra/queries/insert_query.rb +34 -0
  15. data/lib/virsandra/queries/limit_query.rb +17 -0
  16. data/lib/virsandra/queries/order_query.rb +36 -0
  17. data/lib/virsandra/queries/select_query.rb +72 -0
  18. data/lib/virsandra/queries/table_query.rb +13 -0
  19. data/lib/virsandra/queries/values_query.rb +41 -0
  20. data/lib/virsandra/queries/where_query.rb +69 -0
  21. data/lib/virsandra/query.rb +87 -0
  22. data/lib/virsandra/version.rb +3 -0
  23. data/spec/feature_helper.rb +62 -0
  24. data/spec/integration/virsandra_spec.rb +13 -0
  25. data/spec/lib/virsandra/configuration_spec.rb +66 -0
  26. data/spec/lib/virsandra/connection_spec.rb +47 -0
  27. data/spec/lib/virsandra/cql_value_spec.rb +25 -0
  28. data/spec/lib/virsandra/model_query_spec.rb +58 -0
  29. data/spec/lib/virsandra/model_spec.rb +173 -0
  30. data/spec/lib/virsandra/queries/add_query_spec.rb +26 -0
  31. data/spec/lib/virsandra/queries/alter_query_spec.rb +35 -0
  32. data/spec/lib/virsandra/queries/delete_query_spec.rb +34 -0
  33. data/spec/lib/virsandra/queries/insert_query_spec.rb +36 -0
  34. data/spec/lib/virsandra/queries/limit_query_spec.rb +20 -0
  35. data/spec/lib/virsandra/queries/order_query_spec.rb +33 -0
  36. data/spec/lib/virsandra/queries/select_query_spec.rb +108 -0
  37. data/spec/lib/virsandra/queries/table_query_spec.rb +13 -0
  38. data/spec/lib/virsandra/queries/values_query_spec.rb +41 -0
  39. data/spec/lib/virsandra/queries/where_query_spec.rb +76 -0
  40. data/spec/lib/virsandra/query_spec.rb +117 -0
  41. data/spec/lib/virsandra_spec.rb +108 -0
  42. data/spec/spec_helper.rb +19 -0
  43. 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 [![Build Status](https://travis-ci.org/ottbot/virsandra.png)](https://travis-ci.org/ottbot/virsandra) [![Code Climate](https://codeclimate.com/github/ottbot/virsandra.png)](https://codeclimate.com/github/ottbot/virsandra) [![Dependency Status](https://gemnasium.com/ottbot/virsandra.png)](https://gemnasium.com/ottbot/virsandra) [![Coverage Status](https://coveralls.io/repos/ottbot/virsandra/badge.png?branch=master)](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,3 @@
1
+ module Virsandra
2
+ class ConfigurationError < ArgumentError; ; end
3
+ 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