sequel 2.9.0 → 2.10.0

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