sequel 2.9.0 → 2.10.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.
Files changed (78) hide show
  1. data/CHANGELOG +56 -0
  2. data/{README → README.rdoc} +85 -57
  3. data/Rakefile +10 -5
  4. data/bin/sequel +7 -16
  5. data/doc/advanced_associations.rdoc +5 -17
  6. data/doc/cheat_sheet.rdoc +18 -20
  7. data/doc/dataset_filtering.rdoc +8 -32
  8. data/doc/schema.rdoc +20 -0
  9. data/lib/sequel_core.rb +35 -1
  10. data/lib/sequel_core/adapters/ado.rb +1 -1
  11. data/lib/sequel_core/adapters/db2.rb +2 -2
  12. data/lib/sequel_core/adapters/dbi.rb +2 -11
  13. data/lib/sequel_core/adapters/do.rb +205 -0
  14. data/lib/sequel_core/adapters/do/mysql.rb +38 -0
  15. data/lib/sequel_core/adapters/do/postgres.rb +92 -0
  16. data/lib/sequel_core/adapters/do/sqlite.rb +31 -0
  17. data/lib/sequel_core/adapters/firebird.rb +298 -0
  18. data/lib/sequel_core/adapters/informix.rb +10 -1
  19. data/lib/sequel_core/adapters/jdbc.rb +78 -19
  20. data/lib/sequel_core/adapters/jdbc/h2.rb +69 -0
  21. data/lib/sequel_core/adapters/jdbc/mysql.rb +10 -0
  22. data/lib/sequel_core/adapters/jdbc/postgresql.rb +7 -3
  23. data/lib/sequel_core/adapters/mysql.rb +53 -77
  24. data/lib/sequel_core/adapters/odbc.rb +1 -1
  25. data/lib/sequel_core/adapters/openbase.rb +1 -1
  26. data/lib/sequel_core/adapters/oracle.rb +2 -2
  27. data/lib/sequel_core/adapters/postgres.rb +16 -14
  28. data/lib/sequel_core/adapters/shared/mysql.rb +44 -9
  29. data/lib/sequel_core/adapters/shared/oracle.rb +4 -5
  30. data/lib/sequel_core/adapters/shared/postgres.rb +86 -82
  31. data/lib/sequel_core/adapters/shared/sqlite.rb +39 -24
  32. data/lib/sequel_core/adapters/sqlite.rb +11 -1
  33. data/lib/sequel_core/connection_pool.rb +10 -2
  34. data/lib/sequel_core/core_sql.rb +13 -3
  35. data/lib/sequel_core/database.rb +131 -30
  36. data/lib/sequel_core/database/schema.rb +5 -5
  37. data/lib/sequel_core/dataset.rb +31 -6
  38. data/lib/sequel_core/dataset/convenience.rb +11 -11
  39. data/lib/sequel_core/dataset/query.rb +2 -2
  40. data/lib/sequel_core/dataset/sql.rb +6 -6
  41. data/lib/sequel_core/exceptions.rb +4 -0
  42. data/lib/sequel_core/migration.rb +4 -4
  43. data/lib/sequel_core/schema/generator.rb +19 -3
  44. data/lib/sequel_core/schema/sql.rb +24 -20
  45. data/lib/sequel_core/sql.rb +13 -16
  46. data/lib/sequel_core/version.rb +11 -0
  47. data/lib/sequel_model.rb +2 -0
  48. data/lib/sequel_model/base.rb +2 -2
  49. data/lib/sequel_model/hooks.rb +46 -7
  50. data/lib/sequel_model/record.rb +11 -9
  51. data/lib/sequel_model/schema.rb +1 -1
  52. data/lib/sequel_model/validations.rb +72 -61
  53. data/spec/adapters/firebird_spec.rb +371 -0
  54. data/spec/adapters/mysql_spec.rb +118 -62
  55. data/spec/adapters/oracle_spec.rb +5 -5
  56. data/spec/adapters/postgres_spec.rb +33 -18
  57. data/spec/adapters/sqlite_spec.rb +2 -2
  58. data/spec/integration/dataset_test.rb +3 -3
  59. data/spec/integration/schema_test.rb +55 -5
  60. data/spec/integration/spec_helper.rb +11 -0
  61. data/spec/integration/type_test.rb +59 -16
  62. data/spec/sequel_core/connection_pool_spec.rb +14 -0
  63. data/spec/sequel_core/core_sql_spec.rb +24 -14
  64. data/spec/sequel_core/database_spec.rb +96 -11
  65. data/spec/sequel_core/dataset_spec.rb +97 -37
  66. data/spec/sequel_core/expression_filters_spec.rb +51 -40
  67. data/spec/sequel_core/object_graph_spec.rb +2 -2
  68. data/spec/sequel_core/schema_generator_spec.rb +31 -6
  69. data/spec/sequel_core/schema_spec.rb +25 -9
  70. data/spec/sequel_core/spec_helper.rb +4 -1
  71. data/spec/sequel_core/version_spec.rb +7 -0
  72. data/spec/sequel_model/associations_spec.rb +1 -1
  73. data/spec/sequel_model/hooks_spec.rb +68 -13
  74. data/spec/sequel_model/model_spec.rb +4 -4
  75. data/spec/sequel_model/record_spec.rb +22 -0
  76. data/spec/sequel_model/spec_helper.rb +2 -1
  77. data/spec/sequel_model/validations_spec.rb +107 -7
  78. metadata +15 -5
@@ -0,0 +1,11 @@
1
+ module Sequel
2
+ MAJOR = 2
3
+ MINOR = 10
4
+ TINY = 0
5
+
6
+ VERSION = [MAJOR, MINOR, TINY].join('.')
7
+
8
+ def self.version
9
+ VERSION
10
+ end
11
+ end
@@ -50,6 +50,8 @@ module Sequel
50
50
  # cache_store, cache_ttl, dataset_methods, primary_key, restricted_columns,
51
51
  # sti_dataset, and sti_key. You should not usually need to
52
52
  # access these directly.
53
+ # * All validation methods also accept the options specified in #validates_each,
54
+ # in addition to the options specified in the RDoc for that method.
53
55
  # * The following class level attr_accessors are created: raise_on_typecast_failure,
54
56
  # raise_on_save_failure, strict_param_setting, typecast_empty_string_to_nil,
55
57
  # and typecast_on_assignment:
@@ -402,8 +402,8 @@ module Sequel
402
402
  # create dataset methods, so they can be chained for scoping.
403
403
  # For example:
404
404
  #
405
- # Topic.subset(:popular, :num_posts > 100)
406
- # Topic.subset(:recent, :created_on > Date.today - 7)
405
+ # Topic.subset(:popular, :num_posts.sql_number > 100)
406
+ # Topic.subset(:recent, :created_on + 7 > Date.today)
407
407
  #
408
408
  # Allows you to do:
409
409
  #
@@ -8,6 +8,45 @@ module Sequel
8
8
  # Hooks that are only for internal use
9
9
  PRIVATE_HOOKS = [:before_update_values, :before_delete]
10
10
 
11
+ # This adds a new hook type. It will define both a class
12
+ # method that you can use to add hooks, as well as an instance method
13
+ # that you can use to call all hooks of that type. The class method
14
+ # can be called with a symbol or a block or both. If a block is given and
15
+ # and symbol is not, it adds the hook block to the hook type. If a block
16
+ # and symbol are both given, it replaces the hook block associated with
17
+ # that symbol for a given hook type, or adds it if there is no hook block
18
+ # with that symbol for that hook type. If no block is given, it assumes
19
+ # the symbol specifies an instance method to call and adds it to the hook
20
+ # type.
21
+ #
22
+ # If any hook block returns false, the instance method will return false
23
+ # immediately without running the rest of the hooks of that type.
24
+ #
25
+ # It is recommended that you always provide a symbol to this method,
26
+ # for descriptive purposes. It's only necessary to do so when you
27
+ # are using a system that reloads code.
28
+ #
29
+ # All of Sequel's standard hook types are also implemented using this
30
+ # method.
31
+ #
32
+ # Example of usage:
33
+ #
34
+ # class MyModel
35
+ # define_hook :before_move_to
36
+ # before_move_to(:check_move_allowed){|o| o.allow_move?}
37
+ # def move_to(there)
38
+ # return if before_move_to == false
39
+ # # move MyModel object to there
40
+ # end
41
+ # end
42
+ def self.add_hook_type(*hooks)
43
+ hooks.each do |hook|
44
+ @hooks[hook] = []
45
+ instance_eval("def #{hook}(method = nil, &block); define_hook_instance_method(:#{hook}); add_hook(:#{hook}, method, &block) end")
46
+ class_eval("def #{hook}; end")
47
+ end
48
+ end
49
+
11
50
  # Returns true if there are any hook blocks for the given hook.
12
51
  def self.has_hooks?(hook)
13
52
  !@hooks[hook].empty?
@@ -32,7 +71,11 @@ module Sequel
32
71
  if tag && (old = h.find{|x| x[0] == tag})
33
72
  old[1] = block
34
73
  else
35
- h << [tag, block]
74
+ if hook.to_s =~ /^before/
75
+ h.unshift([tag,block])
76
+ else
77
+ h << [tag, block]
78
+ end
36
79
  end
37
80
  end
38
81
 
@@ -43,7 +86,7 @@ module Sequel
43
86
 
44
87
  private_class_method :add_hook, :define_hook_instance_method
45
88
 
46
- private
89
+ private
47
90
 
48
91
  # Runs all hook blocks of given hook type on this object.
49
92
  # Stops running hook blocks and returns false if any hook block returns false.
@@ -53,10 +96,6 @@ module Sequel
53
96
 
54
97
  # For performance reasons, we define empty hook instance methods, which are
55
98
  # overwritten with real hook instance methods whenever the hook class method is called.
56
- (HOOKS + PRIVATE_HOOKS).each do |hook|
57
- @hooks[hook] = []
58
- instance_eval("def #{hook}(method = nil, &block); define_hook_instance_method(:#{hook}); add_hook(:#{hook}, method, &block) end")
59
- class_eval("def #{hook}; end")
60
- end
99
+ add_hook_type(*(HOOKS + PRIVATE_HOOKS))
61
100
  end
62
101
  end
@@ -199,7 +199,7 @@ module Sequel
199
199
  def save!(*columns)
200
200
  opts = columns.extract_options!
201
201
  return save_failure(:save) if before_save == false
202
- if @new
202
+ if new?
203
203
  return save_failure(:create) if before_create == false
204
204
  ds = model.dataset
205
205
  if ds.respond_to?(:insert_select) and h = ds.insert_select(@values)
@@ -212,26 +212,28 @@ module Sequel
212
212
  if (pk = primary_key) && !(Array === pk) && !@values[pk]
213
213
  @values[pk] = iid
214
214
  end
215
- if pk
216
- @this = nil # remove memoized this dataset
217
- refresh
218
- end
215
+ @this = nil if pk
219
216
  end
220
- @new = false
221
217
  after_create
218
+ after_save
219
+ @new = false
220
+ refresh if pk
222
221
  else
223
222
  return save_failure(:update) if before_update == false
224
223
  if columns.empty?
225
224
  vals = opts[:changed] ? @values.reject{|k,v| !changed_columns.include?(k)} : @values
226
225
  this.update(vals)
227
- changed_columns.clear
228
226
  else # update only the specified columns
229
227
  this.update(@values.reject{|k, v| !columns.include?(k)})
230
- changed_columns.reject!{|c| columns.include?(c)}
231
228
  end
232
229
  after_update
230
+ after_save
231
+ if columns.empty?
232
+ changed_columns.clear
233
+ else
234
+ changed_columns.reject!{|c| columns.include?(c)}
235
+ end
233
236
  end
234
- after_save
235
237
  self
236
238
  end
237
239
 
@@ -2,7 +2,7 @@ module Sequel
2
2
  class Model
3
3
  # Creates table, using the column information from set_schema.
4
4
  def self.create_table
5
- db.create_table(table_name, @schema)
5
+ db.create_table(table_name, :generator=>@schema)
6
6
  @db_schema = get_db_schema(true)
7
7
  columns
8
8
  end
@@ -104,26 +104,21 @@ module Sequel
104
104
  end
105
105
 
106
106
  # Validates acceptance of an attribute. Just checks that the value
107
- # is equal to the :accept option.
107
+ # is equal to the :accept option. This method is unique in that
108
+ # :allow_nil is assumed to be true instead of false.
108
109
  #
109
110
  # Possible Options:
110
111
  # * :accept - The value required for the object to be valid (default: '1')
111
- # * :allow_blank - Whether to skip the validation if the value is blank (default: false)
112
- # * :allow_nil - Whether to skip the validation if the value is nil (default: true)
113
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
114
- # skipping this validation if it returns nil or false.
115
112
  # * :message - The message to use (default: 'is not accepted')
116
- # * :tag - The tag to use for this validation (default: :acceptance)
117
113
  def self.validates_acceptance_of(*atts)
118
114
  opts = {
119
115
  :message => 'is not accepted',
120
116
  :allow_nil => true,
121
- :accept => '1'
117
+ :accept => '1',
118
+ :tag => :acceptance,
122
119
  }.merge!(atts.extract_options!)
123
-
124
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:acceptance}
120
+ atts << opts
125
121
  validates_each(*atts) do |o, a, v|
126
- next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
127
122
  o.errors[a] << opts[:message] unless v == opts[:accept]
128
123
  end
129
124
  end
@@ -137,22 +132,15 @@ module Sequel
137
132
  # or email addresses on web forms.
138
133
  #
139
134
  # Possible Options:
140
- # * :allow_false - Whether to skip the validation if the value is blank (default: false)
141
- # * :allow_nil - Whether to skip the validation if the value is nil (default: false)
142
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
143
- # skipping this validation if it returns nil or false.
144
135
  # * :message - The message to use (default: 'is not confirmed')
145
- # * :tag - The tag to use for this validation (default: :confirmation)
146
136
  def self.validates_confirmation_of(*atts)
147
137
  opts = {
148
138
  :message => 'is not confirmed',
139
+ :tag => :confirmation,
149
140
  }.merge!(atts.extract_options!)
150
-
151
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:confirmation}
141
+ atts << opts
152
142
  validates_each(*atts) do |o, a, v|
153
- next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
154
- c = o.send(:"#{a}_confirmation")
155
- o.errors[a] << opts[:message] unless v == c
143
+ o.errors[a] << opts[:message] unless v == o.send(:"#{a}_confirmation")
156
144
  end
157
145
  end
158
146
 
@@ -165,13 +153,30 @@ module Sequel
165
153
  # end
166
154
  #
167
155
  # Possible Options:
156
+ # * :allow_blank - Whether to skip the validation if the value is blank.
157
+ # * :allow_missing - Whether to skip the validation if the attribute isn't a key in the
158
+ # values hash. This is different from allow_nil, because Sequel only sends the attributes
159
+ # in the values when doing an insert or update. If the attribute is not present, Sequel
160
+ # doesn't specify it, so the database will use the table's default value. This is different
161
+ # from having an attribute in values with a value of nil, which Sequel will send as NULL.
162
+ # If your database table has a non NULL default, this may be a good option to use. You
163
+ # don't want to use allow_nil, because if the attribute is in values but has a value nil,
164
+ # Sequel will attempt to insert a NULL value into the database, instead of using the
165
+ # database's default.
166
+ # * :allow_nil - Whether to skip the validation if the value is nil.
168
167
  # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
169
168
  # skipping this validation if it returns nil or false.
170
- # * :tag - The tag to use for this validation (default: nil)
169
+ # * :tag - The tag to use for this validation.
171
170
  def self.validates_each(*atts, &block)
172
171
  opts = atts.extract_options!
173
- blk = if opts[:if]
174
- proc{|o,a,v| block.call(o,a,v) if o.instance_eval(&if_proc(opts))}
172
+ blk = if (i = opts[:if]) || (am = opts[:allow_missing]) || (an = opts[:allow_nil]) || (ab = opts[:allow_blank])
173
+ proc do |o,a,v|
174
+ next if i && !o.instance_eval(&if_proc(opts))
175
+ next if an && Array(v).all?{|x| x.nil?}
176
+ next if ab && Array(v).all?{|x| x.blank?}
177
+ next if am && Array(a).all?{|x| !o.values.has_key?(x)}
178
+ block.call(o,a,v)
179
+ end
175
180
  else
176
181
  block
177
182
  end
@@ -190,25 +195,20 @@ module Sequel
190
195
  # value against the regular expression provided by the :with option.
191
196
  #
192
197
  # Possible Options:
193
- # * :allow_blank - Whether to skip the validation if the value is blank (default: false)
194
- # * :allow_nil - Whether to skip the validation if the value is nil (default: false)
195
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
196
- # skipping this validation if it returns nil or false.
197
198
  # * :message - The message to use (default: 'is invalid')
198
- # * :tag - The tag to use for this validation (default: :format)
199
199
  # * :with - The regular expression to validate the value with (required).
200
200
  def self.validates_format_of(*atts)
201
201
  opts = {
202
202
  :message => 'is invalid',
203
+ :tag => :format,
203
204
  }.merge!(atts.extract_options!)
204
205
 
205
206
  unless opts[:with].is_a?(Regexp)
206
207
  raise ArgumentError, "A regular expression must be supplied as the :with option of the options hash"
207
208
  end
208
209
 
209
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:format}
210
+ atts << opts
210
211
  validates_each(*atts) do |o, a, v|
211
- next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
212
212
  o.errors[a] << opts[:message] unless v.to_s =~ opts[:with]
213
213
  end
214
214
  end
@@ -216,16 +216,11 @@ module Sequel
216
216
  # Validates the length of an attribute.
217
217
  #
218
218
  # Possible Options:
219
- # * :allow_blank - Whether to skip the validation if the value is blank (default: false)
220
- # * :allow_nil - Whether to skip the validation if the value is nil (default: false)
221
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
222
- # skipping this validation if it returns nil or false.
223
219
  # * :is - The exact size required for the value to be valid (no default)
224
220
  # * :maximum - The maximum size allowed for the value (no default)
225
221
  # * :message - The message to use (no default, overrides :too_long, :too_short, and :wrong_length
226
222
  # options if present)
227
223
  # * :minimum - The minimum size allowed for the value (no default)
228
- # * :tag - The tag to use for this validation (default: :length)
229
224
  # * :too_long - The message to use use if it the value is too long (default: 'is too long')
230
225
  # * :too_short - The message to use use if it the value is too short (default: 'is too short')
231
226
  # * :with - The array/range that must include the size of the value for it to be valid (no default)
@@ -237,14 +232,9 @@ module Sequel
237
232
  :wrong_length => 'is the wrong length'
238
233
  }.merge!(atts.extract_options!)
239
234
 
240
- tag = if opts[:tag]
241
- opts[:tag]
242
- else
243
- ([:length] + [:maximum, :minimum, :is, :within].reject{|x| !opts.include?(x)}).join('-').to_sym
244
- end
245
- atts << {:if=>opts[:if], :tag=>tag}
235
+ opts[:tag] ||= ([:length] + [:maximum, :minimum, :is, :within].reject{|x| !opts.include?(x)}).join('-').to_sym
236
+ atts << opts
246
237
  validates_each(*atts) do |o, a, v|
247
- next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
248
238
  if m = opts[:maximum]
249
239
  o.errors[a] << (opts[:message] || opts[:too_long]) unless v && v.size <= m
250
240
  end
@@ -260,24 +250,46 @@ module Sequel
260
250
  end
261
251
  end
262
252
 
253
+ # Validates whether an attribute is not a string. This is generally useful
254
+ # in conjunction with raise_on_typecast_failure = false, where you are
255
+ # passing in string values for non-string attributes (such as numbers and dates).
256
+ # If typecasting fails (invalid number or date), the value of the attribute will
257
+ # be a string in an invalid format, and if typecasting succeeds, the value will
258
+ # not be a string.
259
+ #
260
+ # Possible Options:
261
+ # * :message - The message to use (default: 'is a string' or 'is not a valid (integer|datetime|etc.)' if the type is known)
262
+ def self.validates_not_string(*atts)
263
+ opts = {
264
+ :tag => :not_string,
265
+ }.merge!(atts.extract_options!)
266
+ atts << opts
267
+ validates_each(*atts) do |o, a, v|
268
+ if v.is_a?(String)
269
+ unless message = opts[:message]
270
+ message = if sch = o.db_schema[a] and typ = sch[:type]
271
+ "is not a valid #{typ}"
272
+ else
273
+ "is a string"
274
+ end
275
+ end
276
+ o.errors[a] << message
277
+ end
278
+ end
279
+ end
280
+
263
281
  # Validates whether an attribute is a number.
264
282
  #
265
283
  # Possible Options:
266
- # * :allow_blank - Whether to skip the validation if the value is blank (default: false)
267
- # * :allow_nil - Whether to skip the validation if the value is nil (default: false)
268
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
269
- # skipping this validation if it returns nil or false.
270
284
  # * :message - The message to use (default: 'is not a number')
271
- # * :tag - The tag to use for this validation (default: :numericality)
272
285
  # * :only_integer - Whether only integers are valid values (default: false)
273
286
  def self.validates_numericality_of(*atts)
274
287
  opts = {
275
288
  :message => 'is not a number',
289
+ :tag => :numericality,
276
290
  }.merge!(atts.extract_options!)
277
-
278
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:numericality}
291
+ atts << opts
279
292
  validates_each(*atts) do |o, a, v|
280
- next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
281
293
  begin
282
294
  if opts[:only_integer]
283
295
  Kernel.Integer(v.to_s)
@@ -294,16 +306,13 @@ module Sequel
294
306
  # with false considered present instead of absent.
295
307
  #
296
308
  # Possible Options:
297
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
298
- # skipping this validation if it returns nil or false.
299
309
  # * :message - The message to use (default: 'is not present')
300
- # * :tag - The tag to use for this validation (default: :presence)
301
310
  def self.validates_presence_of(*atts)
302
311
  opts = {
303
312
  :message => 'is not present',
313
+ :tag => :presence,
304
314
  }.merge!(atts.extract_options!)
305
-
306
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:presence}
315
+ atts << opts
307
316
  validates_each(*atts) do |o, a, v|
308
317
  o.errors[a] << opts[:message] if v.blank? && v != false
309
318
  end
@@ -314,26 +323,28 @@ module Sequel
314
323
  # fields to specify that the combination of fields must be unique,
315
324
  # instead of that each field should have a unique value.
316
325
  #
326
+ # This means that the code:
327
+ # validates_uniqueness_of([:column1, :column2])
328
+ # validates the grouping of column1 and column2 while
329
+ # validates_uniqueness_of(:column1, :column2)
330
+ # validates them separately.
331
+ #
317
332
  # You should also add a unique index in the
318
333
  # database, as this suffers from a fairly obvious race condition.
319
334
  #
320
335
  # Possible Options:
321
- # * :allow_nil - Whether to skip the validation if the value(s) is/are nil (default: false)
322
- # * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
323
- # skipping this validation if it returns nil or false.
324
336
  # * :message - The message to use (default: 'is already taken')
325
- # * :tag - The tag to use for this validation (default: :uniqueness)
326
337
  def self.validates_uniqueness_of(*atts)
327
338
  opts = {
328
339
  :message => 'is already taken',
340
+ :tag => :uniqueness,
329
341
  }.merge!(atts.extract_options!)
330
342
 
331
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:uniqueness}
343
+ atts << opts
332
344
  validates_each(*atts) do |o, a, v|
333
345
  error_field = a
334
346
  a = Array(a)
335
347
  v = Array(v)
336
- next unless v.any? or opts[:allow_nil] == false
337
348
  ds = o.class.filter(a.zip(v))
338
349
  num_dups = ds.count
339
350
  allow = if num_dups == 0
@@ -0,0 +1,371 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ unless defined?(FIREBIRD_DB)
4
+ FIREBIRD_URL = 'firebird://sysdba:masterkey@localhost/reality_spec' unless defined? FIREBIRD_URL
5
+ FIREBIRD_DB = Sequel.connect(ENV['SEQUEL_FB_SPEC_DB']||FIREBIRD_URL)
6
+ end
7
+
8
+ FIREBIRD_DB.create_table! :test do
9
+ varchar :name, :size => 50
10
+ integer :val, :index => true
11
+ end
12
+
13
+ FIREBIRD_DB.create_table! :test2 do
14
+ integer :val
15
+ timestamp :time_stamp
16
+ end
17
+
18
+ FIREBIRD_DB.create_table! :test3 do
19
+ integer :val
20
+ timestamp :time_stamp
21
+ end
22
+
23
+ FIREBIRD_DB.create_table! :test5 do
24
+ primary_key :xid
25
+ integer :val
26
+ end
27
+
28
+ context "A Firebird database" do
29
+ setup do
30
+ @db = FIREBIRD_DB
31
+ end
32
+
33
+ specify "should provide disconnect functionality" do
34
+ @db.tables
35
+ @db.pool.size.should == 1
36
+ @db.disconnect
37
+ @db.pool.size.should == 0
38
+ end
39
+
40
+ specify "should raise Sequel::Error on error" do
41
+ proc{@db << "SELECT 1 + 'a'"}.should raise_error(Sequel::Error)
42
+ end
43
+ end
44
+
45
+ context "A Firebird dataset" do
46
+ setup do
47
+ @d = FIREBIRD_DB[:test]
48
+ @d.delete # remove all records
49
+ end
50
+
51
+ specify "should return the correct record count" do
52
+ @d.count.should == 0
53
+ @d << {:name => 'abc', :val => 123}
54
+ @d << {:name => 'abc', :val => 456}
55
+ @d << {:name => 'def', :val => 789}
56
+ @d.count.should == 3
57
+ end
58
+
59
+ specify "should return the correct records" do
60
+ @d.to_a.should == []
61
+ @d << {:name => 'abc', :val => 123}
62
+ @d << {:name => 'abc', :val => 456}
63
+ @d << {:name => 'def', :val => 789}
64
+
65
+ @d.order(:val).to_a.should == [
66
+ {:name => 'abc', :val => 123},
67
+ {:name => 'abc', :val => 456},
68
+ {:name => 'def', :val => 789}
69
+ ]
70
+ end
71
+
72
+ specify "should update records correctly" do
73
+ @d << {:name => 'abc', :val => 123}
74
+ @d << {:name => 'abc', :val => 456}
75
+ @d << {:name => 'def', :val => 789}
76
+ @d.filter(:name => 'abc').update(:val => 530)
77
+
78
+ # the third record should stay the same
79
+ # floating-point precision bullshit
80
+ @d[:name => 'def'][:val].should == 789
81
+ @d.filter(:val => 530).count.should == 2
82
+ end
83
+
84
+ specify "should delete records correctly" do
85
+ @d << {:name => 'abc', :val => 123}
86
+ @d << {:name => 'abc', :val => 456}
87
+ @d << {:name => 'def', :val => 789}
88
+ @d.filter(:name => 'abc').delete
89
+
90
+ @d.count.should == 1
91
+ @d.first[:name].should == 'def'
92
+ end
93
+
94
+ specify "should be able to literalize booleans" do
95
+ proc {@d.literal(true)}.should_not raise_error
96
+ proc {@d.literal(false)}.should_not raise_error
97
+ end
98
+
99
+ specify "should quote columns and tables using double quotes if quoting identifiers" do
100
+ @d.quote_identifiers = true
101
+ @d.select(:name).sql.should == \
102
+ 'SELECT "NAME" FROM "TEST"'
103
+
104
+ @d.select('COUNT(*)'.lit).sql.should == \
105
+ 'SELECT COUNT(*) FROM "TEST"'
106
+
107
+ @d.select(:max[:val]).sql.should == \
108
+ 'SELECT max("VAL") FROM "TEST"'
109
+
110
+ @d.select(:now[]).sql.should == \
111
+ 'SELECT now() FROM "TEST"'
112
+
113
+ @d.select(:max[:items__val]).sql.should == \
114
+ 'SELECT max("ITEMS"."VAL") FROM "TEST"'
115
+
116
+ @d.order(:name.desc).sql.should == \
117
+ 'SELECT * FROM "TEST" ORDER BY "NAME" DESC'
118
+
119
+ @d.select('TEST.NAME AS item_:name'.lit).sql.should == \
120
+ 'SELECT TEST.NAME AS item_:name FROM "TEST"'
121
+
122
+ @d.select('"NAME"'.lit).sql.should == \
123
+ 'SELECT "NAME" FROM "TEST"'
124
+
125
+ @d.select('max(TEST."NAME") AS "max_:name"'.lit).sql.should == \
126
+ 'SELECT max(TEST."NAME") AS "max_:name" FROM "TEST"'
127
+
128
+ @d.select(:test[:ABC, 'hello']).sql.should == \
129
+ "SELECT test(\"ABC\", 'hello') FROM \"TEST\""
130
+
131
+ @d.select(:test[:ABC__DEF, 'hello']).sql.should == \
132
+ "SELECT test(\"ABC\".\"DEF\", 'hello') FROM \"TEST\""
133
+
134
+ @d.select(:test[:ABC__DEF, 'hello'].as(:X2)).sql.should == \
135
+ "SELECT test(\"ABC\".\"DEF\", 'hello') AS \"X2\" FROM \"TEST\""
136
+
137
+ @d.insert_sql(:val => 333).should =~ \
138
+ /\AINSERT INTO "TEST" \("VAL"\) VALUES \(333\)( RETURNING NULL)?\z/
139
+
140
+ @d.insert_sql(:X => :Y).should =~ \
141
+ /\AINSERT INTO "TEST" \("X"\) VALUES \("Y"\)( RETURNING NULL)?\z/
142
+ end
143
+
144
+ specify "should quote fields correctly when reversing the order if quoting identifiers" do
145
+ @d.quote_identifiers = true
146
+ @d.reverse_order(:name).sql.should == \
147
+ 'SELECT * FROM "TEST" ORDER BY "NAME" DESC'
148
+
149
+ @d.reverse_order(:name.desc).sql.should == \
150
+ 'SELECT * FROM "TEST" ORDER BY "NAME" ASC'
151
+
152
+ @d.reverse_order(:name, :test.desc).sql.should == \
153
+ 'SELECT * FROM "TEST" ORDER BY "NAME" DESC, "TEST" ASC'
154
+
155
+ @d.reverse_order(:name.desc, :test).sql.should == \
156
+ 'SELECT * FROM "TEST" ORDER BY "NAME" ASC, "TEST" DESC'
157
+ end
158
+
159
+ specify "should support transactions" do
160
+ FIREBIRD_DB.transaction do
161
+ @d << {:name => 'abc', :val => 1}
162
+ end
163
+
164
+ @d.count.should == 1
165
+ end
166
+
167
+ specify "should have #transaction yield the connection" do
168
+ FIREBIRD_DB.transaction do |conn|
169
+ conn.should_not == nil
170
+ end
171
+ end
172
+
173
+ specify "should correctly rollback transactions" do
174
+ proc do
175
+ FIREBIRD_DB.transaction do
176
+ @d << {:name => 'abc', :val => 1}
177
+ raise RuntimeError, 'asdf'
178
+ end
179
+ end.should raise_error(RuntimeError)
180
+
181
+ @d.count.should == 0
182
+ end
183
+
184
+ specify "should handle returning inside of the block by committing" do
185
+ def FIREBIRD_DB.ret_commit
186
+ transaction do
187
+ self[:test] << {:name => 'abc'}
188
+ return
189
+ self[:test] << {:name => 'd'}
190
+ end
191
+ end
192
+ @d.count.should == 0
193
+ FIREBIRD_DB.ret_commit
194
+ @d.count.should == 1
195
+ FIREBIRD_DB.ret_commit
196
+ @d.count.should == 2
197
+ proc do
198
+ FIREBIRD_DB.transaction do
199
+ raise RuntimeError, 'asdf'
200
+ end
201
+ end.should raise_error(RuntimeError)
202
+
203
+ @d.count.should == 2
204
+ end
205
+
206
+ specify "should quote and upcase reserved keywords" do
207
+ @d = FIREBIRD_DB[:testing]
208
+ @d.quote_identifiers = true
209
+ @d.select(:select).sql.should == \
210
+ 'SELECT "SELECT" FROM "TESTING"'
211
+ end
212
+ end
213
+
214
+ context "A Firebird dataset with a timestamp field" do
215
+ setup do
216
+ @d = FIREBIRD_DB[:test3]
217
+ @d.delete
218
+ end
219
+
220
+ specify "should store milliseconds in time fields" do
221
+ t = Time.now
222
+ @d << {:val=>1, :time_stamp=>t}
223
+ @d.literal(@d[:val =>'1'][:time_stamp]).should == @d.literal(t)
224
+ @d[:val=>'1'][:time_stamp].usec.should == t.usec - t.usec % 100
225
+ end
226
+ end
227
+
228
+ context "A Firebird database" do
229
+ setup do
230
+ @db = FIREBIRD_DB
231
+ end
232
+
233
+ specify "should allow us to name the sequences" do
234
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
235
+ primary_key :id, :sequence_name => "seq_test"
236
+ end
237
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
238
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )",
239
+ "CREATE SEQUENCE SEQ_TEST",
240
+ " CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_test;\n end\n end\n\n"
241
+ ], "DROP SEQUENCE SEQ_TEST" ]
242
+ end
243
+
244
+ specify "should allow us to set the starting position for the sequences" do
245
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
246
+ primary_key :id, :sequence_start_position => 999
247
+ end
248
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
249
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )",
250
+ "CREATE SEQUENCE SEQ_POSTS_ID",
251
+ "ALTER SEQUENCE SEQ_POSTS_ID RESTART WITH 999",
252
+ " CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_posts_id;\n end\n end\n\n"
253
+ ], "DROP SEQUENCE SEQ_POSTS_ID" ]
254
+ end
255
+
256
+ specify "should allow us to name and set the starting position for the sequences" do
257
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
258
+ primary_key :id, :sequence_name => "seq_test", :sequence_start_position => 999
259
+ end
260
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
261
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )",
262
+ "CREATE SEQUENCE SEQ_TEST",
263
+ "ALTER SEQUENCE SEQ_TEST RESTART WITH 999",
264
+ " CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_test;\n end\n end\n\n"
265
+ ], "DROP SEQUENCE SEQ_TEST" ]
266
+ end
267
+
268
+ specify "should allow us to name the triggers" do
269
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
270
+ primary_key :id, :trigger_name => "trig_test"
271
+ end
272
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
273
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )",
274
+ "CREATE SEQUENCE SEQ_POSTS_ID",
275
+ " CREATE TRIGGER TRIG_TEST for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_posts_id;\n end\n end\n\n"
276
+ ], "DROP SEQUENCE SEQ_POSTS_ID" ]
277
+ end
278
+
279
+ specify "should allow us to not create the sequence" do
280
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
281
+ primary_key :id, :create_sequence => false
282
+ end
283
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
284
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )",
285
+ " CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_posts_id;\n end\n end\n\n"
286
+ ], nil]
287
+ end
288
+
289
+ specify "should allow us to not create the trigger" do
290
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
291
+ primary_key :id, :create_trigger => false
292
+ end
293
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
294
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )",
295
+ "CREATE SEQUENCE SEQ_POSTS_ID",
296
+ ], "DROP SEQUENCE SEQ_POSTS_ID"]
297
+ end
298
+
299
+ specify "should allow us to not create either the sequence nor the trigger" do
300
+ g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
301
+ primary_key :id, :create_sequence => false, :create_trigger => false
302
+ end
303
+ FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
304
+ "CREATE TABLE POSTS (ID integer PRIMARY KEY )"
305
+ ], nil]
306
+ end
307
+
308
+ specify "should support column operations" do
309
+ @db.create_table!(:test2){varchar :name, :size => 50; integer :val}
310
+ @db[:test2] << {}
311
+ @db[:test2].columns.should == [:name, :val]
312
+
313
+ @db.add_column :test2, :xyz, :varchar, :size => 50
314
+ @db[:test2].columns.should == [:name, :val, :xyz]
315
+
316
+ @db[:test2].columns.should == [:name, :val, :xyz]
317
+ @db.drop_column :test2, :xyz
318
+
319
+ @db[:test2].columns.should == [:name, :val]
320
+
321
+ @db[:test2].delete
322
+ @db.add_column :test2, :xyz, :varchar, :default => '000', :size => 50#, :create_domain => 'xyz_varchar'
323
+ @db[:test2] << {:name => 'mmm', :val => 111, :xyz => 'qqqq'}
324
+
325
+ @db[:test2].columns.should == [:name, :val, :xyz]
326
+ @db.rename_column :test2, :xyz, :zyx
327
+ @db[:test2].columns.should == [:name, :val, :zyx]
328
+ @db[:test2].first[:zyx].should == 'qqqq'
329
+
330
+ @db.add_column :test2, :xyz, :decimal, :elements => [12, 2]
331
+ @db[:test2].delete
332
+ @db[:test2] << {:name => 'mmm', :val => 111, :xyz => 56.4}
333
+ @db.set_column_type :test2, :xyz, :varchar, :size => 50
334
+
335
+ @db[:test2].first[:xyz].should == "56.40"
336
+ end
337
+ end
338
+
339
+ context "Postgres::Dataset#insert" do
340
+ setup do
341
+ @ds = FIREBIRD_DB[:test5]
342
+ @ds.delete
343
+ end
344
+
345
+ specify "should using call insert_returning_sql" do
346
+ @ds.should_receive(:single_value).once.with(:sql=>'INSERT INTO TEST5 (VAL) VALUES (10) RETURNING XID')
347
+ @ds.insert(:val=>10)
348
+ end
349
+
350
+ specify "should have insert_returning_sql use the RETURNING keyword" do
351
+ @ds.insert_returning_sql(:XID, :val=>10).should == "INSERT INTO TEST5 (VAL) VALUES (10) RETURNING XID"
352
+ @ds.insert_returning_sql('*'.lit, :val=>10).should == "INSERT INTO TEST5 (VAL) VALUES (10) RETURNING *"
353
+ @ds.insert_returning_sql('NULL'.lit, :val=>10).should == "INSERT INTO TEST5 (VAL) VALUES (10) RETURNING NULL"
354
+ end
355
+
356
+ specify "should correctly return the inserted record's primary key value" do
357
+ value1 = 10
358
+ id1 = @ds.insert(:val=>value1)
359
+ @ds.first(:XID=>id1)[:val].should == value1
360
+ value2 = 20
361
+ id2 = @ds.insert(:val=>value2)
362
+ @ds.first(:XID=>id2)[:val].should == value2
363
+ end
364
+
365
+ specify "should return nil if the table has no primary key" do
366
+ ds = FIREBIRD_DB[:test]
367
+ ds.delete
368
+ ds.insert(:name=>'a').should == nil
369
+ end
370
+ end
371
+