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.
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