sequel_model 0.1

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.
@@ -0,0 +1,97 @@
1
+ module Sequel
2
+ class Model
3
+ # Returns the database associated with the Model class.
4
+ def self.db
5
+ @db ||= (superclass != Object) && superclass.db or
6
+ raise Error, "No database associated with #{self}"
7
+ end
8
+
9
+ # Sets the database associated with the Model class.
10
+ def self.db=(db)
11
+ @db = db
12
+ end
13
+
14
+ # Called when a database is opened in order to automatically associate the
15
+ # first opened database with model classes.
16
+ def self.database_opened(db)
17
+ @db = db if (self == Model) && !@db
18
+ end
19
+
20
+ # Returns the dataset associated with the Model class.
21
+ def self.dataset
22
+ @dataset || super_dataset or
23
+ raise Error, "No dataset associated with #{self}"
24
+ end
25
+
26
+ def self.super_dataset # :nodoc:
27
+ superclass.dataset if superclass and superclass.respond_to? :dataset
28
+ end
29
+
30
+ # Returns the columns in the result set in their original order.
31
+ #
32
+ # See Dataset#columns for more information.
33
+ def self.columns
34
+ @columns ||= @dataset.columns or
35
+ raise Error, "Could not fetch columns for #{self}"
36
+ end
37
+
38
+ # Sets the dataset associated with the Model class.
39
+ def self.set_dataset(ds)
40
+ @db = ds.db
41
+ @dataset = ds
42
+ @dataset.set_model(self)
43
+ @dataset.transform(@transform) if @transform
44
+ end
45
+
46
+ # Returns the database assoiated with the object's Model class.
47
+ def db
48
+ @db ||= model.db
49
+ end
50
+
51
+ # Returns the dataset assoiated with the object's Model class.
52
+ #
53
+ # See Dataset for more information.
54
+ def dataset
55
+ model.dataset
56
+ end
57
+
58
+ # Returns the columns associated with the object's Model class.
59
+ def columns
60
+ model.columns
61
+ end
62
+
63
+ # Serializes column with YAML or through marshalling.
64
+ def self.serialize(*columns)
65
+ format = columns.pop[:format] if Hash === columns.last
66
+ format ||= :yaml
67
+
68
+ @transform = columns.inject({}) do |m, c|
69
+ m[c] = format
70
+ m
71
+ end
72
+ @dataset.transform(@transform) if @dataset
73
+ end
74
+ end
75
+
76
+ # Lets you create a Model class with its table name already set or reopen
77
+ # an existing Model.
78
+ #
79
+ # Makes given dataset inherited.
80
+ #
81
+ # === Example:
82
+ # class Comment < Sequel::Model(:comments)
83
+ # table_name # => :comments
84
+ #
85
+ # # ...
86
+ #
87
+ # end
88
+ def self.Model(source)
89
+ @models ||= {}
90
+ @models[source] ||= Class.new(Sequel::Model) do
91
+ meta_def(:inherited) do |c|
92
+ c.set_dataset(source.is_a?(Dataset) ? source : c.db[source])
93
+ end
94
+ end
95
+ end
96
+
97
+ end
@@ -0,0 +1,42 @@
1
+ module Sequel
2
+ class Model
3
+ def self.set_cache(store, opts = {})
4
+ @cache_store = store
5
+ if (ttl = opts[:ttl])
6
+ set_cache_ttl(ttl)
7
+ end
8
+
9
+ meta_def(:[]) do |*args|
10
+ if (args.size == 1) && (Hash === (h = args.first))
11
+ return dataset[h]
12
+ end
13
+
14
+ unless obj = @cache_store.get(cache_key_from_values(args))
15
+ obj = dataset[primary_key_hash((args.size == 1) ? args.first : args)]
16
+ @cache_store.set(cache_key_from_values(args), obj, cache_ttl)
17
+ end
18
+ obj
19
+ end
20
+
21
+ class_def(:set) {|v| store.delete(cache_key); super}
22
+ class_def(:save) {store.delete(cache_key); super}
23
+ class_def(:delete) {store.delete(cache_key); super}
24
+ end
25
+
26
+ def self.set_cache_ttl(ttl)
27
+ @cache_ttl = ttl
28
+ end
29
+
30
+ def self.cache_store
31
+ @cache_store
32
+ end
33
+
34
+ def self.cache_ttl
35
+ @cache_ttl ||= 3600
36
+ end
37
+
38
+ def self.cache_key_from_values(values)
39
+ "#{self}:#{values.join(',')}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,122 @@
1
+ module Sequel
2
+ class Model
3
+ # This Hash translates verbs to methodnames used in chain manipulation
4
+ # methods.
5
+ VERB_TO_METHOD = {:prepend => :unshift, :append => :push}
6
+
7
+ # Returns @hooks which is an instance of Hash with its hook identifier
8
+ # (Symbol) as key and the chain of hooks (Array) as value.
9
+ #
10
+ # If it is not already set it'll be with an empty set of hooks.
11
+ # This behaviour will change in the future to allow inheritance.
12
+ #
13
+ # For the time being, you should be able to do:
14
+ #
15
+ # class A < Sequel::Model(:a)
16
+ # before_save { 'Do something...' }
17
+ # end
18
+ #
19
+ # class B < A
20
+ # @hooks = superclass.hooks.clone
21
+ # before_save # => [#<Proc:0x0000c6e8@(example.rb):123>]
22
+ # end
23
+ #
24
+ # In this case you should remember that the clone doesn't create any new
25
+ # instances of your chains, so if you change the chain here it changes in
26
+ # its superclass, too.
27
+ def self.hooks
28
+ @hooks ||= Hash.new { |h, k| h[k] = [] }
29
+ end
30
+
31
+ # Adds block to chain of Hooks for <tt>:before_save</tt>.
32
+ # It can either be prepended (default) or appended.
33
+ #
34
+ # Returns the chain itself.
35
+ #
36
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
37
+ def self.before_save(verb = :prepend, &block)
38
+ hooks[:before_save].send VERB_TO_METHOD.fetch(verb), block if block
39
+ hooks[:before_save]
40
+ end
41
+ # Adds block to chain of Hooks for <tt>:before_create</tt>.
42
+ # It can either be prepended (default) or appended.
43
+ #
44
+ # Returns the chain itself.
45
+ #
46
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
47
+ def self.before_create(verb = :prepend, &block)
48
+ hooks[:before_create].send VERB_TO_METHOD.fetch(verb), block if block
49
+ hooks[:before_create]
50
+ end
51
+ # Adds block to chain of Hooks for <tt>:before_update</tt>.
52
+ # It can either be prepended (default) or appended.
53
+ #
54
+ # Returns the chain itself.
55
+ #
56
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
57
+ def self.before_update(verb = :prepend, &block)
58
+ hooks[:before_update].send VERB_TO_METHOD.fetch(verb), block if block
59
+ hooks[:before_update]
60
+ end
61
+ # Adds block to chain of Hooks for <tt>:before_destroy</tt>.
62
+ # It can either be prepended (default) or appended.
63
+ #
64
+ # Returns the chain itself.
65
+ #
66
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
67
+ def self.before_destroy(verb = :prepend, &block)
68
+ hooks[:before_destroy].send VERB_TO_METHOD.fetch(verb), block if block
69
+ hooks[:before_destroy]
70
+ end
71
+
72
+ # Adds block to chain of Hooks for <tt>:after_save</tt>.
73
+ # It can either be prepended or appended (default).
74
+ #
75
+ # Returns the chain itself.
76
+ #
77
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
78
+ def self.after_save(verb = :append, &block)
79
+ hooks[:after_save].send VERB_TO_METHOD.fetch(verb), block if block
80
+ hooks[:after_save]
81
+ end
82
+ # Adds block to chain of Hooks for <tt>:after_create</tt>.
83
+ # It can either be prepended or appended (default).
84
+ #
85
+ # Returns the chain itself.
86
+ #
87
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
88
+ def self.after_create(verb = :append, &block)
89
+ hooks[:after_create].send VERB_TO_METHOD.fetch(verb), block if block
90
+ hooks[:after_create]
91
+ end
92
+ # Adds block to chain of Hooks for <tt>:after_update</tt>.
93
+ # It can either be prepended or appended (default).
94
+ #
95
+ # Returns the chain itself.
96
+ #
97
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
98
+ def self.after_update(verb = :append, &block)
99
+ hooks[:after_update].send VERB_TO_METHOD.fetch(verb), block if block
100
+ hooks[:after_update]
101
+ end
102
+ # Adds block to chain of Hooks for <tt>:after_destroy</tt>.
103
+ # It can either be prepended or appended (default).
104
+ #
105
+ # Returns the chain itself.
106
+ #
107
+ # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
108
+ def self.after_destroy(verb = :append, &block)
109
+ hooks[:after_destroy].send VERB_TO_METHOD.fetch(verb), block if block
110
+ hooks[:after_destroy]
111
+ end
112
+
113
+ # Evaluates specified chain of Hooks through <tt>instance_eval</tt>.
114
+ def run_hooks(key)
115
+ model.hooks[key].each {|h| instance_eval(&h)}
116
+ end
117
+
118
+ def self.has_hooks?(key)
119
+ hooks[key] && !hooks[key].empty?
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,44 @@
1
+ module Sequel
2
+ module Plugins; end
3
+
4
+ class Model
5
+ class << self
6
+ # Loads a plugin for use with the model class, passing optional arguments
7
+ # to the plugin.
8
+ def is(plugin, *args)
9
+ m = plugin_module(plugin)
10
+ if m.respond_to?(:apply)
11
+ m.apply(self, *args)
12
+ end
13
+ if m.const_defined?("InstanceMethods")
14
+ class_def(:"#{plugin}_opts") {args.first}
15
+ include(m::InstanceMethods)
16
+ end
17
+ if m.const_defined?("ClassMethods")
18
+ meta_def(:"#{plugin}_opts") {args.first}
19
+ metaclass.send(:include, m::ClassMethods)
20
+ end
21
+ if m.const_defined?("DatasetMethods")
22
+ dataset.meta_def(:"#{plugin}_opts") {args.first}
23
+ dataset.metaclass.send(:include, m::DatasetMethods)
24
+ end
25
+ end
26
+ alias_method :is_a, :is
27
+
28
+ # Returns the module for the specified plugin. If the module is not
29
+ # defined, the corresponding plugin gem is automatically loaded.
30
+ def plugin_module(plugin)
31
+ module_name = plugin.to_s.gsub(/(^|_)(.)/) {$2.upcase}
32
+ if not Sequel::Plugins.const_defined?(module_name)
33
+ require plugin_gem(plugin)
34
+ end
35
+ Sequel::Plugins.const_get(module_name)
36
+ end
37
+
38
+ # Returns the gem name for the given plugin.
39
+ def plugin_gem(plugin)
40
+ "sequel_#{plugin}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,73 @@
1
+ module Sequel
2
+ # Prints nice-looking plain-text tables
3
+ # +--+-------+
4
+ # |id|name |
5
+ # |--+-------|
6
+ # |1 |fasdfas|
7
+ # |2 |test |
8
+ # +--+-------+
9
+ module PrettyTable
10
+ def self.records_columns(records)
11
+ columns = []
12
+ records.each do |r|
13
+ if Array === r && (k = r.keys)
14
+ return k
15
+ elsif Hash === r
16
+ r.keys.each {|k| columns << k unless columns.include?(k)}
17
+ end
18
+ end
19
+ columns
20
+ end
21
+
22
+ def self.column_sizes(records, columns)
23
+ sizes = Hash.new {0}
24
+ columns.each do |c|
25
+ s = c.to_s.size
26
+ sizes[c.to_sym] = s if s > sizes[c.to_sym]
27
+ end
28
+ records.each do |r|
29
+ columns.each do |c|
30
+ s = r[c].to_s.size
31
+ sizes[c.to_sym] = s if s > sizes[c.to_sym]
32
+ end
33
+ end
34
+ sizes
35
+ end
36
+
37
+ def self.separator_line(columns, sizes)
38
+ l = ''
39
+ '+' + columns.map {|c| '-' * sizes[c]}.join('+') + '+'
40
+ end
41
+
42
+ def self.format_cell(size, v)
43
+ case v
44
+ when Bignum, Fixnum
45
+ "%#{size}d" % v
46
+ when Float
47
+ "%#{size}g" % v
48
+ else
49
+ "%-#{size}s" % v.to_s
50
+ end
51
+ end
52
+
53
+ def self.data_line(columns, sizes, record)
54
+ '|' + columns.map {|c| format_cell(sizes[c], record[c])}.join('|') + '|'
55
+ end
56
+
57
+ def self.header_line(columns, sizes)
58
+ '|' + columns.map {|c| "%-#{sizes[c]}s" % c.to_s}.join('|') + '|'
59
+ end
60
+
61
+ def self.print(records, columns = nil) # records is an array of hashes
62
+ columns ||= records_columns(records)
63
+ sizes = column_sizes(records, columns)
64
+
65
+ puts separator_line(columns, sizes)
66
+ puts header_line(columns, sizes)
67
+ puts separator_line(columns, sizes)
68
+ records.each {|r| puts data_line(columns, sizes, r)}
69
+ puts separator_line(columns, sizes)
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,309 @@
1
+ module Sequel
2
+ class Model
3
+ attr_reader :values
4
+ attr_reader :changed_columns
5
+
6
+ # Returns value of attribute.
7
+ def [](column)
8
+ @values[column]
9
+ end
10
+ # Sets value of attribute and marks the column as changed.
11
+ def []=(column, value)
12
+ @values[column] = value
13
+ @changed_columns << column unless @changed_columns.include?(column)
14
+ end
15
+
16
+ # Enumerates through all attributes.
17
+ #
18
+ # === Example:
19
+ # Ticket.find(7).each { |k, v| puts "#{k} => #{v}" }
20
+ def each(&block)
21
+ @values.each(&block)
22
+ end
23
+ # Returns attribute names.
24
+ def keys
25
+ @values.keys
26
+ end
27
+
28
+ # Returns value for <tt>:id</tt> attribute.
29
+ def id
30
+ @values[:id]
31
+ end
32
+
33
+ # Compares model instances by values.
34
+ def ==(obj)
35
+ (obj.class == model) && (obj.values == @values)
36
+ end
37
+
38
+ # Compares model instances by pkey.
39
+ def ===(obj)
40
+ (obj.class == model) && (obj.pk == pk)
41
+ end
42
+
43
+ # Returns key for primary key.
44
+ def self.primary_key
45
+ :id
46
+ end
47
+
48
+ # Returns primary key attribute hash.
49
+ def self.primary_key_hash(value)
50
+ {:id => value}
51
+ end
52
+
53
+ # Sets primary key, regular and composite are possible.
54
+ #
55
+ # == Example:
56
+ # class Tagging < Sequel::Model(:taggins)
57
+ # # composite key
58
+ # set_primary_key :taggable_id, :tag_id
59
+ # end
60
+ #
61
+ # class Person < Sequel::Model(:person)
62
+ # # regular key
63
+ # set_primary_key :person_id
64
+ # end
65
+ #
66
+ # <i>You can even set it to nil!</i>
67
+ def self.set_primary_key(*key)
68
+ # if k is nil, we go to no_primary_key
69
+ if key.empty? || (key.size == 1 && key.first == nil)
70
+ return no_primary_key
71
+ end
72
+
73
+ # backwards compat
74
+ key = (key.length == 1) ? key[0] : key.flatten
75
+
76
+ # redefine primary_key
77
+ meta_def(:primary_key) {key}
78
+
79
+ unless key.is_a? Array # regular primary key
80
+ class_def(:this) do
81
+ @this ||= dataset.filter(key => @values[key]).limit(1).naked
82
+ end
83
+ class_def(:pk) do
84
+ @pk ||= @values[key]
85
+ end
86
+ class_def(:pk_hash) do
87
+ @pk ||= {key => @values[key]}
88
+ end
89
+ class_def(:cache_key) do
90
+ pk = @values[key] || (raise Error, 'no primary key for this record')
91
+ @cache_key ||= "#{self.class}:#{pk}"
92
+ end
93
+ meta_def(:primary_key_hash) do |v|
94
+ {key => v}
95
+ end
96
+ else # composite key
97
+ exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
98
+ block = eval("proc {@this ||= self.class.dataset.filter(#{exp_list.join(',')}).limit(1).naked}")
99
+ class_def(:this, &block)
100
+
101
+ exp_list = key.map {|k| "@values[#{k.inspect}]"}
102
+ block = eval("proc {@pk ||= [#{exp_list.join(',')}]}")
103
+ class_def(:pk, &block)
104
+
105
+ exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
106
+ block = eval("proc {@this ||= {#{exp_list.join(',')}}}")
107
+ class_def(:pk_hash, &block)
108
+
109
+ exp_list = key.map {|k| '#{@values[%s]}' % k.inspect}.join(',')
110
+ block = eval('proc {@cache_key ||= "#{self.class}:%s"}' % exp_list)
111
+ class_def(:cache_key, &block)
112
+
113
+ meta_def(:primary_key_hash) do |v|
114
+ key.inject({}) {|m, i| m[i] = v.shift; m}
115
+ end
116
+ end
117
+ end
118
+
119
+ def self.no_primary_key #:nodoc:
120
+ meta_def(:primary_key) {nil}
121
+ meta_def(:primary_key_hash) {|v| raise Error, "#{self} does not have a primary key"}
122
+ class_def(:this) {raise Error, "No primary key is associated with this model"}
123
+ class_def(:pk) {raise Error, "No primary key is associated with this model"}
124
+ class_def(:pk_hash) {raise Error, "No primary key is associated with this model"}
125
+ class_def(:cache_key) {raise Error, "No primary key is associated with this model"}
126
+ end
127
+
128
+ # Creates new instance with values set to passed-in Hash ensuring that
129
+ # new? returns true.
130
+ def self.create(values = {})
131
+ db.transaction do
132
+ obj = new(values, true)
133
+ obj.save
134
+ obj
135
+ end
136
+ end
137
+
138
+ class << self
139
+ def create_with_params(params)
140
+ create(params.reject {|k, v| !columns.include?(k.to_sym)})
141
+ end
142
+ alias_method :create_with, :create_with_params
143
+ end
144
+
145
+ # Returns (naked) dataset bound to current instance.
146
+ def this
147
+ @this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked
148
+ end
149
+
150
+ # Returns a key unique to the underlying record for caching
151
+ def cache_key
152
+ pk = @values[:id] || (raise Error, 'no primary key for this record')
153
+ @cache_key ||= "#{self.class}:#{pk}"
154
+ end
155
+
156
+ # Returns primary key column(s) for object's Model class.
157
+ def primary_key
158
+ @primary_key ||= self.class.primary_key
159
+ end
160
+
161
+ # Returns value for primary key.
162
+ def pkey
163
+ warn "Model#pkey is deprecated. Please use Model#pk instead."
164
+ @pkey ||= @values[self.class.primary_key]
165
+ end
166
+
167
+ # Returns the primary key value identifying the model instance. Stock implementation.
168
+ def pk
169
+ @pk ||= @values[:id]
170
+ end
171
+
172
+ # Returns a hash identifying the model instance. Stock implementation.
173
+ def pk_hash
174
+ @pk_hash ||= {:id => @values[:id]}
175
+ end
176
+
177
+ # Creates new instance with values set to passed-in Hash.
178
+ #
179
+ # This method guesses whether the record exists when
180
+ # <tt>new_record</tt> is set to false.
181
+ def initialize(values = {}, new_record = false, &block)
182
+ @values = values
183
+ @changed_columns = []
184
+
185
+ @new = new_record
186
+ unless @new # determine if it's a new record
187
+ k = self.class.primary_key
188
+ # if there's no primary key for the model class, or
189
+ # @values doesn't contain a primary key value, then
190
+ # we regard this instance as new.
191
+ @new = (k == nil) || (!(Array === k) && !@values[k])
192
+ end
193
+
194
+ block[self] if block
195
+ end
196
+
197
+ # Returns true if the current instance represents a new record.
198
+ def new?
199
+ @new
200
+ end
201
+ alias :new_record? :new?
202
+
203
+ # Returns true when current instance exists, false otherwise.
204
+ def exists?
205
+ this.count > 0
206
+ end
207
+
208
+ # Creates or updates the associated record. This method can also
209
+ # accept a list of specific columns to update.
210
+ def save(*columns)
211
+ run_hooks(:before_save)
212
+ if @new
213
+ run_hooks(:before_create)
214
+ iid = model.dataset.insert(@values)
215
+ # if we have a regular primary key and it's not set in @values,
216
+ # we assume it's the last inserted id
217
+ if (pk = primary_key) && !(Array === pk) && !@values[pk]
218
+ @values[pk] = iid
219
+ end
220
+ if pk
221
+ @this = nil # remove memoized this dataset
222
+ refresh
223
+ end
224
+ @new = false
225
+ run_hooks(:after_create)
226
+ else
227
+ run_hooks(:before_update)
228
+ if columns.empty?
229
+ this.update(@values)
230
+ @changed_columns = []
231
+ else # update only the specified columns
232
+ this.update(@values.reject {|k, v| !columns.include?(k)})
233
+ @changed_columns.reject! {|c| columns.include?(c)}
234
+ end
235
+ run_hooks(:after_update)
236
+ end
237
+ run_hooks(:after_save)
238
+ self
239
+ end
240
+
241
+ # Saves only changed columns or does nothing if no columns are marked as
242
+ # chanaged.
243
+ def save_changes
244
+ save(*@changed_columns) unless @changed_columns.empty?
245
+ end
246
+
247
+ # Updates and saves values to database from the passed-in Hash.
248
+ def set(values)
249
+ this.update(values)
250
+ values.each {|k, v| @values[k] = v}
251
+ end
252
+ alias_method :update, :set
253
+
254
+ # Reloads values from database and returns self.
255
+ def refresh
256
+ @values = this.first || raise(Error, "Record not found")
257
+ self
258
+ end
259
+
260
+ # Like delete but runs hooks before and after delete.
261
+ def destroy
262
+ db.transaction do
263
+ run_hooks(:before_destroy)
264
+ delete
265
+ run_hooks(:after_destroy)
266
+ end
267
+ end
268
+
269
+ # Deletes and returns self.
270
+ def delete
271
+ this.delete
272
+ self
273
+ end
274
+
275
+ ATTR_RE = /^([a-zA-Z_]\w*)(=)?$/.freeze
276
+
277
+ def method_missing(m, *args) #:nodoc:
278
+ if m.to_s =~ ATTR_RE
279
+ att = $1.to_sym
280
+ write = $2 == '='
281
+
282
+ # check whether the column is legal
283
+ unless columns.include?(att)
284
+ # if read accessor and a value exists for the column, we return it
285
+ if !write && @values.has_key?(att)
286
+ return @values[att]
287
+ end
288
+
289
+ # otherwise, raise an error
290
+ raise Error, "Invalid column (#{att.inspect}) for #{self}"
291
+ end
292
+
293
+ # define the column accessor
294
+ Thread.exclusive do
295
+ if write
296
+ model.class_def(m) {|v| self[att] = v}
297
+ else
298
+ model.class_def(m) {self[att]}
299
+ end
300
+ end
301
+
302
+ # call the accessor
303
+ respond_to?(m) ? send(m, *args) : super(m, *args)
304
+ else
305
+ super(m, *args)
306
+ end
307
+ end
308
+ end
309
+ end