sequel 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/association_basics.rdoc +18 -0
  4. data/doc/migration.rdoc +30 -0
  5. data/doc/release_notes/4.7.0.txt +103 -0
  6. data/doc/security.rdoc +5 -0
  7. data/doc/sql.rdoc +21 -12
  8. data/doc/validations.rdoc +10 -2
  9. data/doc/virtual_rows.rdoc +22 -29
  10. data/lib/sequel/adapters/jdbc.rb +4 -1
  11. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  12. data/lib/sequel/adapters/odbc.rb +0 -1
  13. data/lib/sequel/adapters/postgres.rb +8 -1
  14. data/lib/sequel/adapters/shared/db2.rb +5 -0
  15. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  16. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  17. data/lib/sequel/adapters/shared/postgres.rb +5 -0
  18. data/lib/sequel/adapters/shared/sqlanywhere.rb +1 -1
  19. data/lib/sequel/adapters/shared/sqlite.rb +6 -1
  20. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +1 -1
  21. data/lib/sequel/database/schema_methods.rb +1 -0
  22. data/lib/sequel/database/transactions.rb +11 -26
  23. data/lib/sequel/dataset/actions.rb +3 -3
  24. data/lib/sequel/dataset/features.rb +5 -0
  25. data/lib/sequel/dataset/sql.rb +16 -1
  26. data/lib/sequel/model/associations.rb +22 -7
  27. data/lib/sequel/model/base.rb +2 -2
  28. data/lib/sequel/plugins/auto_validations.rb +5 -1
  29. data/lib/sequel/plugins/instance_hooks.rb +21 -3
  30. data/lib/sequel/plugins/pg_array_associations.rb +21 -5
  31. data/lib/sequel/plugins/update_or_create.rb +60 -0
  32. data/lib/sequel/plugins/validation_helpers.rb +5 -2
  33. data/lib/sequel/sql.rb +55 -9
  34. data/lib/sequel/version.rb +1 -1
  35. data/spec/adapters/postgres_spec.rb +25 -4
  36. data/spec/core/database_spec.rb +1 -1
  37. data/spec/core/dataset_spec.rb +1 -1
  38. data/spec/core/expression_filters_spec.rb +53 -1
  39. data/spec/extensions/auto_validations_spec.rb +18 -0
  40. data/spec/extensions/instance_hooks_spec.rb +14 -0
  41. data/spec/extensions/pg_array_associations_spec.rb +40 -0
  42. data/spec/extensions/to_dot_spec.rb +1 -1
  43. data/spec/extensions/update_or_create_spec.rb +81 -0
  44. data/spec/extensions/validation_helpers_spec.rb +15 -11
  45. data/spec/integration/associations_test.rb +1 -1
  46. data/spec/integration/database_test.rb +8 -0
  47. data/spec/integration/dataset_test.rb +15 -10
  48. data/spec/integration/type_test.rb +4 -0
  49. data/spec/model/associations_spec.rb +20 -0
  50. data/spec/spec_config.rb +1 -1
  51. metadata +364 -360
@@ -382,6 +382,11 @@ module Sequel
382
382
  end
383
383
  end
384
384
 
385
+ # DB2 supports quoted function names.
386
+ def supports_quoted_function_names?
387
+ true
388
+ end
389
+
385
390
  def _truncate_sql(table)
386
391
  # "TRUNCATE #{table} IMMEDIATE" is only for newer version of db2, so we
387
392
  # use the following one
@@ -301,7 +301,7 @@ module Sequel
301
301
  end
302
302
 
303
303
  DATABASE_ERROR_REGEXPS = {
304
- /Violation of UNIQUE KEY constraint/ => UniqueConstraintViolation,
304
+ /Violation of UNIQUE KEY constraint|Violation of PRIMARY KEY constraint.+Cannot insert duplicate key/ => UniqueConstraintViolation,
305
305
  /conflicted with the (FOREIGN KEY.*|REFERENCE) constraint/ => ForeignKeyConstraintViolation,
306
306
  /conflicted with the CHECK constraint/ => CheckConstraintViolation,
307
307
  /column does not allow nulls/ => NotNullConstraintViolation,
@@ -485,6 +485,11 @@ module Sequel
485
485
  sql << FROM
486
486
  source_list_append(sql, @opts[:from] || DUAL)
487
487
  end
488
+
489
+ # Oracle supports quoted function names.
490
+ def supports_quoted_function_names?
491
+ true
492
+ end
488
493
  end
489
494
  end
490
495
  end
@@ -1485,6 +1485,11 @@ module Sequel
1485
1485
  db.server_version(@opts[:server])
1486
1486
  end
1487
1487
 
1488
+ # PostgreSQL supports quoted function names.
1489
+ def supports_quoted_function_names?
1490
+ true
1491
+ end
1492
+
1488
1493
  # Concatenate the expressions with a space in between
1489
1494
  def full_text_string_join(cols)
1490
1495
  cols = Array(cols).map{|x| SQL::Function.new(:COALESCE, x, EMPTY_STRING)}
@@ -121,7 +121,7 @@ module Sequel
121
121
  private
122
122
 
123
123
  DATABASE_ERROR_REGEXPS = {
124
- /would not be unique/ => Sequel::UniqueConstraintViolation,
124
+ /would not be unique|Primary key for table.+is not unique/ => Sequel::UniqueConstraintViolation,
125
125
  /Column .* in table .* cannot be NULL/ => Sequel::NotNullConstraintViolation,
126
126
  /Constraint .* violated: Invalid value in table .*/ => Sequel::CheckConstraintViolation,
127
127
  /No primary key value for foreign key .* in table .*/ => Sequel::ForeignKeyConstraintViolation,
@@ -332,7 +332,7 @@ module Sequel
332
332
  end
333
333
 
334
334
  DATABASE_ERROR_REGEXPS = {
335
- /is not unique\z/ => UniqueConstraintViolation,
335
+ /(is|are) not unique\z/ => UniqueConstraintViolation,
336
336
  /foreign key constraint failed\z/ => ForeignKeyConstraintViolation,
337
337
  /\A(SQLITE ERROR 19 \(CONSTRAINT\) : )?constraint failed\z/ => ConstraintViolation,
338
338
  /may not be NULL\z/ => NotNullConstraintViolation,
@@ -692,6 +692,11 @@ module Sequel
692
692
  super unless @opts[:lock] == :update
693
693
  end
694
694
 
695
+ # SQLite supports quoted function names.
696
+ def supports_quoted_function_names?
697
+ true
698
+ end
699
+
695
700
  # SQLite treats a DELETE with no WHERE clause as a TRUNCATE
696
701
  def _truncate_sql(table)
697
702
  "DELETE FROM #{table}"
@@ -26,7 +26,7 @@ module Sequel
26
26
  sql = @opts[:append_sql] || ''
27
27
  subselect_sql_append(sql, unlimited.
28
28
  unordered.
29
- select_append{ROW_NUMBER(:over, :order=>order){}.as(rn)}.
29
+ select_append{ROW_NUMBER{}.over(:order=>order).as(rn)}.
30
30
  from_self(:alias=>dsa1).
31
31
  select(*columns).
32
32
  limit(@opts[:limit]).
@@ -44,6 +44,7 @@ module Sequel
44
44
  #
45
45
  # Options:
46
46
  # :ignore_errors :: Ignore any DatabaseErrors that are raised
47
+ # :name :: Name to use for index instead of default
47
48
  #
48
49
  # See <tt>alter_table</tt>.
49
50
  def add_index(table, columns, options=OPTS)
@@ -216,32 +216,17 @@ module Sequel
216
216
  SQL_BEGIN
217
217
  end
218
218
 
219
- if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
220
- # :nocov:
221
- # Whether to commit the current transaction. On ruby 1.8 and rubinius,
222
- # Thread.current.status is checked because Thread#kill skips rescue
223
- # blocks (so exception would be nil), but the transaction should
224
- # still be rolled back.
225
- def commit_or_rollback_transaction(exception, conn, opts)
226
- if exception
227
- false
228
- else
229
- if Thread.current.status == 'aborting'
230
- rollback_transaction(conn, opts)
231
- false
232
- else
233
- commit_transaction(conn, opts)
234
- true
235
- end
236
- end
237
- end
238
- # :nocov:
239
- else
240
- # Whether to commit the current transaction. On ruby 1.9 and JRuby,
241
- # transactions will be committed if Thread#kill is used on an thread
242
- # that has a transaction open, and there isn't a work around.
243
- def commit_or_rollback_transaction(exception, conn, opts)
244
- if exception
219
+ # Whether to commit the current transaction. Thread.current.status is
220
+ # checked because Thread#kill skips rescue blocks (so exception would be
221
+ # nil), but the transaction should still be rolled back. On Ruby 1.9 (but
222
+ # not 1.8 or 2.0), the thread status will still be "run", so Thread#kill
223
+ # will erroneously commit the transaction, and there isn't a workaround.
224
+ def commit_or_rollback_transaction(exception, conn, opts)
225
+ if exception
226
+ false
227
+ else
228
+ if Thread.current.status == 'aborting'
229
+ rollback_transaction(conn, opts)
245
230
  false
246
231
  else
247
232
  commit_transaction(conn, opts)
@@ -102,14 +102,14 @@ module Sequel
102
102
  if no_arg
103
103
  if block
104
104
  arg = Sequel.virtual_row(&block)
105
- aggregate_dataset.get{count(arg).as(count)}
105
+ aggregate_dataset.get{count(arg).as(:count)}
106
106
  else
107
- aggregate_dataset.get{count(:*){}.as(count)}.to_i
107
+ aggregate_dataset.get{count{}.*.as(:count)}.to_i
108
108
  end
109
109
  elsif block
110
110
  raise Error, 'cannot provide both argument and block to Dataset#count'
111
111
  else
112
- aggregate_dataset.get{count(arg).as(count)}
112
+ aggregate_dataset.get{count(arg).as(:count)}
113
113
  end
114
114
  end
115
115
 
@@ -167,6 +167,11 @@ module Sequel
167
167
  true
168
168
  end
169
169
 
170
+ # Whether the database supports quoting function names, false by default.
171
+ def supports_quoted_function_names?
172
+ false
173
+ end
174
+
170
175
  # Whether the RETURNING clause is used for the given dataset.
171
176
  # +type+ can be :insert, :update, or :delete.
172
177
  def uses_returning?(type)
@@ -743,7 +743,22 @@ module Sequel
743
743
 
744
744
  # Backbone of function_sql_append and emulated_function_sql_append.
745
745
  def _function_sql_append(sql, name, args)
746
- sql << name.to_s
746
+ case name
747
+ when SQL::Identifier
748
+ if supports_quoted_function_names?
749
+ literal_append(sql, name)
750
+ else
751
+ sql << name.value.to_s
752
+ end
753
+ when SQL::QualifiedIdentifier
754
+ if supports_quoted_function_names?
755
+ literal_append(sql, name)
756
+ else
757
+ sql << split_qualifiers(name).join(DOT)
758
+ end
759
+ else
760
+ sql << name.to_s
761
+ end
747
762
  if args.empty?
748
763
  sql << FUNCTION_EMPTY
749
764
  else
@@ -152,6 +152,12 @@ module Sequel
152
152
  {filter_by_associations_conditions_key=>ds}
153
153
  end
154
154
 
155
+ # Whether to handle silent modification failure when adding/removing
156
+ # associated records, false by default.
157
+ def handle_silent_modification_failure?
158
+ false
159
+ end
160
+
155
161
  # The limit and offset for this association (returned as a two element array).
156
162
  def limit_and_offset
157
163
  if (v = self[:limit]).is_a?(Array)
@@ -479,6 +485,11 @@ module Sequel
479
485
  :"#{underscore(demodulize(self[:model].name))}_id"
480
486
  end
481
487
 
488
+ # Handle silent failure of add/remove methods if raise_on_save_failure is false.
489
+ def handle_silent_modification_failure?
490
+ self[:raise_on_save_failure] == false
491
+ end
492
+
482
493
  # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
483
494
  def predicate_key
484
495
  cached_fetch(:predicate_key){qualify_assoc(self[:key])}
@@ -975,6 +986,8 @@ module Sequel
975
986
  # to the model method call, where :primary_key_column refers to the underlying column.
976
987
  # Should only be used if the model method differs from the primary key column, in
977
988
  # conjunction with defining a model alias method for the primary key column.
989
+ # :raise_on_save_failure :: Do not raise exceptions for hook or validation failures when saving associated
990
+ # objects in the add/remove methods (return nil instead) [one_to_many only].
978
991
  # === :many_to_many
979
992
  # :graph_join_table_block :: The block to pass to +join_table+ for
980
993
  # the join table when eagerly loading the association via +eager_graph+.
@@ -1116,7 +1129,7 @@ module Sequel
1116
1129
  def apply_window_function_eager_limit_strategy(ds, opts)
1117
1130
  rn = ds.row_number_column
1118
1131
  limit, offset = opts.limit_and_offset
1119
- ds = ds.unordered.select_append{row_number(:over, :partition=>opts.predicate_key, :order=>ds.opts[:order]){}.as(rn)}.from_self
1132
+ ds = ds.unordered.select_append{row_number{}.over(:partition=>opts.predicate_key, :order=>ds.opts[:order]).as(rn)}.from_self
1120
1133
  ds = if opts[:type] == :one_to_one
1121
1134
  ds.where(rn => offset ? offset+1 : 1)
1122
1135
  elsif offset
@@ -1426,7 +1439,7 @@ module Sequel
1426
1439
  cks.each{|k| ck_nil_hash[k] = nil}
1427
1440
 
1428
1441
  unless opts[:read_only]
1429
- validate = opts[:validate]
1442
+ save_opts = {:validate=>opts[:validate]}
1430
1443
 
1431
1444
  if one_to_one
1432
1445
  setter = opts[:setter] || proc do |o|
@@ -1437,21 +1450,23 @@ module Sequel
1437
1450
  end
1438
1451
  checked_transaction do
1439
1452
  up_ds.update(ck_nil_hash)
1440
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save") if o
1453
+ o.save(save_opts) || raise(Sequel::Error, "invalid associated object, cannot save") if o
1441
1454
  end
1442
1455
  end
1443
1456
  association_module_private_def(opts._setter_method, opts, &setter)
1444
1457
  association_module_def(opts.setter_method, opts){|o| set_one_to_one_associated_object(opts, o)}
1445
1458
  else
1459
+ save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false
1460
+
1446
1461
  adder = opts[:adder] || proc do |o|
1447
1462
  cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
1448
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
1463
+ o.save(save_opts)
1449
1464
  end
1450
1465
  association_module_private_def(opts._add_method, opts, &adder)
1451
1466
 
1452
1467
  remover = opts[:remover] || proc do |o|
1453
1468
  cks.each{|k| o.send(:"#{k}=", nil)}
1454
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
1469
+ o.save(save_opts)
1455
1470
  end
1456
1471
  association_module_private_def(opts._remove_method, opts, &remover)
1457
1472
 
@@ -1590,7 +1605,7 @@ module Sequel
1590
1605
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
1591
1606
  ensure_associated_primary_key(opts, o, *args)
1592
1607
  return if run_association_callbacks(opts, :before_add, o) == false
1593
- send(opts._add_method, o, *args)
1608
+ return if !send(opts._add_method, o, *args) && opts.handle_silent_modification_failure?
1594
1609
  if array = associations[opts[:name]] and !array.include?(o)
1595
1610
  array.push(o)
1596
1611
  end
@@ -1699,7 +1714,7 @@ module Sequel
1699
1714
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
1700
1715
  raise(Sequel::Error, "associated object #{o.inspect} does not have a primary key") if opts.need_associated_primary_key? && !o.pk
1701
1716
  return if run_association_callbacks(opts, :before_remove, o) == false
1702
- send(opts._remove_method, o, *args)
1717
+ return if !send(opts._remove_method, o, *args) && opts.handle_silent_modification_failure?
1703
1718
  associations[opts[:name]].delete_if{|x| o === x} if associations.include?(opts[:name])
1704
1719
  remove_reciprocal_object(opts, o)
1705
1720
  run_association_callbacks(opts, :after_remove, o)
@@ -41,8 +41,8 @@ module Sequel
41
41
  attr_reader :primary_key
42
42
 
43
43
  # Whether to raise an error instead of returning nil on a failure
44
- # to save/create/save_changes/etc due to a validation failure or
45
- # a before_* hook returning false.
44
+ # to save/create/save_changes/update/destroy due to a validation failure or
45
+ # a before_* hook returning false (default: true).
46
46
  attr_accessor :raise_on_save_failure
47
47
 
48
48
  # Whether to raise an error when unable to typecast data for a column
@@ -136,7 +136,11 @@ module Sequel
136
136
 
137
137
  validates_schema_types if model.auto_validate_types?
138
138
 
139
- model.auto_validate_unique_columns.each{|cols| validates_unique(cols)}
139
+ unique_opts = {}
140
+ if model.respond_to?(:sti_dataset)
141
+ unique_opts[:dataset] = model.sti_dataset
142
+ end
143
+ model.auto_validate_unique_columns.each{|cols| validates_unique(cols, unique_opts)}
140
144
  end
141
145
  end
142
146
  end
@@ -4,7 +4,7 @@ module Sequel
4
4
  # by passing a block to a _hook method (e.g. before_save_hook{do_something}).
5
5
  # The block is executed when the hook is called (e.g. before_save).
6
6
  #
7
- # All of the standard hooks are supported, except for after_initialize.
7
+ # All of the standard hooks are supported.
8
8
  # Instance level before hooks are executed in reverse order of addition before
9
9
  # calling super. Instance level after hooks are executed in order of addition
10
10
  # after calling super. If any of the instance level before hook blocks return
@@ -16,6 +16,8 @@ module Sequel
16
16
  # be run the first time you save the object (creating it), and the before_update
17
17
  # hook will be run the second time you save the object (updating it), and no
18
18
  # hooks will be run the third time you save the object.
19
+ #
20
+ # Validation hooks are not cleared until after a successful save.
19
21
  #
20
22
  # Usage:
21
23
  #
@@ -27,7 +29,7 @@ module Sequel
27
29
  module InstanceHooks
28
30
  module InstanceMethods
29
31
  BEFORE_HOOKS = Sequel::Model::BEFORE_HOOKS
30
- AFTER_HOOKS = Sequel::Model::AFTER_HOOKS - [:after_initialize]
32
+ AFTER_HOOKS = Sequel::Model::AFTER_HOOKS
31
33
  HOOKS = BEFORE_HOOKS + AFTER_HOOKS
32
34
  HOOKS.each{|h| class_eval(<<-END , __FILE__, __LINE__+1)}
33
35
  def #{h}_hook(&block)
@@ -38,7 +40,7 @@ module Sequel
38
40
  END
39
41
 
40
42
  BEFORE_HOOKS.each{|h| class_eval("def #{h}; run_before_instance_hooks(:#{h}) == false ? false : super end", __FILE__, __LINE__)}
41
- AFTER_HOOKS.each{|h| class_eval(<<-END, __FILE__, __LINE__ + 1)}
43
+ (AFTER_HOOKS - [:after_validation, :after_save]).each{|h| class_eval(<<-END, __FILE__, __LINE__ + 1)}
42
44
  def #{h}
43
45
  super
44
46
  run_after_instance_hooks(:#{h})
@@ -46,6 +48,22 @@ module Sequel
46
48
  @instance_hooks.delete(:#{h.to_s.sub('after', 'before')})
47
49
  end
48
50
  END
51
+
52
+ # Run after validation hooks, without clearing the validation hooks.
53
+ def after_validation
54
+ super
55
+ run_after_instance_hooks(:after_validation)
56
+ end
57
+
58
+ # Run after save hooks, clearing both the save and validation hooks.
59
+ def after_save
60
+ super
61
+ run_after_instance_hooks(:after_save)
62
+ @instance_hooks.delete(:after_save)
63
+ @instance_hooks.delete(:before_save)
64
+ @instance_hooks.delete(:after_validation)
65
+ @instance_hooks.delete(:before_validation)
66
+ end
49
67
 
50
68
  private
51
69
 
@@ -52,6 +52,8 @@ module Sequel
52
52
  # such as when this plugin needs to create an array type,
53
53
  # and typecasting is turned off or not setup correctly
54
54
  # for the model object.
55
+ # :raise_on_save_failure :: Do not raise exceptions for hook or validation failures when saving associated
56
+ # objects in the add/remove methods (return nil instead).
55
57
  # :save_after_modify :: For pg_array_to_many associations, this makes the
56
58
  # the modification methods save the current object,
57
59
  # so they operate more similarly to the one_to_many
@@ -93,6 +95,11 @@ module Sequel
93
95
  :"#{underscore(demodulize(self[:model].name))}_ids"
94
96
  end
95
97
 
98
+ # Handle silent failure of add/remove methods if raise_on_save_failure is false.
99
+ def handle_silent_modification_failure?
100
+ self[:raise_on_save_failure] == false
101
+ end
102
+
96
103
  # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
97
104
  def predicate_key
98
105
  cached_fetch(:predicate_key){qualify_assoc(self[:key_column])}
@@ -159,6 +166,12 @@ module Sequel
159
166
  :"#{singularize(self[:name])}_ids"
160
167
  end
161
168
 
169
+ # Handle silent failure of add/remove methods if raise_on_save_failure is false
170
+ # and save_after_modify is true.
171
+ def handle_silent_modification_failure?
172
+ self[:raise_on_save_failure] == false && self[:save_after_modify]
173
+ end
174
+
162
175
  # A qualified version of the associated primary key.
163
176
  def predicate_key
164
177
  cached_fetch(:predicate_key){qualify_assoc(primary_key)}
@@ -286,7 +299,8 @@ module Sequel
286
299
  def_association_dataset_methods(opts)
287
300
 
288
301
  unless opts[:read_only]
289
- validate = opts[:validate]
302
+ save_opts = {:validate=>opts[:validate]}
303
+ save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false
290
304
 
291
305
  array_type = opts[:array_type] ||= :integer
292
306
  adder = opts[:adder] || proc do |o|
@@ -295,14 +309,14 @@ module Sequel
295
309
  else
296
310
  o.send("#{key}=", Sequel.pg_array([send(pk)], array_type))
297
311
  end
298
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
312
+ o.save(save_opts)
299
313
  end
300
314
  association_module_private_def(opts._add_method, opts, &adder)
301
315
 
302
316
  remover = opts[:remover] || proc do |o|
303
317
  if (array = o.send(key)) && !array.empty?
304
318
  array.delete(send(pk))
305
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
319
+ o.save(save_opts)
306
320
  end
307
321
  end
308
322
  association_module_private_def(opts._remove_method, opts, &remover)
@@ -388,11 +402,13 @@ module Sequel
388
402
  def_association_dataset_methods(opts)
389
403
 
390
404
  unless opts[:read_only]
391
- validate = opts[:validate]
405
+ save_opts = {:validate=>opts[:validate]}
406
+ save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false
392
407
  array_type = opts[:array_type] ||= :integer
408
+
393
409
  if opts[:save_after_modify]
394
410
  save_after_modify = proc do |obj|
395
- obj.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
411
+ obj.save(save_opts)
396
412
  end
397
413
  end
398
414