redpear 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,28 +1,121 @@
1
- module Redpear::Connection
2
- extend Redpear::Concern
1
+ # == Redpear::Connection
2
+ #
3
+ # Abstract connection class, with support for master/slave sharding.
4
+ # @see Redpear::Connection#initialize for examples
5
+ #
6
+ class Redpear::Connection
3
7
 
4
- module ClassMethods
8
+ MASTER_METHODS = %w|
9
+ append auth
10
+ bgrewriteaof bgsave blpop brpop brpoplpush
11
+ config
12
+ decr decrby del discard
13
+ exec expire expireat
14
+ flushall flushdb getset
15
+ hset hsetnx hincrby hmset hdel
16
+ incr incrby
17
+ linsert lpop lpush lpushx lrem lset ltrim
18
+ mapped_hmset mapped_mset mapped_msetnx
19
+ move mset msetnx multi
20
+ persist pipelined psubscribe punsubscribe quit
21
+ rename renamenx rpop rpoplpush rpush rpushx
22
+ sadd save sdiffstore set setbit
23
+ setex setnx setrange sinterstore
24
+ shutdown smove spop srem subscribe
25
+ sunionstore sync synchronize
26
+ unsubscribe unwatch watch
27
+ zadd zincrby zinterstore zrem
28
+ zremrangebyrank zremrangebyscore zunionstore
29
+ |.freeze
5
30
 
6
- # @return [Redis] the current master connection
7
- def master_connection
8
- @master_connection ||= (superclass.respond_to?(:master_connection) ? superclass.master_connection : Redis.current)
31
+ SLAVE_METHODS = %w|
32
+ dbsize debug get getbit getrange
33
+ echo exists
34
+ hget hmget hexists hlen hkeys hvals hgetall
35
+ info keys lastsave lindex llen lrange
36
+ mapped_hmget mapped_mget mget monitor
37
+ object ping publish randomkey
38
+ scard sdiff select sinter sismember slaveof
39
+ smembers sort srandmember strlen substr sunion
40
+ ttl type
41
+ zcard zcount zrange zrangebyscore zrank
42
+ zrevrange zrevrangebyscore zrevrank zscore
43
+ |.freeze
44
+
45
+ # @return [Symbol] ther current connection, either :master or :slave
46
+ attr_reader :current
47
+ attr_accessor :master, :slave
48
+
49
+ # Constructor, accepts a master connection and an optional slave connection.
50
+ # Connections can be instances of Redis::Client, URL strings, or e.g.
51
+ # ConnectionPool objects. Examples:
52
+ #
53
+ # # Use current redis client as master and slave
54
+ # Redpear::Connection.new Redis.current
55
+ #
56
+ # # Use current redis client as slave and a remote URL as master
57
+ # Redpear::Connection.new "redis://master.host:6379", Redis.current
58
+ #
59
+ # # Use a connection pool - https://github.com/mperham/connection_pool
60
+ # slave_pool = ConnectionPool.new(:size => 5, :timeout => 5) { Redis.connect("redis://slave.host:6379") }
61
+ # Redpear::Connection.new "redis://master.host:6379", slave_pool
62
+ #
63
+ # @param [Redis::Client|String|ConnectionPool] master
64
+ # The master connection, defaults to `Redis.current`
65
+ # @param [Redis::Client|String|ConnectionPool] slave
66
+ # The (optional) slave connection, defaults to master
67
+ def initialize(master = Redis.current, slave = nil)
68
+ @master = _connect(master)
69
+ @slave = _connect(slave || master)
70
+ @transaction = nil
71
+ end
72
+
73
+ # @param [Symbol] name
74
+ # Either :master or :slave
75
+ # @yield
76
+ # Perform a block with the given connection
77
+ def on(name)
78
+ @current = send(name)
79
+ yield
80
+ ensure
81
+ @current = nil
82
+ end
83
+
84
+ # Run a transaction, prevents accidental transaction nesting
85
+ def transaction(&block)
86
+ if @transaction
87
+ yield
88
+ else
89
+ begin
90
+ @transaction = true
91
+ multi(&block)
92
+ ensure
93
+ @transaction = nil
94
+ end
9
95
  end
10
- alias_method :connection, :master_connection
96
+ end
11
97
 
12
- # @param [Redis] the master connection to assign
13
- def master_connection=(value)
14
- @master_connection = value
98
+ MASTER_METHODS.each do |meth|
99
+ define_method(meth) do |*a, &b|
100
+ (current || master).send(meth, *a, &b)
15
101
  end
102
+ end
16
103
 
17
- # @return [Redis] the current slave connection
18
- def slave_connection
19
- @slave_connection
104
+ SLAVE_METHODS.each do |meth|
105
+ define_method(meth) do |*a, &b|
106
+ (current || slave).send(meth, *a, &b)
20
107
  end
108
+ end
21
109
 
22
- # @param [Redis] the slave connection to assign
23
- def slave_connection=(value)
24
- @slave_connection = value
110
+ private
111
+
112
+ def _connect(conn)
113
+ case conn
114
+ when String
115
+ Redis.connect(:url => conn)
116
+ else
117
+ conn
118
+ end
25
119
  end
26
120
 
27
- end
28
121
  end
@@ -0,0 +1,18 @@
1
+ module Redpear::Model::Expiration
2
+
3
+ # Expires the record.
4
+ # @overload expire(time)
5
+ # @param [Time] time The time to expire the record at
6
+ # @overload expire(number)
7
+ # @param [Integer] number Expire in `number` of seconds from now
8
+ def expire(value)
9
+ attributes.expire(value)
10
+ end
11
+
12
+ # @return [Integer] the period this record has to live.
13
+ # May return nil for non-expiring records and non-existing records.
14
+ def ttl
15
+ attributes.ttl
16
+ end
17
+
18
+ end
@@ -0,0 +1,27 @@
1
+ require 'factory_girl'
2
+ require 'redpear/model'
3
+
4
+ class Redpear::Model
5
+
6
+ # FactoryGirl module for your tests/specs. Example:
7
+ #
8
+ # require 'redpear/model/factory_girl'
9
+ #
10
+ # FactoryGirl.define do
11
+ # factory :post do
12
+ # title { "A Title" }
13
+ # created_at { Time.at(1313131313) }
14
+ # end
15
+ # end
16
+ #
17
+ module FactoryGirl
18
+
19
+ # @return [Boolean] always true. FactoryGirl requires it.
20
+ def save!
21
+ true
22
+ end
23
+
24
+ end
25
+ include FactoryGirl
26
+
27
+ end
@@ -0,0 +1,37 @@
1
+ module Redpear::Model::Finders
2
+ extend Redpear::Concern
3
+
4
+ module ClassMethods
5
+
6
+ # @return [Integer] the number of total records
7
+ def count
8
+ members.size
9
+ end
10
+
11
+ # @param [String] id the ID to check
12
+ # @return [Boolean] true or false
13
+ def exists?(id)
14
+ !id.nil? && members.include?(id)
15
+ end
16
+
17
+ # @param [String] id the ID of the record to find
18
+ # @return [Redpear::Model] a record, or nil when not found
19
+ def find(id)
20
+ instantiate(id) if exists?(id)
21
+ end
22
+
23
+ # @return [Array<Redpear::Model>] all records
24
+ def all
25
+ members.map &method(:find)
26
+ end
27
+
28
+ # @yield over each available record
29
+ # @yieldparam [Redpear::Model] record
30
+ def find_each
31
+ members.each do |id|
32
+ yield find(id)
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ require 'machinist'
2
+ require 'redpear/model'
3
+
4
+ class Redpear::Model
5
+ extend ::Machinist::Machinable
6
+
7
+ # Used internally by Machinist
8
+ # @return [Class] the blueprint class
9
+ def self.blueprint_class
10
+ self::Machinist::Blueprint
11
+ end
12
+
13
+ # Machinist module for your tests/specs. Example:
14
+ #
15
+ # # spec/support/blueprints.rb
16
+ # require "redpear/model/machinist"
17
+ #
18
+ # Post.blueprint do
19
+ # title { "A Title" }
20
+ # created_at { 2.days.ago }
21
+ # end
22
+ #
23
+ module Machinist
24
+
25
+ class Blueprint < ::Machinist::Blueprint
26
+
27
+ def make!(attributes = {})
28
+ make(attributes)
29
+ end
30
+
31
+ def lathe_class #:nodoc:
32
+ Lathe
33
+ end
34
+
35
+ end
36
+
37
+ class Lathe < ::Machinist::Lathe
38
+ protected
39
+
40
+ def make_one_value(attribute, args)
41
+ return unless block_given?
42
+ raise_argument_error(attribute) unless args.empty?
43
+ yield
44
+ end
45
+
46
+ def assign_attribute(key, value) #:nodoc:
47
+ @assigned_attributes[key.to_sym] = value
48
+ @object[key] = value
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
data/lib/redpear/model.rb CHANGED
@@ -1,10 +1,9 @@
1
- require "set"
2
- require "redpear/core_ext/stringify_keys"
1
+ require 'redpear'
3
2
 
4
3
  =begin
5
4
  Redis is a simple key/value store, hence storing structured data can be a
6
- challenge. Redpear allows you to store/find/associate "records" in a Redis DB
7
- very efficiently, minimising IO operations and storage space where possible.
5
+ challenge. Redpear::Model allows you to store/find/associate "records" in a Redis
6
+ DB very efficiently, minimising IO operations and storage space where possible.
8
7
 
9
8
  For example:
10
9
 
@@ -18,53 +17,100 @@ For example:
18
17
  index :post_id
19
18
  end
20
19
 
21
- Let's create a post and a comment:
22
-
23
- post = Post.save :title => "Hi!", :body => "I'm a new post"
24
- comment = Comment.save :post_id => post.id, :body => "I like this!"
25
-
26
- Redpear is VERY lightweight. Compared with other ORMs, it offers raw speed at
27
- the expense of convenience.
20
+ Redpear::Model is VERY lightweight. It is optimised for raw speed at the
21
+ expense of convenience.
28
22
  =end
29
23
  class Redpear::Model < Hash
30
- include Redpear::Connection
31
- include Redpear::Namespace
32
- include Redpear::Persistence
33
- include Redpear::Expiration
34
- include Redpear::Counters
24
+ autoload :Finders, 'redpear/model/finders'
25
+ autoload :Expiration, 'redpear/model/expiration'
26
+
35
27
  include Redpear::Schema
36
- include Redpear::Finders
28
+ include Finders
29
+ include Expiration
30
+
31
+ class << self
32
+
33
+ alias_method :create, :new
34
+
35
+ # @param [Redpear::Connection] define a custom connection
36
+ attr_writer :connection
37
+
38
+ # @param [String] define a custom scope
39
+ attr_writer :scope
40
+
41
+ # @return [Redpear::Connection] the connection
42
+ def connection
43
+ @connection ||= (superclass.respond_to?(:connection) ? superclass.connection : Redpear::Connection.new)
44
+ end
45
+
46
+ # @return [String] the scope of this model. Example:
47
+ # Comment.scope # => "comments"
48
+ def scope
49
+ @scope ||= "#{name.split('::').last.downcase}s"
50
+ end
51
+
52
+ # @param [multiple] tokens
53
+ # The tokens to add to the scope
54
+ # @return [String] the full scope.
55
+ # Examples:
56
+ # Comment.nested_key(123) # => "comments:123"
57
+ # Comment.nested_key("abc", 123) # => "comments:abc:123"
58
+ def nested_key(*tokens)
59
+ [scope, *tokens].join(':')
60
+ end
37
61
 
38
- # Ensure we can read raw level values
39
- alias_method :__fetch__, :[]
62
+ # @return [Redpear::Store::Set] the IDs of all existing records
63
+ def members
64
+ @_members ||= Redpear::Store::Set.new nested_key(:~), connection
65
+ end
66
+
67
+ # @return [Redpear::Store::Counter] the generator of primary keys
68
+ def pk_counter
69
+ @_pk_counter ||= Redpear::Store::Counter.new nested_key(:+), connection
70
+ end
71
+
72
+ # Runs a bulk-operation.
73
+ # @yield operations that should be run in the transaction
74
+ def transaction(&block)
75
+ connection.transaction(&block)
76
+ end
77
+
78
+ # Destroys a record. Example:
79
+ # @param [String] id the ID of the record to destroy
80
+ # @return [Redpear::Model] the destroyed record
81
+ def destroy(id)
82
+ instantiate(id).tap(&:destroy)
83
+ end
84
+
85
+ # Allocate an instance
86
+ def instantiate(id)
87
+ instance = allocate
88
+ instance.send :store, 'id', id.to_s
89
+ instance
90
+ end
91
+ private :instantiate
92
+
93
+ end
40
94
 
41
95
  def initialize(attrs = {})
42
96
  super()
43
- @__attributes__ = {}
44
- @__loaded__ = true
97
+ store 'id', (attrs.delete("id") || attrs.delete(:id) || self.class.pk_counter.next).to_s
45
98
  update(attrs)
99
+ self.class.members.add(id)
46
100
  end
47
101
 
48
- # Returns the ID of the record
49
- # @return [String]
102
+ # @return [String] the ID of this record
50
103
  def id
51
- value = __fetch__("id")
52
- value.to_s if value
53
- end
54
-
55
- # ID accessor
56
- # @param [Object] id
57
- def id=(value)
58
- self["id"] = value.to_s
104
+ fetch 'id'
59
105
  end
60
106
 
61
107
  # Custom comparator, inspired by ActiveRecord::Base#==
62
108
  # @param [Object] other the comparison object
63
- # @return [Boolean] true, if +other+ is persisted and ID
109
+ # @return [Boolean] true, if +other+ is persisted and ID
64
110
  def ==(other)
65
111
  case other
66
112
  when Redpear::Model
67
- other.instance_of?(self.class) && persisted? && other.id == id
113
+ other.instance_of?(self.class) && other.id == id
68
114
  else
69
115
  super
70
116
  end
@@ -76,57 +122,134 @@ class Redpear::Model < Hash
76
122
  id.hash
77
123
  end
78
124
 
79
- # Attribute reader with type-casting
125
+ # Reads and (caches) a single value
126
+ # @param [String] name
127
+ # The name of the attributes
80
128
  # @return [Object]
129
+ # The attribute value
81
130
  def [](name)
82
- __ensure_loaded__
83
- name = name.to_s
84
- @__attributes__[name] ||= begin
85
- column = self.class.columns.lookup[name]
86
- value = super(name)
87
- column ? column.type_cast(value) : value
131
+ return if frozen?
132
+
133
+ column = self.class.columns[name]
134
+ return super if column.nil? || key?(column)
135
+
136
+ value = case column
137
+ when Redpear::Schema::Score
138
+ column.members[id]
139
+ when Redpear::Schema::Column
140
+ attributes[column]
88
141
  end
142
+
143
+ store column, column.type_cast(value)
89
144
  end
90
145
 
91
- # Attribute writer
146
+ # Write a single attribute
92
147
  # @param [String] name
148
+ # The name of the attributes
93
149
  # @param [Object] value
150
+ # The value to store
94
151
  def []=(name, value)
95
- __ensure_loaded__
96
- name = name.to_s
97
- @__attributes__.delete(name)
98
- super
152
+ column = self.class.columns[name] || return
153
+ delete column.to_s
154
+ store_attribute attributes, column, value
99
155
  end
100
156
 
101
- # Returns a Hash with attributes
102
- # @param [Boolean] clean
103
- # If true, only actual values will be returned (without nils), defaults to false
157
+ # Increments the value of a counter attribute
158
+ # @param [String] name
159
+ # The column name to increment
160
+ # @param [Integer] by
161
+ # Increment by this value
162
+ def increment(name, by = 1)
163
+ column = self.class.columns[name]
164
+ return false unless column && column.type == :counter
165
+
166
+ store column, attributes.increment(column, by)
167
+ end
168
+
169
+ # Decrements the value of a counter attribute
170
+ # @param [String|Symbol] name
171
+ # The column name to decrement
172
+ # @param [Integer] by
173
+ # Decrement by this value
174
+ def decrement(name, by = 1)
175
+ increment name, -by
176
+ end
177
+
178
+ # Bulk-updates the model
104
179
  # @return [Hash]
105
- def to_hash(clean = false)
106
- __ensure_loaded__
107
- attrs = clean ? reject {|_, v| v.nil? } : self
108
- {}.update(attrs)
180
+ def update(hash)
181
+ clear
182
+ self.class.transaction do
183
+ bulk = {}
184
+ hash.each do |name, value|
185
+ column = self.class.columns[name] || next
186
+ store_attribute bulk, column, value
187
+ end
188
+ attributes.merge! bulk
189
+ end
190
+ self
191
+ end
192
+
193
+ # Clear all the cached attributes, but keep ID
194
+ # @return [Hash] self
195
+ def clear
196
+ value = self.id
197
+ super
198
+ ensure
199
+ store 'id', value
200
+ end
201
+
202
+ # Returns the attributes store
203
+ # @return [Redpear::Store::Hash] attributes
204
+ def attributes
205
+ @_attributes ||= Redpear::Store::Hash.new self.class.nested_key("", id), self.class.connection
206
+ end
207
+
208
+ # Return lookups, relevant to this record
209
+ # @return [Array<Redpear::Store::Enumerable>] the lookups this record is related to
210
+ def lookups
211
+ @_lookups ||= [self.class.members] + self.class.columns.indicies.map {|i| i.for(self) }
212
+ end
213
+
214
+ # Destroy the record.
215
+ # @return [Boolean] true if successful
216
+ def destroy
217
+ lookups # Build before transaction
218
+ self.class.transaction do
219
+ lookups.each {|l| l.delete(id) }
220
+ attributes.purge!
221
+ end
222
+ freeze
109
223
  end
110
224
 
111
225
  # Show information about this record
112
226
  # @return [String]
113
227
  def inspect
114
- __ensure_loaded__
115
228
  "#<#{self.class.name} #{super}>"
116
229
  end
117
230
 
118
- # Bulk-update attributes
119
- # @param [Hash] attrs
120
- def update(attrs)
121
- attrs = (attrs ? attrs.stringify_keys : {})
122
- attrs["id"] = attrs["id"].to_s if attrs["id"]
123
- super
231
+ # Cache a key/value pair
232
+ def store(key, value)
233
+ super key.to_s, value
124
234
  end
125
235
 
126
- private
127
-
128
- def __ensure_loaded__
129
- refresh_attributes unless @__loaded__
236
+ # Store an attribute in target
237
+ def store_attribute(target, column, value)
238
+ value = column.encode_value(value)
239
+
240
+ case column
241
+ when Redpear::Schema::Score
242
+ column.members[id] = value unless value.nil?
243
+ when Redpear::Schema::Index
244
+ column.members(value).add(id) unless value.nil?
245
+ target[column] = value
246
+ when Redpear::Schema::Column
247
+ target[column] = value
130
248
  end
131
249
 
250
+ value
251
+ end
252
+ protected :store, :store_attribute
253
+ private :fetch, :delete, :delete_if, :keep_if, :merge!, :reject!, :select!, :replace
254
+
132
255
  end
@@ -1,7 +1,7 @@
1
1
  # Stores the column information
2
2
  class Redpear::Schema::Collection < Array
3
3
 
4
- # @param [multiple] the column definition. Please see Redpear::Column#initialize
4
+ # @param [multiple] the column definition. Please see Redpear::Schema::Column#initialize
5
5
  def store(klass, *args)
6
6
  reset!
7
7
  klass.new(*args).tap do |col|
@@ -14,16 +14,16 @@ class Redpear::Schema::Collection < Array
14
14
  @names ||= lookup.keys
15
15
  end
16
16
 
17
- # @return [Array] the names of the indices only
18
- def indices
19
- @indices ||= select(&:index?)
20
- end
21
-
22
17
  # @return [Hash] the column lookup, indexed by name
23
18
  def lookup
24
19
  @lookup ||= inject({}) {|r, c| r.update c.to_s => c }
25
20
  end
26
21
 
22
+ # @return [Array] only the index columns
23
+ def indicies
24
+ @indicies ||= to_a.select {|i| i.is_a?(Redpear::Schema::Index) }
25
+ end
26
+
27
27
  # @param [String] the column name
28
28
  # @return [Boolean] if name is part of the collection
29
29
  def include?(name)
@@ -31,7 +31,7 @@ class Redpear::Schema::Collection < Array
31
31
  end
32
32
 
33
33
  # @param [String] the column name
34
- # @return [Redpear::Column] the column for the given name
34
+ # @return [Redpear::Schema::Column] the column for the given name
35
35
  def [](name)
36
36
  lookup[name.to_s]
37
37
  end
@@ -1,4 +1,4 @@
1
- class Redpear::Column < String
1
+ class Redpear::Schema::Column < String
2
2
  attr_reader :type, :model
3
3
 
4
4
  # Creates a new column.
@@ -35,6 +35,17 @@ class Redpear::Column < String
35
35
  end
36
36
  end
37
37
 
38
+ # Encodes a value, for storage
39
+ # @return [Object] the encoded value
40
+ def encode_value(value)
41
+ case value
42
+ when Time
43
+ value.to_i
44
+ else
45
+ value
46
+ end
47
+ end
48
+
38
49
  # @return [String] the column name
39
50
  def name
40
51
  to_s
@@ -50,9 +61,4 @@ class Redpear::Column < String
50
61
  true
51
62
  end
52
63
 
53
- # @return [Boolean] true if the column is an index
54
- def index?
55
- is_a? Redpear::Index
56
- end
57
-
58
64
  end
@@ -0,0 +1,22 @@
1
+ class Redpear::Schema::Index < Redpear::Schema::Column
2
+
3
+ # @param [Redpear::Model] record the owner record
4
+ # @return [Redpear::Store::Set] the set holding the IDs for `record's` index
5
+ def for(record)
6
+ members record.send(name)
7
+ end
8
+
9
+ # @param [String] index value
10
+ # @return [Redpear::Store::Set] the set holding the IDs for the given `foreign_key`
11
+ def members(value)
12
+ value = '_' if value.nil?
13
+ Redpear::Store::Set.new nested_key(name, value), model.connection
14
+ end
15
+
16
+ private
17
+
18
+ def nested_key(*tokens)
19
+ model.nested_key(:~, *tokens)
20
+ end
21
+
22
+ end
@@ -0,0 +1,13 @@
1
+ class Redpear::Schema::Score < Redpear::Schema::Index
2
+
3
+ # @return [Redpear::Store::Set] the set holding the IDs for `record's` index
4
+ def for(*)
5
+ members
6
+ end
7
+
8
+ # @return [Redpear::Store::SortedSet] the sorted set holding the pairs
9
+ def members(*)
10
+ @members ||= Redpear::Store::SortedSet.new nested_key(name), model.connection
11
+ end
12
+
13
+ end