cassandra_object_rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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