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.
- data/lib/cequel.rb +16 -0
- data/lib/cequel/batch.rb +58 -0
- data/lib/cequel/cql_row_specification.rb +22 -0
- data/lib/cequel/data_set.rb +346 -0
- data/lib/cequel/errors.rb +4 -0
- data/lib/cequel/keyspace.rb +106 -0
- data/lib/cequel/model.rb +95 -0
- data/lib/cequel/model/associations.rb +120 -0
- data/lib/cequel/model/callbacks.rb +32 -0
- data/lib/cequel/model/class_internals.rb +48 -0
- data/lib/cequel/model/column.rb +20 -0
- data/lib/cequel/model/dictionary.rb +202 -0
- data/lib/cequel/model/dirty.rb +53 -0
- data/lib/cequel/model/dynamic.rb +31 -0
- data/lib/cequel/model/errors.rb +13 -0
- data/lib/cequel/model/inheritable.rb +48 -0
- data/lib/cequel/model/instance_internals.rb +23 -0
- data/lib/cequel/model/local_association.rb +42 -0
- data/lib/cequel/model/magic.rb +79 -0
- data/lib/cequel/model/mass_assignment_security.rb +21 -0
- data/lib/cequel/model/naming.rb +17 -0
- data/lib/cequel/model/observer.rb +42 -0
- data/lib/cequel/model/persistence.rb +173 -0
- data/lib/cequel/model/properties.rb +143 -0
- data/lib/cequel/model/railtie.rb +33 -0
- data/lib/cequel/model/remote_association.rb +40 -0
- data/lib/cequel/model/scope.rb +362 -0
- data/lib/cequel/model/scoped.rb +50 -0
- data/lib/cequel/model/subclass_internals.rb +45 -0
- data/lib/cequel/model/timestamps.rb +52 -0
- data/lib/cequel/model/translation.rb +17 -0
- data/lib/cequel/model/validations.rb +50 -0
- data/lib/cequel/new_relic_instrumentation.rb +22 -0
- data/lib/cequel/row_specification.rb +63 -0
- data/lib/cequel/statement.rb +23 -0
- data/lib/cequel/version.rb +3 -0
- data/spec/environment.rb +3 -0
- data/spec/examples/data_set_spec.rb +382 -0
- data/spec/examples/keyspace_spec.rb +63 -0
- data/spec/examples/model/associations_spec.rb +109 -0
- data/spec/examples/model/callbacks_spec.rb +79 -0
- data/spec/examples/model/dictionary_spec.rb +413 -0
- data/spec/examples/model/dirty_spec.rb +39 -0
- data/spec/examples/model/dynamic_spec.rb +41 -0
- data/spec/examples/model/inheritable_spec.rb +45 -0
- data/spec/examples/model/magic_spec.rb +199 -0
- data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
- data/spec/examples/model/naming_spec.rb +9 -0
- data/spec/examples/model/observer_spec.rb +86 -0
- data/spec/examples/model/persistence_spec.rb +201 -0
- data/spec/examples/model/properties_spec.rb +81 -0
- data/spec/examples/model/scope_spec.rb +677 -0
- data/spec/examples/model/serialization_spec.rb +20 -0
- data/spec/examples/model/spec_helper.rb +12 -0
- data/spec/examples/model/timestamps_spec.rb +52 -0
- data/spec/examples/model/translation_spec.rb +23 -0
- data/spec/examples/model/validations_spec.rb +86 -0
- data/spec/examples/spec_helper.rb +9 -0
- data/spec/models/asset.rb +21 -0
- data/spec/models/asset_observer.rb +5 -0
- data/spec/models/blog.rb +14 -0
- data/spec/models/blog_posts.rb +6 -0
- data/spec/models/category.rb +9 -0
- data/spec/models/comment.rb +12 -0
- data/spec/models/photo.rb +5 -0
- data/spec/models/post.rb +88 -0
- data/spec/models/post_comments.rb +14 -0
- data/spec/models/post_observer.rb +43 -0
- data/spec/support/helpers.rb +26 -0
- data/spec/support/result_stub.rb +27 -0
- 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,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,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
|