redpear 0.6.4 → 0.7.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.
@@ -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