cassandra_object_rails 0.0.1

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 (97) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG +5 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE +13 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.rdoc +97 -0
  9. data/Rakefile +12 -0
  10. data/cassandra_object_rails.gemspec +26 -0
  11. data/lib/cassandra_object/attribute_methods.rb +87 -0
  12. data/lib/cassandra_object/attribute_methods/definition.rb +19 -0
  13. data/lib/cassandra_object/attribute_methods/dirty.rb +44 -0
  14. data/lib/cassandra_object/attribute_methods/primary_key.rb +25 -0
  15. data/lib/cassandra_object/attribute_methods/typecasting.rb +59 -0
  16. data/lib/cassandra_object/base.rb +69 -0
  17. data/lib/cassandra_object/belongs_to.rb +63 -0
  18. data/lib/cassandra_object/belongs_to/association.rb +48 -0
  19. data/lib/cassandra_object/belongs_to/builder.rb +40 -0
  20. data/lib/cassandra_object/belongs_to/reflection.rb +30 -0
  21. data/lib/cassandra_object/callbacks.rb +29 -0
  22. data/lib/cassandra_object/config.rb +15 -0
  23. data/lib/cassandra_object/connection.rb +36 -0
  24. data/lib/cassandra_object/consistency.rb +18 -0
  25. data/lib/cassandra_object/core.rb +59 -0
  26. data/lib/cassandra_object/errors.rb +6 -0
  27. data/lib/cassandra_object/identity.rb +24 -0
  28. data/lib/cassandra_object/inspect.rb +25 -0
  29. data/lib/cassandra_object/log_subscriber.rb +29 -0
  30. data/lib/cassandra_object/persistence.rb +169 -0
  31. data/lib/cassandra_object/rails_initializer.rb +19 -0
  32. data/lib/cassandra_object/railtie.rb +11 -0
  33. data/lib/cassandra_object/savepoints.rb +79 -0
  34. data/lib/cassandra_object/schema.rb +78 -0
  35. data/lib/cassandra_object/schema/tasks.rb +48 -0
  36. data/lib/cassandra_object/scope.rb +48 -0
  37. data/lib/cassandra_object/scope/batches.rb +32 -0
  38. data/lib/cassandra_object/scope/finder_methods.rb +47 -0
  39. data/lib/cassandra_object/scope/query_methods.rb +111 -0
  40. data/lib/cassandra_object/scoping.rb +19 -0
  41. data/lib/cassandra_object/serialization.rb +6 -0
  42. data/lib/cassandra_object/tasks/cassandra.rake +53 -0
  43. data/lib/cassandra_object/timestamps.rb +19 -0
  44. data/lib/cassandra_object/type.rb +16 -0
  45. data/lib/cassandra_object/types.rb +8 -0
  46. data/lib/cassandra_object/types/array_type.rb +76 -0
  47. data/lib/cassandra_object/types/base_type.rb +26 -0
  48. data/lib/cassandra_object/types/boolean_type.rb +20 -0
  49. data/lib/cassandra_object/types/date_type.rb +17 -0
  50. data/lib/cassandra_object/types/float_type.rb +16 -0
  51. data/lib/cassandra_object/types/integer_type.rb +16 -0
  52. data/lib/cassandra_object/types/json_type.rb +52 -0
  53. data/lib/cassandra_object/types/string_type.rb +15 -0
  54. data/lib/cassandra_object/types/time_type.rb +16 -0
  55. data/lib/cassandra_object/validations.rb +44 -0
  56. data/lib/cassandra_object_rails.rb +64 -0
  57. data/test/support/connect.rb +17 -0
  58. data/test/support/issue.rb +5 -0
  59. data/test/support/teardown.rb +24 -0
  60. data/test/test_helper.rb +34 -0
  61. data/test/unit/active_model_test.rb +18 -0
  62. data/test/unit/attribute_methods/definition_test.rb +13 -0
  63. data/test/unit/attribute_methods/dirty_test.rb +71 -0
  64. data/test/unit/attribute_methods/primary_key_test.rb +26 -0
  65. data/test/unit/attribute_methods/typecasting_test.rb +112 -0
  66. data/test/unit/attribute_methods_test.rb +39 -0
  67. data/test/unit/base_test.rb +20 -0
  68. data/test/unit/belongs_to/reflection_test.rb +12 -0
  69. data/test/unit/belongs_to_test.rb +62 -0
  70. data/test/unit/callbacks_test.rb +46 -0
  71. data/test/unit/config_test.rb +23 -0
  72. data/test/unit/connection_test.rb +10 -0
  73. data/test/unit/consistency_test.rb +13 -0
  74. data/test/unit/core_test.rb +55 -0
  75. data/test/unit/identity_test.rb +26 -0
  76. data/test/unit/inspect_test.rb +26 -0
  77. data/test/unit/log_subscriber_test.rb +22 -0
  78. data/test/unit/persistence_test.rb +187 -0
  79. data/test/unit/savepoints_test.rb +35 -0
  80. data/test/unit/schema/tasks_test.rb +29 -0
  81. data/test/unit/schema_test.rb +47 -0
  82. data/test/unit/scope/batches_test.rb +30 -0
  83. data/test/unit/scope/finder_methods_test.rb +51 -0
  84. data/test/unit/scope/query_methods_test.rb +26 -0
  85. data/test/unit/scoping_test.rb +7 -0
  86. data/test/unit/timestamps_test.rb +27 -0
  87. data/test/unit/types/array_type_test.rb +71 -0
  88. data/test/unit/types/base_type_test.rb +24 -0
  89. data/test/unit/types/boolean_type_test.rb +24 -0
  90. data/test/unit/types/date_type_test.rb +11 -0
  91. data/test/unit/types/float_type_test.rb +17 -0
  92. data/test/unit/types/integer_type_test.rb +19 -0
  93. data/test/unit/types/json_type_test.rb +77 -0
  94. data/test/unit/types/string_type_test.rb +32 -0
  95. data/test/unit/types/time_type_test.rb +14 -0
  96. data/test/unit/validations_test.rb +27 -0
  97. metadata +208 -0
@@ -0,0 +1,48 @@
1
+ require 'cassandra_object/scope/batches'
2
+ require 'cassandra_object/scope/finder_methods'
3
+ require 'cassandra_object/scope/query_methods'
4
+
5
+ module CassandraObject
6
+ class Scope
7
+ include Batches, FinderMethods, QueryMethods
8
+
9
+ attr_accessor :klass
10
+ attr_accessor :limit_value, :select_values, :where_values
11
+
12
+ def initialize(klass)
13
+ @klass = klass
14
+
15
+ @limit_value = nil
16
+ @select_values = []
17
+ @where_values = []
18
+ end
19
+
20
+ private
21
+ def method_missing(method_name, *args, &block)
22
+ if klass.respond_to?(method_name)
23
+ klass.send(method_name, *args, &block)
24
+ elsif Array.method_defined?(method_name)
25
+ to_a.send(method_name, *args, &block)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def instantiate_from_cql(cql_string, *args)
32
+ results = []
33
+ klass.execute_cql(cql_string, *args).fetch do |cql_row|
34
+ results << instantiate_cql_row(cql_row)
35
+ end
36
+ results.compact!
37
+ results
38
+ end
39
+
40
+ def instantiate_cql_row(cql_row)
41
+ attributes = cql_row.to_hash
42
+ key = attributes.delete('KEY')
43
+ if attributes.any?
44
+ klass.instantiate(key, attributes)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ module CassandraObject
2
+ class Scope
3
+ module Batches
4
+ def find_each(options = {})
5
+ find_in_batches(options) do |records|
6
+ records.each { |record| yield record }
7
+ end
8
+ end
9
+
10
+ def find_in_batches(options = {})
11
+ batch_size = options.delete(:batch_size) || 1000
12
+ start_key = nil
13
+
14
+ scope = limit(batch_size + 1)
15
+ records = scope.to_a
16
+
17
+ while records.any?
18
+ if records.size > batch_size
19
+ next_record = records.pop
20
+ else
21
+ next_record = nil
22
+ end
23
+
24
+ yield records
25
+ break if next_record.nil?
26
+
27
+ records = scope.where("KEY >= '#{next_record.id}'").to_a
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module CassandraObject
2
+ class Scope
3
+ module FinderMethods
4
+ def find(ids)
5
+ if ids.is_a?(Array)
6
+ find_some(ids)
7
+ else
8
+ find_one(ids)
9
+ end
10
+ end
11
+
12
+ def find_by_id(ids)
13
+ find(ids)
14
+ rescue CassandraObject::RecordNotFound
15
+ nil
16
+ end
17
+
18
+ def all
19
+ to_a
20
+ end
21
+
22
+ def first
23
+ limit(1).to_a.first
24
+ end
25
+
26
+ private
27
+ def find_one(id)
28
+ if id.blank?
29
+ raise CassandraObject::RecordNotFound, "Couldn't find #{self.name} with key #{id.inspect}"
30
+ elsif record = where('KEY' => id).first
31
+ record
32
+ else
33
+ raise CassandraObject::RecordNotFound
34
+ end
35
+ end
36
+
37
+ def find_some(ids)
38
+ ids = ids.flatten
39
+ return [] if ids.empty?
40
+
41
+ ids = ids.compact.map(&:to_s).uniq
42
+
43
+ where("KEY" => ids).to_a
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,111 @@
1
+ module CassandraObject
2
+ class Scope
3
+ module QueryMethods
4
+ def select!(*values)
5
+ self.select_values += values.flatten
6
+ self
7
+ end
8
+
9
+ def select(*values, &block)
10
+ if block_given?
11
+ to_a.select(&block)
12
+ else
13
+ clone.select! *values
14
+ end
15
+ end
16
+
17
+ def where!(*values)
18
+ self.where_values += values.flatten
19
+ self
20
+ end
21
+
22
+ def where(*values)
23
+ clone.where! values
24
+ end
25
+
26
+ def limit!(value)
27
+ self.limit_value = value
28
+ self
29
+ end
30
+
31
+ def limit(value)
32
+ clone.limit! value
33
+ end
34
+
35
+ def to_cql
36
+ [
37
+ "SELECT #{select_string} FROM #{klass.column_family}",
38
+ consistency_string,
39
+ where_string,
40
+ limit_string
41
+ ].delete_if(&:blank?) * ' '
42
+ end
43
+
44
+ def to_a
45
+ instantiate_from_cql(to_cql)
46
+ end
47
+
48
+ private
49
+ def select_string
50
+ if select_values.any?
51
+ (['KEY'] | select_values) * ','
52
+ else
53
+ '*'
54
+ end
55
+ end
56
+
57
+ def where_string
58
+ if where_values.any?
59
+ wheres = []
60
+
61
+ where_values.map do |where_value|
62
+ wheres.concat format_where_statement(where_value)
63
+ end
64
+
65
+ "WHERE #{wheres * ' AND '}"
66
+ else
67
+ ''
68
+ end
69
+ end
70
+
71
+ def format_where_statement(where_value)
72
+ if where_value.is_a?(String)
73
+ [where_value]
74
+ elsif where_value.is_a?(Hash)
75
+ where_value.map do |column, value|
76
+ if value.is_a?(Array)
77
+ "#{column} IN (#{escape_where_value(value)})"
78
+ else
79
+ "#{column} = #{escape_where_value(value)}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def escape_where_value(value)
86
+ if value.is_a?(Array)
87
+ value.map { |v| escape_where_value(v) }.join(",")
88
+ elsif value.is_a?(String)
89
+ value = value.gsub("'", "''")
90
+ "'#{value}'"
91
+ else
92
+ value
93
+ end
94
+ end
95
+
96
+ def limit_string
97
+ if limit_value
98
+ "LIMIT #{limit_value}"
99
+ else
100
+ ""
101
+ end
102
+ end
103
+
104
+ def consistency_string
105
+ if klass.default_consistency
106
+ "USING CONSISTENCY #{klass.default_consistency}"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,19 @@
1
+ module CassandraObject
2
+ module Scoping
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ singleton_class.class_eval do
7
+ delegate :find, :find_by_id, :first, :all, to: :scope
8
+ delegate :find_each, :find_in_batches, to: :scope
9
+ delegate :select, :where, to: :scope
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def scope
15
+ Scope.new(self)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module CassandraObject
2
+ module Serialization
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Serializers::JSON
5
+ end
6
+ end
@@ -0,0 +1,53 @@
1
+ namespace :cassandra do
2
+
3
+ desc 'Creates the keyspace in config/cassandra.yml for the current environment'
4
+ task create: :environment do
5
+ begin
6
+ CassandraObject::Schema.create_keyspace cassandra_config.keyspace, cassandra_config.keyspace_options
7
+ rescue Exception => e
8
+ if e.message =~ /conflicts/
9
+ p "Keyspace #{cassandra_config.keyspace} already exists"
10
+ else
11
+ raise e
12
+ end
13
+ end
14
+ end
15
+
16
+ desc 'Drops the keyspace in config/cassandra.yml for the current environment'
17
+ task drop: :environment do
18
+ begin
19
+ CassandraObject::Schema.drop_keyspace cassandra_config.keyspace
20
+ rescue Exception => e
21
+ if e.message =~ /non existing keyspace/
22
+ p "Keyspace #{cassandra_config.keyspace} does not exist"
23
+ else
24
+ raise e
25
+ end
26
+ end
27
+ end
28
+
29
+ task reset: [:drop, :setup]
30
+ task setup: [:create, :load]
31
+
32
+ task dump: :environment do
33
+ filename = ENV['SCHEMA'] || "#{Rails.root}/db/cassandra/structure.cql"
34
+ File.open(filename, "w:utf-8") do |file|
35
+ CassandraObject::Schema.dump(file)
36
+ end
37
+ end
38
+
39
+ task load: :environment do
40
+ filename = ENV['SCHEMA'] || "#{Rails.root}/db/cassandra/structure.cql"
41
+ File.open(filename) do |file|
42
+ CassandraObject::Schema.load(file)
43
+ end
44
+ end
45
+
46
+ private
47
+ def cassandra_config
48
+ @cassandra_config ||= begin
49
+ cassandra_configs = YAML.load_file(Rails.root.join('config', 'cassandra.yml'))
50
+ CassandraObject::Config.new cassandra_configs[Rails.env || 'development']
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ module CassandraObject
2
+ module Timestamps
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attribute :created_at, type: :time
7
+ attribute :updated_at, type: :time
8
+
9
+ before_create do
10
+ self.created_at ||= Time.current
11
+ self.updated_at ||= Time.current
12
+ end
13
+
14
+ before_update if: :changed? do
15
+ self.updated_at = Time.current
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module CassandraObject
2
+ class Type
3
+ cattr_accessor :attribute_types
4
+ self.attribute_types = {}.with_indifferent_access
5
+
6
+ class << self
7
+ def register(name, coder)
8
+ attribute_types[name] = coder
9
+ end
10
+
11
+ def get_coder(name)
12
+ attribute_types[name]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ CassandraObject::Type.register(:array, CassandraObject::Types::ArrayType)
2
+ CassandraObject::Type.register(:boolean, CassandraObject::Types::BooleanType)
3
+ CassandraObject::Type.register(:date, CassandraObject::Types::DateType)
4
+ CassandraObject::Type.register(:float, CassandraObject::Types::FloatType)
5
+ CassandraObject::Type.register(:integer, CassandraObject::Types::IntegerType)
6
+ CassandraObject::Type.register(:json, CassandraObject::Types::JsonType)
7
+ CassandraObject::Type.register(:time, CassandraObject::Types::TimeType)
8
+ CassandraObject::Type.register(:string, CassandraObject::Types::StringType)
@@ -0,0 +1,76 @@
1
+ module CassandraObject
2
+ module Types
3
+ class ArrayType < BaseType
4
+ class DirtyArray < Array
5
+ attr_accessor :record, :name, :options
6
+ def initialize(record, name, array, options)
7
+ @record = record
8
+ @name = name.to_s
9
+ @options = options
10
+
11
+ super(array)
12
+ setify!
13
+ end
14
+
15
+ def <<(obj)
16
+ modifying do
17
+ super
18
+ setify!
19
+ end
20
+ end
21
+
22
+ def delete(obj)
23
+ modifying do
24
+ super
25
+ end
26
+ end
27
+
28
+ private
29
+ def setify!
30
+ if options[:unique]
31
+ reject!(&:blank?)
32
+ uniq!
33
+ begin sort! rescue ArgumentError end
34
+ end
35
+ end
36
+
37
+ def modifying
38
+ unless record.changed_attributes.include?(name)
39
+ original = dup
40
+ end
41
+
42
+ result = yield
43
+
44
+ if !record.changed_attributes.key?(name) && original != self
45
+ record.changed_attributes[name] = original
46
+ end
47
+
48
+ record.send("#{name}=", self)
49
+
50
+ result
51
+ end
52
+ end
53
+
54
+ def default
55
+ []
56
+ end
57
+
58
+ def encode(array)
59
+ raise ArgumentError.new("#{array.inspect} is not an Array") unless array.kind_of?(Array)
60
+ array.to_a.to_json
61
+ end
62
+
63
+ def decode(str)
64
+ return [] if str.blank?
65
+
66
+ ActiveSupport::JSON.decode(str).tap do |array|
67
+ array.uniq! if options[:unique]
68
+ end
69
+ end
70
+
71
+ def wrap(record, name, value)
72
+ DirtyArray.new(record, name, Array(value), options)
73
+ end
74
+ end
75
+ end
76
+ end