ricordami 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +13 -0
- data/ISSUES.md +0 -0
- data/MIT-LICENSE +21 -0
- data/README.md +454 -0
- data/TODO.md +21 -0
- data/examples/calls.rb +64 -0
- data/examples/singers.rb +42 -0
- data/lib/ricordami/attribute.rb +52 -0
- data/lib/ricordami/can_be_queried.rb +133 -0
- data/lib/ricordami/can_be_validated.rb +27 -0
- data/lib/ricordami/can_have_relationships.rb +152 -0
- data/lib/ricordami/configuration.rb +39 -0
- data/lib/ricordami/connection.rb +22 -0
- data/lib/ricordami/exceptions.rb +14 -0
- data/lib/ricordami/has_attributes.rb +159 -0
- data/lib/ricordami/has_indices.rb +105 -0
- data/lib/ricordami/is_lockable.rb +52 -0
- data/lib/ricordami/is_persisted.rb +123 -0
- data/lib/ricordami/is_retrievable.rb +35 -0
- data/lib/ricordami/key_namer.rb +63 -0
- data/lib/ricordami/model.rb +29 -0
- data/lib/ricordami/query.rb +68 -0
- data/lib/ricordami/relationship.rb +40 -0
- data/lib/ricordami/unique_index.rb +59 -0
- data/lib/ricordami/unique_validator.rb +21 -0
- data/lib/ricordami/value_index.rb +26 -0
- data/lib/ricordami/version.rb +3 -0
- data/lib/ricordami.rb +26 -0
- data/spec/acceptance/manage_relationships_spec.rb +42 -0
- data/spec/acceptance/model_with_validation_spec.rb +78 -0
- data/spec/acceptance/query_model_spec.rb +93 -0
- data/spec/acceptance_helper.rb +2 -0
- data/spec/ricordami/attribute_spec.rb +113 -0
- data/spec/ricordami/can_be_queried_spec.rb +254 -0
- data/spec/ricordami/can_be_validated_spec.rb +115 -0
- data/spec/ricordami/can_have_relationships_spec.rb +255 -0
- data/spec/ricordami/configuration_spec.rb +45 -0
- data/spec/ricordami/connection_spec.rb +25 -0
- data/spec/ricordami/exceptions_spec.rb +43 -0
- data/spec/ricordami/has_attributes_spec.rb +266 -0
- data/spec/ricordami/has_indices_spec.rb +73 -0
- data/spec/ricordami/is_lockable_spec.rb +45 -0
- data/spec/ricordami/is_persisted_spec.rb +186 -0
- data/spec/ricordami/is_retrievable_spec.rb +55 -0
- data/spec/ricordami/key_namer_spec.rb +56 -0
- data/spec/ricordami/model_spec.rb +65 -0
- data/spec/ricordami/query_spec.rb +156 -0
- data/spec/ricordami/relationship_spec.rb +123 -0
- data/spec/ricordami/unique_index_spec.rb +87 -0
- data/spec/ricordami/unique_validator_spec.rb +41 -0
- data/spec/ricordami/value_index_spec.rb +40 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/constants.rb +43 -0
- data/spec/support/db_manager.rb +18 -0
- data/test/bin/data_loader.rb +107 -0
- data/test/data/domains.txt +462 -0
- data/test/data/first_names.txt +1220 -0
- data/test/data/last_names.txt +1028 -0
- data/test/data/people_100_000.csv.bz2 +0 -0
- data/test/data/people_10_000.csv.bz2 +0 -0
- data/test/data/people_1_000_000.csv.bz2 +0 -0
- metadata +258 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
require "ricordami/query.rb"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module CanBeQueried
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
[:where, :and, :not, :any].each do |op|
|
10
|
+
define_method(op) do |*args|
|
11
|
+
options = args.first || {}
|
12
|
+
Query.new(self).send(op, options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def sort(*args)
|
17
|
+
Query.new(self).send(:sort, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def all(opts = {})
|
21
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
22
|
+
get_result_ids(result_key, opts).map do |id|
|
23
|
+
self[id]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def paginate(opts = {})
|
28
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
29
|
+
page = opts[:page] || 1
|
30
|
+
per_page = opts[:per_page] || 20
|
31
|
+
start = (page - 1) * per_page
|
32
|
+
opts[:limit] = [start, per_page]
|
33
|
+
get_result_ids(result_key, opts).map do |id|
|
34
|
+
self[id]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def first(opts = {})
|
39
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
40
|
+
opts[:limit] = [0, 1]
|
41
|
+
ids = get_result_ids(result_key, opts)
|
42
|
+
self[ids.first]
|
43
|
+
end
|
44
|
+
|
45
|
+
def last(opts = {})
|
46
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
47
|
+
size = redis.scard(result_key)
|
48
|
+
opts[:limit] = [size - 1, 1]
|
49
|
+
ids = get_result_ids(result_key, opts)
|
50
|
+
self[ids.first]
|
51
|
+
end
|
52
|
+
|
53
|
+
def rand(opts = {})
|
54
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
55
|
+
size = redis.scard(result_key)
|
56
|
+
opts[:limit] = [Kernel.rand(size), 1]
|
57
|
+
ids = get_result_ids(result_key, opts)
|
58
|
+
self[ids.first]
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def run_expressions(expressions)
|
64
|
+
key_all_ids = indices[:id].uidx_key_name
|
65
|
+
result_key = expressions.reduce(key_all_ids) do |key, expression|
|
66
|
+
type, conditions = expression
|
67
|
+
condition_keys = get_keys_for_each_condition(conditions)
|
68
|
+
next key if condition_keys.empty?
|
69
|
+
target_key = key_name_for_expression(type, conditions, key)
|
70
|
+
send("run_#{type}", target_key, key, condition_keys)
|
71
|
+
end
|
72
|
+
result_key.empty?? [] : result_key
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_keys_for_each_condition(conditions)
|
76
|
+
conditions.map do |field, value|
|
77
|
+
index = indices[field]
|
78
|
+
raise MissingIndex.new("class: #{self}, attribute: #{field.inspect}") if index.nil?
|
79
|
+
index.key_name_for_value(value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def key_name_for_expression(type, conditions, previous_key)
|
84
|
+
KeyNamer.volatile_set(self, :key => previous_key,
|
85
|
+
:info => [type] + conditions.keys)
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_result_ids(key, opts)
|
89
|
+
return redis.smembers(key) unless opts[:sort_by] || opts[:limit]
|
90
|
+
sort_key = KeyNamer.sort(self, :sort_by => opts[:sort_by])
|
91
|
+
sort_options = opts.slice(:order, :limit)
|
92
|
+
redis.sort(key, sort_options.merge(:by => sort_key))
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_and(key_name, start_key, keys)
|
96
|
+
# we get the intersection of the start key and the condition keys
|
97
|
+
redis.sinterstore(key_name, start_key, *keys)
|
98
|
+
key_name
|
99
|
+
end
|
100
|
+
|
101
|
+
alias :run_where :run_and
|
102
|
+
|
103
|
+
def run_any(key_name, start_key, keys)
|
104
|
+
tmp_key = KeyNamer.temporary(self)
|
105
|
+
keys.each_with_index do |key, i|
|
106
|
+
if i == 0
|
107
|
+
# if only one condition key, :any condition is same as :and condition
|
108
|
+
redis.sinterstore(key_name, start_key, keys.first)
|
109
|
+
else
|
110
|
+
# we get the intersection of the start key with each condition key
|
111
|
+
# and we make a union of all of those
|
112
|
+
end
|
113
|
+
redis.sinterstore(tmp_key, start_key, key)
|
114
|
+
redis.sunionstore(key_name, key_name, tmp_key)
|
115
|
+
end
|
116
|
+
redis.del(tmp_key)
|
117
|
+
key_name
|
118
|
+
end
|
119
|
+
|
120
|
+
def run_not(key_name, start_key, keys)
|
121
|
+
keys.each_with_index do |key, i|
|
122
|
+
redis.sdiffstore(key_name, i == 0 ? start_key : key_name, key)
|
123
|
+
end
|
124
|
+
key_name
|
125
|
+
end
|
126
|
+
|
127
|
+
def reverse_order(order)
|
128
|
+
return order.sub("DESC", "ASC") if order.index("DESC")
|
129
|
+
order.sub("ASC", "DESC")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "active_model/validations"
|
2
|
+
require "ricordami/unique_validator"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module CanBeValidated
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def validates_uniqueness_of(*attr_names)
|
11
|
+
validates_with UniqueValidator, _merge_attributes(attr_names)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
def valid?
|
17
|
+
raise ModelHasBeenDeleted.new("can't validate a deleted model") if deleted?
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def save(opts = {})
|
22
|
+
return false unless opts[:validate] == false || valid?
|
23
|
+
super(opts)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "ricordami/relationship"
|
2
|
+
require "ricordami/can_be_queried"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module CanHaveRelationships
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do |base|
|
9
|
+
base.send(:include, CanBeQueried)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def relationships
|
14
|
+
@relationships ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
Relationship::SUPPORTED_TYPES.each do |type|
|
18
|
+
define_method(type) do |*args|
|
19
|
+
name = args.first
|
20
|
+
options = args[1] || {}
|
21
|
+
options.merge!(:other => name, :self => self.to_s.underscore.to_sym)
|
22
|
+
relationship = Relationship.new(type, options)
|
23
|
+
self.relationships[relationship.name] = relationship
|
24
|
+
setup_method = :"setup_#{type}"
|
25
|
+
send(setup_method, relationship) if respond_to?(setup_method, true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_block_to_delete_dependents(relationship)
|
32
|
+
queue_deleting_operations do |obj, session|
|
33
|
+
ref_objs = obj.send(relationship.name)
|
34
|
+
ref_objs = [ref_objs] if relationship.type == :references_one
|
35
|
+
ref_objs.each { |ref_obj| ref_obj.prepare_delete(session) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_block_to_nullify_dependents(relationship)
|
40
|
+
queue_deleting_operations do |obj, session|
|
41
|
+
ref_objs = obj.send(relationship.name)
|
42
|
+
ref_objs = [ref_objs] if relationship.type == :references_one
|
43
|
+
ref_objs.each do |ref_obj|
|
44
|
+
ref_obj.update_attributes(relationship.referrer_id => nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def lazy_setup_references_many(relationship)
|
50
|
+
klass = relationship.object_class
|
51
|
+
referrer_id_sym = relationship.referrer_id.to_sym
|
52
|
+
define_method(relationship.name) do
|
53
|
+
return Query.new([], klass) unless persisted?
|
54
|
+
klass.where(referrer_id_sym => self.id)
|
55
|
+
end
|
56
|
+
case relationship.dependent
|
57
|
+
when :delete then set_block_to_delete_dependents(relationship)
|
58
|
+
when :nullify then set_block_to_nullify_dependents(relationship)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def define_builders(name, klass, referrer_id_sym)
|
63
|
+
# define reference build method
|
64
|
+
build_method = :"build_#{name}"
|
65
|
+
define_method(build_method) do |*args|
|
66
|
+
options = args.first || {}
|
67
|
+
klass.new(options.merge(referrer_id_sym => self.id))
|
68
|
+
end
|
69
|
+
# define reference create method
|
70
|
+
define_method(:"create_#{name}") do |*args|
|
71
|
+
send(build_method, *args).tap { |obj| obj.save }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def lazy_setup_references_one(relationship)
|
76
|
+
klass = relationship.object_class
|
77
|
+
referrer_id_sym = relationship.referrer_id.to_sym
|
78
|
+
define_builders(relationship.name, klass, referrer_id_sym)
|
79
|
+
# define reference method reader
|
80
|
+
define_method(relationship.name) do
|
81
|
+
return nil unless persisted?
|
82
|
+
klass.where(referrer_id_sym => self.id).first
|
83
|
+
end
|
84
|
+
case relationship.dependent
|
85
|
+
when :delete then set_block_to_delete_dependents(relationship)
|
86
|
+
when :nullify then set_block_to_nullify_dependents(relationship)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def setup_referenced_in(relationship)
|
91
|
+
attribute(relationship.referrer_id, :indexed => :value)
|
92
|
+
overide_referrer_id_reader(relationship)
|
93
|
+
end
|
94
|
+
|
95
|
+
def lazy_setup_referenced_in(relationship)
|
96
|
+
klass = relationship.object_class
|
97
|
+
name = relationship.name
|
98
|
+
define_builders(name, klass, relationship.referrer_id.to_sym)
|
99
|
+
referrer_var = :"@#{name}"
|
100
|
+
define_method(name) do
|
101
|
+
referrer = instance_variable_get(referrer_var)
|
102
|
+
return referrer unless referrer.nil?
|
103
|
+
referrer_id_val = send(relationship.referrer_id)
|
104
|
+
return nil if referrer_id_val.nil?
|
105
|
+
klass.get(referrer_id_val).tap do |referrer|
|
106
|
+
instance_variable_set(referrer_var, referrer)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def overide_referrer_id_reader(relationship)
|
112
|
+
referrer_var = :"@#{relationship.name}"
|
113
|
+
# overide referrer id to sweep cache
|
114
|
+
define_method(:"#{relationship.referrer_id}=") do |value|
|
115
|
+
instance_variable_set(referrer_var, nil)
|
116
|
+
super(value)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module InstanceMethods
|
122
|
+
private
|
123
|
+
|
124
|
+
RE_METHOD = /^(build|create)_(.*)$/
|
125
|
+
|
126
|
+
def method_missing(meth, *args, &blk)
|
127
|
+
match = RE_METHOD.match(meth.to_s)
|
128
|
+
meth_root = match.nil?? meth : match[2].to_sym
|
129
|
+
if relationship = self.class.relationships[meth_root]
|
130
|
+
self.class.send(:"lazy_setup_#{relationship.type}", relationship)
|
131
|
+
send(meth, *args, &blk)
|
132
|
+
else
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if Object.respond_to?(:respond_to_missing?)
|
138
|
+
def respond_to_missing?(meth, include_private)
|
139
|
+
self.class.relationships.has_key?(meth)
|
140
|
+
end
|
141
|
+
else
|
142
|
+
def respond_to?(meth)
|
143
|
+
match = RE_METHOD.match(meth.to_s)
|
144
|
+
meth_root = match.nil?? meth : match[2].to_sym
|
145
|
+
return true if self.class.relationships.has_key?(meth_root)
|
146
|
+
super
|
147
|
+
end
|
148
|
+
public :respond_to?
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Ricordami
|
2
|
+
module Configuration
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def configure(&block)
|
7
|
+
raise ArgumentError.new("block missing") unless block_given?
|
8
|
+
yield configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= Config.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Config
|
18
|
+
ATTRIBUTE_NAMES = [:redis_host, :redis_port, :redis_db, :thread_safe]
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@options = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_hash(options)
|
25
|
+
@options = options.slice(*ATTRIBUTE_NAMES)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def method_missing(meth, *args, &blk)
|
31
|
+
return @options[meth] if ATTRIBUTE_NAMES.include?(meth)
|
32
|
+
if args.length == 1 && match = /^(.*)=$/.match(meth.to_s)
|
33
|
+
name = match[1].to_sym
|
34
|
+
return @options[name] = args.first if ATTRIBUTE_NAMES.include?(name)
|
35
|
+
end
|
36
|
+
raise AttributeNotSupported.new("attribute #{meth.to_s.inspect} is not supported")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Ricordami
|
2
|
+
module Connection
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def driver
|
7
|
+
@driver ||= create_driver
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def create_driver
|
13
|
+
c = self.configuration
|
14
|
+
Redis.new(:host => c.redis_host,
|
15
|
+
:port => c.redis_port,
|
16
|
+
:db => c.redis_db,
|
17
|
+
:thread_safe => c.thread_safe,
|
18
|
+
:timeout => 10)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Ricordami
|
2
|
+
Error = Class.new(StandardError)
|
3
|
+
NotFound = Class.new(Error)
|
4
|
+
AttributeNotSupported = Class.new(Error)
|
5
|
+
ReadOnlyAttribute = Class.new(Error)
|
6
|
+
InvalidIndexDefinition = Class.new(Error)
|
7
|
+
ModelHasBeenDeleted = Class.new(Error)
|
8
|
+
TypeNotSupported = Class.new(Error)
|
9
|
+
MissingIndex = Class.new(Error)
|
10
|
+
LockTimeout = Class.new(Error)
|
11
|
+
EventNotSupported = Class.new(Error)
|
12
|
+
OptionValueInvalid = Class.new(Error)
|
13
|
+
MissingMandatoryArgs = Class.new(Error)
|
14
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
require "ricordami/attribute"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module HasAttributes
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveModel::AttributeMethods
|
8
|
+
include ActiveModel::Dirty
|
9
|
+
|
10
|
+
included do
|
11
|
+
attribute_method_suffix('', '=')
|
12
|
+
attribute :id, :read_only => true,
|
13
|
+
:initial => :sequence,
|
14
|
+
:type => :string
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def attributes
|
19
|
+
@attributes ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def attribute(name, options = {})
|
23
|
+
instance = Attribute.new(name, options)
|
24
|
+
options = OptionsExpander.new(self, options)
|
25
|
+
self.attributes[name.to_sym] = instance
|
26
|
+
index(instance.indexed => name.to_sym) if instance.indexed?
|
27
|
+
instance
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes_key_name_for(id)
|
31
|
+
KeyNamer.attributes(self.to_s, :id => id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InstanceMethods
|
36
|
+
attr_reader :attributes
|
37
|
+
|
38
|
+
def initialize(attrs = {})
|
39
|
+
@attributes = {}.with_indifferent_access
|
40
|
+
@reloading = false
|
41
|
+
update_mem_attributes(attrs) unless attrs.empty?
|
42
|
+
set_default_attribute_values
|
43
|
+
end
|
44
|
+
|
45
|
+
# ActiveModel::AttributeMethods doesn't seem
|
46
|
+
# to generate a reader for id that uses
|
47
|
+
# #attributes["id"] in Ruby 1.8.7, so hard-coding
|
48
|
+
# it now
|
49
|
+
def id
|
50
|
+
@attributes["id"]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Replace attribute values with the hash attrs
|
54
|
+
# Note: attrs keys can be strings or symbols
|
55
|
+
def update_mem_attributes(attrs)
|
56
|
+
@reloading = true
|
57
|
+
update_mem_attributes!(attrs)
|
58
|
+
@reloading = false
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_mem_attributes!(attrs)
|
62
|
+
valid_keys = self.class.attributes.keys
|
63
|
+
attrs.symbolize_keys.slice(*valid_keys).each do |name, value|
|
64
|
+
send(:"#{name}=", value)
|
65
|
+
end
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def update_mem_attributes(attrs)
|
70
|
+
valid_keys = self.class.attributes.keys
|
71
|
+
attrs.symbolize_keys.slice(*valid_keys).each do |name, value|
|
72
|
+
write_attribute(name, value)
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def write_attribute(name, value)
|
80
|
+
converted = value.send(self.class.attributes[name].converter) unless value.nil?
|
81
|
+
return converted if @attributes[name] == converted
|
82
|
+
if @persisted_attributes && @persisted_attributes[name] == converted
|
83
|
+
@changed_attributes.delete(name.to_s)
|
84
|
+
else
|
85
|
+
attribute_will_change!(name.to_s)
|
86
|
+
end
|
87
|
+
@attributes[name] = converted
|
88
|
+
end
|
89
|
+
|
90
|
+
def attribute(name)
|
91
|
+
@attributes[name]
|
92
|
+
end
|
93
|
+
|
94
|
+
def attribute=(name, value)
|
95
|
+
raise ModelHasBeenDeleted.new("can't update attribute #{name}") if deleted?
|
96
|
+
assert_can_update!(name) unless @reloading
|
97
|
+
write_attribute(name.to_sym, value)
|
98
|
+
end
|
99
|
+
|
100
|
+
def assert_can_update!(name)
|
101
|
+
definition = self.class.attributes[name.to_sym]
|
102
|
+
if definition.read_only? && @attributes[name].present?
|
103
|
+
raise ReadOnlyAttribute.new("can't change #{name}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_default_attribute_values
|
108
|
+
self.class.attributes.each do |name, attribute|
|
109
|
+
unless @attributes.has_key?(name)
|
110
|
+
@attributes[name] = if attribute.default_value?
|
111
|
+
attribute_will_change!(name.to_s)
|
112
|
+
attribute.default_value
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_initial_attribute_values
|
121
|
+
self.class.attributes.each do |name, attribute|
|
122
|
+
unless @attributes[name].present?
|
123
|
+
@attributes[name] = if attribute.initial_value?
|
124
|
+
attribute_will_change!(name.to_s)
|
125
|
+
attribute.initial_value
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def attributes_key_name
|
134
|
+
@attributes_key_name ||= self.class.attributes_key_name_for(id)
|
135
|
+
end
|
136
|
+
|
137
|
+
def attributes_synced_with_db!
|
138
|
+
@persisted_attributes = @attributes.clone
|
139
|
+
@previously_changed = changes
|
140
|
+
@changed_attributes.clear if @changed_attributes
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class OptionsExpander
|
145
|
+
def initialize(model, opts = {})
|
146
|
+
opts.slice(:initial, :default).each do |name, value|
|
147
|
+
next unless value.is_a?(Symbol) && respond_to?(value)
|
148
|
+
opts[name] = send(value, model)
|
149
|
+
end
|
150
|
+
opts
|
151
|
+
end
|
152
|
+
|
153
|
+
def sequence(model)
|
154
|
+
key = KeyNamer.sequence(model, :type => "id")
|
155
|
+
Proc.new { model.redis.incr(key).to_s }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "ricordami/has_attributes"
|
2
|
+
require "ricordami/unique_index"
|
3
|
+
require "ricordami/value_index"
|
4
|
+
|
5
|
+
module Ricordami
|
6
|
+
module HasIndices
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def indices
|
11
|
+
@indices ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def index(options = {})
|
15
|
+
# for now we can only create unique indices
|
16
|
+
options.assert_valid_keys(:unique, :get_by, :value)
|
17
|
+
fields = options.delete(:unique)
|
18
|
+
return unique_index(fields, options) if fields.present?
|
19
|
+
field = options.delete(:value)
|
20
|
+
return value_index(field) if field.present?
|
21
|
+
raise InvalidIndexDefinition.new(self.class)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def add_index(index)
|
27
|
+
return false if self.indices.has_key?(index.name)
|
28
|
+
self.indices[index.name] = index
|
29
|
+
end
|
30
|
+
|
31
|
+
def value_index(field)
|
32
|
+
index = ValueIndex.new(self, field)
|
33
|
+
return nil unless add_index(index)
|
34
|
+
queue_saving_operations do |obj, session|
|
35
|
+
old_v = obj.send("#{field}_was")
|
36
|
+
new_v = obj.send(field)
|
37
|
+
next if old_v == new_v
|
38
|
+
if obj.persisted? && old_v.present?
|
39
|
+
indices[index.name].rem(obj.id, old_v)
|
40
|
+
end
|
41
|
+
indices[index.name].add(obj.id, new_v)
|
42
|
+
end
|
43
|
+
queue_deleting_operations do |obj, session|
|
44
|
+
if value = obj.send("#{field}_was")
|
45
|
+
indices[index.name].rem(obj.id, value, true).each do |command|
|
46
|
+
session.commands << command
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
index
|
51
|
+
end
|
52
|
+
|
53
|
+
def unique_index(fields, options = {})
|
54
|
+
create_unique_index(fields, options).tap do |index|
|
55
|
+
next if index.nil?
|
56
|
+
create_unique_get_method(index) if options[:get_by]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_unique_index(fields, options)
|
61
|
+
index = UniqueIndex.new(self, fields, options)
|
62
|
+
return nil unless add_index(index)
|
63
|
+
queue_saving_operations do |obj, session|
|
64
|
+
old_v = serialize_values(index.fields, obj, :previous => true)
|
65
|
+
new_v = serialize_values(index.fields, obj)
|
66
|
+
next if old_v == new_v
|
67
|
+
if obj.persisted? && old_v.present?
|
68
|
+
indices[index.name].rem(obj.id, old_v)
|
69
|
+
end
|
70
|
+
indices[index.name].add(obj.id, new_v)
|
71
|
+
end
|
72
|
+
queue_deleting_operations do |obj, session|
|
73
|
+
if value = serialize_values(index.fields, obj)
|
74
|
+
indices[index.name].rem(obj.id, value, true).each do |command|
|
75
|
+
session.commands << command
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
index
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_unique_get_method(index)
|
83
|
+
meth = :"get_by_#{index.fields.map(&:to_s).join("-")}"
|
84
|
+
define_singleton_method(meth) do |*args|
|
85
|
+
all = redis.hgetall(index.ref_key_name)
|
86
|
+
id = index.id_for_values(*args)
|
87
|
+
get(id)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def serialize_values(fields, obj, opts = {})
|
92
|
+
fields.map do |f|
|
93
|
+
attr = opts[:previous] ? "#{f}_was" : f
|
94
|
+
obj.send(attr)
|
95
|
+
end.join(UniqueIndex::SEPARATOR)
|
96
|
+
end
|
97
|
+
|
98
|
+
def define_singleton_method(*args, &block)
|
99
|
+
class << self
|
100
|
+
self
|
101
|
+
end.send(:define_method, *args, &block)
|
102
|
+
end unless method_defined? :define_singleton_method
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
module IsLockable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module InstanceMethods
|
8
|
+
# Pretty much stolen from redis objects
|
9
|
+
# http://github.com/nateware/redis-objects/blob/master/lib/redis/lock.rb
|
10
|
+
def lock!(options = {}, &block)
|
11
|
+
key = KeyNamer.lock(self.class, :id => id)
|
12
|
+
start = Time.now
|
13
|
+
acquired_lock = false
|
14
|
+
expiration = nil
|
15
|
+
expires_in = options.fetch(:expiration, 15)
|
16
|
+
timeout = options.fetch(:timeout, 1)
|
17
|
+
|
18
|
+
while (Time.now - start) < timeout
|
19
|
+
expiration = generate_expiration(expires_in)
|
20
|
+
acquired_lock = redis.setnx(key, expiration)
|
21
|
+
break if acquired_lock
|
22
|
+
|
23
|
+
old_expiration = redis.get(key).to_f
|
24
|
+
|
25
|
+
if old_expiration < Time.now.to_f
|
26
|
+
expiration = generate_expiration(expires_in)
|
27
|
+
old_expiration = redis.getset(key, expiration).to_f
|
28
|
+
|
29
|
+
if old_expiration < Time.now.to_f
|
30
|
+
acquired_lock = true
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
sleep 0.1
|
36
|
+
end
|
37
|
+
|
38
|
+
raise(LockTimeout.new(key, timeout)) unless acquired_lock
|
39
|
+
|
40
|
+
begin
|
41
|
+
yield
|
42
|
+
ensure
|
43
|
+
redis.del(key) if expiration > Time.now.to_f
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_expiration(expiration)
|
48
|
+
(Time.now + expiration.to_f).to_f
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|