cequel 0.0.0 → 0.4.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 (71) hide show
  1. data/lib/cequel.rb +16 -0
  2. data/lib/cequel/batch.rb +58 -0
  3. data/lib/cequel/cql_row_specification.rb +22 -0
  4. data/lib/cequel/data_set.rb +346 -0
  5. data/lib/cequel/errors.rb +4 -0
  6. data/lib/cequel/keyspace.rb +106 -0
  7. data/lib/cequel/model.rb +95 -0
  8. data/lib/cequel/model/associations.rb +120 -0
  9. data/lib/cequel/model/callbacks.rb +32 -0
  10. data/lib/cequel/model/class_internals.rb +48 -0
  11. data/lib/cequel/model/column.rb +20 -0
  12. data/lib/cequel/model/dictionary.rb +202 -0
  13. data/lib/cequel/model/dirty.rb +53 -0
  14. data/lib/cequel/model/dynamic.rb +31 -0
  15. data/lib/cequel/model/errors.rb +13 -0
  16. data/lib/cequel/model/inheritable.rb +48 -0
  17. data/lib/cequel/model/instance_internals.rb +23 -0
  18. data/lib/cequel/model/local_association.rb +42 -0
  19. data/lib/cequel/model/magic.rb +79 -0
  20. data/lib/cequel/model/mass_assignment_security.rb +21 -0
  21. data/lib/cequel/model/naming.rb +17 -0
  22. data/lib/cequel/model/observer.rb +42 -0
  23. data/lib/cequel/model/persistence.rb +173 -0
  24. data/lib/cequel/model/properties.rb +143 -0
  25. data/lib/cequel/model/railtie.rb +33 -0
  26. data/lib/cequel/model/remote_association.rb +40 -0
  27. data/lib/cequel/model/scope.rb +362 -0
  28. data/lib/cequel/model/scoped.rb +50 -0
  29. data/lib/cequel/model/subclass_internals.rb +45 -0
  30. data/lib/cequel/model/timestamps.rb +52 -0
  31. data/lib/cequel/model/translation.rb +17 -0
  32. data/lib/cequel/model/validations.rb +50 -0
  33. data/lib/cequel/new_relic_instrumentation.rb +22 -0
  34. data/lib/cequel/row_specification.rb +63 -0
  35. data/lib/cequel/statement.rb +23 -0
  36. data/lib/cequel/version.rb +3 -0
  37. data/spec/environment.rb +3 -0
  38. data/spec/examples/data_set_spec.rb +382 -0
  39. data/spec/examples/keyspace_spec.rb +63 -0
  40. data/spec/examples/model/associations_spec.rb +109 -0
  41. data/spec/examples/model/callbacks_spec.rb +79 -0
  42. data/spec/examples/model/dictionary_spec.rb +413 -0
  43. data/spec/examples/model/dirty_spec.rb +39 -0
  44. data/spec/examples/model/dynamic_spec.rb +41 -0
  45. data/spec/examples/model/inheritable_spec.rb +45 -0
  46. data/spec/examples/model/magic_spec.rb +199 -0
  47. data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
  48. data/spec/examples/model/naming_spec.rb +9 -0
  49. data/spec/examples/model/observer_spec.rb +86 -0
  50. data/spec/examples/model/persistence_spec.rb +201 -0
  51. data/spec/examples/model/properties_spec.rb +81 -0
  52. data/spec/examples/model/scope_spec.rb +677 -0
  53. data/spec/examples/model/serialization_spec.rb +20 -0
  54. data/spec/examples/model/spec_helper.rb +12 -0
  55. data/spec/examples/model/timestamps_spec.rb +52 -0
  56. data/spec/examples/model/translation_spec.rb +23 -0
  57. data/spec/examples/model/validations_spec.rb +86 -0
  58. data/spec/examples/spec_helper.rb +9 -0
  59. data/spec/models/asset.rb +21 -0
  60. data/spec/models/asset_observer.rb +5 -0
  61. data/spec/models/blog.rb +14 -0
  62. data/spec/models/blog_posts.rb +6 -0
  63. data/spec/models/category.rb +9 -0
  64. data/spec/models/comment.rb +12 -0
  65. data/spec/models/photo.rb +5 -0
  66. data/spec/models/post.rb +88 -0
  67. data/spec/models/post_comments.rb +14 -0
  68. data/spec/models/post_observer.rb +43 -0
  69. data/spec/support/helpers.rb +26 -0
  70. data/spec/support/result_stub.rb +27 -0
  71. metadata +125 -23
@@ -0,0 +1,53 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Dirty
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ActiveModel::Dirty
11
+ include ChangedAttributesWithIndifferentAccess
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def column(name, type, options = {})
17
+ define_attribute_method(name)
18
+ super
19
+ end
20
+
21
+ end
22
+
23
+ def save
24
+ super.tap do
25
+ @previously_changed = changes
26
+ changed_attributes.clear
27
+ end
28
+ end
29
+
30
+ def _hydrate(row)
31
+ super.tap { changed_attributes.clear }
32
+ end
33
+
34
+ private
35
+
36
+ def write_attribute(name, value)
37
+ attribute_will_change!(name) if value != read_attribute(name)
38
+ super
39
+ end
40
+
41
+ end
42
+
43
+ module ChangedAttributesWithIndifferentAccess
44
+
45
+ def changed_attributes
46
+ @changed_attributes ||= HashWithIndifferentAccess.new
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,31 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Dynamic
6
+
7
+ def [](column)
8
+ read_attribute(column)
9
+ end
10
+
11
+ def []=(column, value)
12
+ write_attribute(column, value)
13
+ end
14
+
15
+ private
16
+
17
+ def attribute_change(attr)
18
+ if attribute_changed?(attr)
19
+ if respond_to_without_attributes?(attr)
20
+ super
21
+ else
22
+ [changed_attributes[attr], self[attr]]
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,13 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ Error = Class.new(Cequel::Error)
6
+ RecordNotFound = Class.new(Error)
7
+ RecordInvalid = Class.new(Error)
8
+ MissingKey = Class.new(Error)
9
+ InvalidQuery = Class.new(Error)
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,48 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Inheritable
6
+
7
+ module SubclassMethods
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+
13
+ def all
14
+ super.where(@_cequel.type_column.name => name)
15
+ end
16
+
17
+ end
18
+
19
+ def initialize(*args, &block)
20
+ super
21
+ __send__("#{self.class.type_column.name}=", self.class.name)
22
+ end
23
+
24
+ end
25
+
26
+ def inherited(subclass)
27
+ super
28
+ unless @_cequel.type_column
29
+ raise ArgumentError,
30
+ "Can't subclass model class that does not define a type column"
31
+ end
32
+ subclass._cequel = SubclassInternals.new(subclass, @_cequel)
33
+ subclass.module_eval { include(SubclassMethods) }
34
+ end
35
+
36
+ def base_class
37
+ @_cequel.base_class
38
+ end
39
+
40
+ protected
41
+
42
+ attr_writer :_cequel
43
+
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,23 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ #
6
+ # @private
7
+ #
8
+ class InstanceInternals
9
+
10
+ attr_accessor :key, :attributes, :persisted
11
+ attr_reader :associations
12
+
13
+ def initialize(instance)
14
+ @instance = instance
15
+ @attributes = ActiveSupport::HashWithIndifferentAccess.new
16
+ @associations = {}
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,42 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ class LocalAssociation
6
+
7
+ attr_reader :clazz, :name
8
+
9
+ def initialize(name, owning_class, options)
10
+ @name, @owning_class = name, owning_class
11
+ @class_name = options[:class_name] || name.to_s.classify.to_sym
12
+ @foreign_key_name = options[:foreign_key]
13
+ end
14
+
15
+ def primary_key
16
+ @primary_key ||= clazz.key_column
17
+ end
18
+
19
+ def primary_key_name
20
+ @primary_key_name ||= primary_key.name
21
+ end
22
+
23
+ def foreign_key_name
24
+ @foreign_key_name ||= :"#{name}_id"
25
+ end
26
+
27
+ def scope(instance)
28
+ clazz.where(primary_key_name => instance.__send__(foreign_key_name))
29
+ end
30
+
31
+ def clazz
32
+ @clazz ||= @class_name.to_s.constantize
33
+ end
34
+
35
+ def dependent
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,79 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Magic
6
+
7
+ FIND_BY_PATTERN = /^find_by_(\w+)$/
8
+ FIND_ALL_BY_PATTERN = /^find_all_by_(\w+)$/
9
+ FIND_OR_CREATE_BY_PATTERN = /^find_or_create_by_(\w+)$/
10
+ FIND_OR_INITIALIZE_BY_PATTERN = /^find_or_initialize_by_(\w+)$/
11
+
12
+ def self.scope(scope, columns_string, args)
13
+ scope.where(extract_row_specifications(columns_string, args))
14
+ end
15
+
16
+ def self.find_or_create_by(scope, columns_string, args, &block)
17
+ find_or_initialize_by(scope, columns_string, args, &block).tap do |instance|
18
+ instance.save unless instance.persisted?
19
+ end
20
+ end
21
+
22
+ def self.find_or_initialize_by(scope, columns_string, args, &block)
23
+ row_specifications = extract_row_specifications(columns_string, args)
24
+ instance = scope.where(row_specifications).first
25
+ if instance.nil?
26
+ if args.length == 1 && args.first.is_a?(Hash)
27
+ attributes = args.first
28
+ else
29
+ attributes = row_specifications
30
+ end
31
+ instance = scope.new(attributes, &block)
32
+ end
33
+ instance
34
+ end
35
+
36
+ def self.extract_row_specifications(columns_string, args)
37
+ columns = columns_string.split('_and_').map { |column| column.to_sym }
38
+ if args.length == 1 && args.first.is_a?(Hash)
39
+ args.first.symbolize_keys.slice(*columns)
40
+ else
41
+ if columns.length != args.length
42
+ raise ArgumentError,
43
+ "wrong number of arguments(#{args.length} for #{columns.length})"
44
+ end
45
+ Hash[columns.zip(args)]
46
+ end
47
+ end
48
+
49
+ def respond_to?(method, priv = false)
50
+ case method
51
+ when FIND_BY_PATTERN, FIND_ALL_BY_PATTERN,
52
+ FIND_OR_CREATE_BY_PATTERN, FIND_OR_INITIALIZE_BY_PATTERN
53
+
54
+ true
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def method_missing(method, *args, &block)
61
+ case method.to_s
62
+ when FIND_BY_PATTERN
63
+ Magic.scope(all, $1, args).first
64
+ when FIND_ALL_BY_PATTERN
65
+ Magic.scope(all, $1, args).to_a
66
+ when FIND_OR_CREATE_BY_PATTERN
67
+ Magic.find_or_create_by(all, $1, args, &block)
68
+ when FIND_OR_INITIALIZE_BY_PATTERN
69
+ Magic.find_or_initialize_by(all, $1, args, &block)
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,21 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module MassAssignmentSecurity
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ActiveModel::MassAssignmentSecurity
11
+ end
12
+
13
+ def attributes=(attributes)
14
+ super(sanitize_for_mass_assignment(attributes))
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,17 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Naming
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ActiveModel::Naming
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,42 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ #
6
+ # This is ripped directly off of ActiveRecord::Observer
7
+ #
8
+ class Observer < ActiveModel::Observer
9
+
10
+ protected
11
+
12
+ def observed_classes
13
+ klasses = super
14
+ klasses + klasses.map { |klass| klass.descendants }.flatten
15
+ end
16
+
17
+ def add_observer!(klass)
18
+ super
19
+ define_callbacks klass
20
+ end
21
+
22
+ def define_callbacks(klass)
23
+ observer = self
24
+ observer_name = observer.class.name.underscore.gsub('/', '__')
25
+
26
+ Cequel::Model::Callbacks::CALLBACKS.each do |callback|
27
+ next unless respond_to?(callback)
28
+ callback_meth = :"_notify_#{observer_name}_for_#{callback}"
29
+ unless klass.respond_to?(callback_meth)
30
+ klass.send(:define_method, callback_meth) do |&block|
31
+ observer.update(callback, self, &block)
32
+ end
33
+ klass.send(callback, callback_meth)
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,173 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Persistence
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ delegate :update_all, :destroy_all, :delete_all, :to => :all
12
+
13
+ def index_preference(*columns)
14
+ @_cequel.index_preference.concat(columns.map { |c| c.to_sym })
15
+ end
16
+
17
+ def index_preference_columns
18
+ @_cequel.index_preference
19
+ end
20
+
21
+ def find(*keys)
22
+ coerce_array = keys.first.is_a?(Array)
23
+ keys.flatten!
24
+ if keys.length == 1
25
+ instance = find_one(keys.first)
26
+ coerce_array ? [instance] : instance
27
+ else
28
+ find_many(keys)
29
+ end
30
+ end
31
+
32
+ def create(attributes = {}, &block)
33
+ new(attributes, &block).tap { |instance| instance.save }
34
+ end
35
+
36
+ def column_family_name
37
+ @_cequel.column_family_name
38
+ end
39
+
40
+ def column_family
41
+ keyspace[column_family_name]
42
+ end
43
+
44
+ def keyspace
45
+ Cequel::Model.keyspace
46
+ end
47
+
48
+ def _hydrate(row)
49
+ type_column_name = @_cequel.type_column.try(:name)
50
+ if type_column_name && row[type_column_name]
51
+ clazz = row[type_column_name].constantize
52
+ else
53
+ clazz = self
54
+ end
55
+ clazz.allocate._hydrate(row.except(:type))
56
+ end
57
+
58
+ private
59
+
60
+ def find_one(key)
61
+ all.where!(key_alias => key).first.tap do |result|
62
+ if result.nil?
63
+ raise RecordNotFound,
64
+ "Couldn't find #{name} with #{key_alias}=#{key}"
65
+ end
66
+ end
67
+ end
68
+
69
+ def find_many(keys)
70
+ results = all.where!(key_alias => keys).reject do |result|
71
+ result.attributes.keys == [key_alias.to_s]
72
+ end
73
+
74
+ if results.length < keys.length
75
+ raise RecordNotFound,
76
+ "Couldn't find all #{name.pluralize} with #{key_alias} (#{keys.join(', ')})" <<
77
+ "(found #{results.length} results, but was looking for #{keys.length}"
78
+ end
79
+ results
80
+ end
81
+
82
+ end
83
+
84
+ def save
85
+ persisted? ? update : insert
86
+ end
87
+
88
+ def update_attributes(attributes)
89
+ self.attributes = attributes
90
+ save
91
+ end
92
+
93
+ def update_attribute(column, value)
94
+ update_attributes(column => value)
95
+ end
96
+
97
+ def insert
98
+ raise MissingKey if @_cequel.key.nil?
99
+ return if @_cequel.attributes.empty?
100
+ self.class.column_family.insert(attributes)
101
+ persisted!
102
+ end
103
+
104
+ def update
105
+ update_attributes, delete_attributes = {}, []
106
+ changed.each do |attr|
107
+ new = read_attribute(attr)
108
+ if new.nil?
109
+ delete_attributes << attr
110
+ else
111
+ update_attributes[attr] = new
112
+ end
113
+ end
114
+ data_set.update(update_attributes) if update_attributes.any?
115
+ data_set.delete(*delete_attributes) if delete_attributes.any?
116
+ transient! if @_cequel.attributes.empty?
117
+ end
118
+
119
+ def destroy
120
+ data_set.delete
121
+ end
122
+
123
+ def reload
124
+ result = data_set.first
125
+ key_alias = self.class.key_alias
126
+ if result.keys == [key_alias.to_s]
127
+ raise RecordNotFound,
128
+ "Couldn't find #{self.class.name} with #{key_alias}=#{@_cequel.key}"
129
+ end
130
+ _hydrate(result)
131
+ self
132
+ end
133
+
134
+ def _hydrate(row)
135
+ @_cequel = InstanceInternals.new(self)
136
+ tap do
137
+ key_alias = self.class.key_alias.to_s
138
+ key_alias = 'KEY' if key_alias.upcase == 'KEY'
139
+ @_cequel.key = row[key_alias]
140
+ @_cequel.attributes = row.except(key_alias)
141
+ persisted!
142
+ end
143
+ end
144
+
145
+ def persisted!
146
+ @_cequel.persisted = true
147
+ end
148
+
149
+ def transient!
150
+ @_cequel.persisted = false
151
+ end
152
+
153
+ def persisted?
154
+ !!@_cequel.persisted
155
+ end
156
+
157
+ def transient?
158
+ !persisted?
159
+ end
160
+
161
+ private
162
+
163
+ def data_set
164
+ raise MissingKey if @_cequel.key.nil?
165
+ self.class.column_family.
166
+ where(self.class.key_alias => @_cequel.key)
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+
173
+ end