sequel_model 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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