sequel 5.18.0 → 5.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +40 -0
  3. data/doc/opening_databases.rdoc +5 -2
  4. data/doc/release_notes/5.19.0.txt +28 -0
  5. data/doc/release_notes/5.20.0.txt +89 -0
  6. data/doc/sharding.rdoc +12 -0
  7. data/doc/transactions.rdoc +38 -0
  8. data/lib/sequel/adapters/jdbc.rb +7 -2
  9. data/lib/sequel/adapters/mysql2.rb +2 -2
  10. data/lib/sequel/adapters/shared/postgres.rb +8 -8
  11. data/lib/sequel/adapters/shared/sqlite.rb +3 -1
  12. data/lib/sequel/adapters/sqlanywhere.rb +33 -17
  13. data/lib/sequel/adapters/sqlite.rb +20 -13
  14. data/lib/sequel/connection_pool.rb +0 -5
  15. data/lib/sequel/database/misc.rb +10 -9
  16. data/lib/sequel/database/query.rb +1 -1
  17. data/lib/sequel/database/schema_generator.rb +1 -1
  18. data/lib/sequel/database/transactions.rb +57 -5
  19. data/lib/sequel/dataset/actions.rb +6 -5
  20. data/lib/sequel/dataset/graph.rb +2 -2
  21. data/lib/sequel/dataset/placeholder_literalizer.rb +4 -1
  22. data/lib/sequel/dataset/prepared_statements.rb +1 -1
  23. data/lib/sequel/dataset/query.rb +1 -1
  24. data/lib/sequel/extensions/constraint_validations.rb +14 -0
  25. data/lib/sequel/extensions/pg_enum.rb +23 -15
  26. data/lib/sequel/extensions/schema_dumper.rb +1 -1
  27. data/lib/sequel/model/associations.rb +38 -12
  28. data/lib/sequel/model/base.rb +1 -1
  29. data/lib/sequel/model/plugins.rb +104 -0
  30. data/lib/sequel/plugins/association_dependencies.rb +3 -3
  31. data/lib/sequel/plugins/association_pks.rb +14 -4
  32. data/lib/sequel/plugins/class_table_inheritance.rb +1 -0
  33. data/lib/sequel/plugins/composition.rb +13 -9
  34. data/lib/sequel/plugins/finder.rb +2 -2
  35. data/lib/sequel/plugins/hook_class_methods.rb +17 -5
  36. data/lib/sequel/plugins/inverted_subsets.rb +2 -2
  37. data/lib/sequel/plugins/json_serializer.rb +3 -3
  38. data/lib/sequel/plugins/nested_attributes.rb +1 -1
  39. data/lib/sequel/plugins/pg_array_associations.rb +8 -4
  40. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +61 -32
  41. data/lib/sequel/plugins/prepared_statements.rb +1 -1
  42. data/lib/sequel/plugins/prepared_statements_safe.rb +1 -1
  43. data/lib/sequel/plugins/subset_conditions.rb +2 -2
  44. data/lib/sequel/plugins/validation_class_methods.rb +5 -3
  45. data/lib/sequel/plugins/validation_helpers.rb +2 -2
  46. data/lib/sequel/sql.rb +1 -1
  47. data/lib/sequel/version.rb +1 -1
  48. data/spec/adapters/postgres_spec.rb +40 -0
  49. data/spec/core/database_spec.rb +73 -2
  50. data/spec/core/schema_spec.rb +7 -1
  51. data/spec/extensions/class_table_inheritance_spec.rb +30 -8
  52. data/spec/extensions/constraint_validations_spec.rb +20 -2
  53. data/spec/extensions/core_refinements_spec.rb +1 -1
  54. data/spec/extensions/hook_class_methods_spec.rb +22 -0
  55. data/spec/extensions/migration_spec.rb +13 -0
  56. data/spec/extensions/pg_auto_constraint_validations_spec.rb +8 -0
  57. data/spec/extensions/pg_enum_spec.rb +5 -0
  58. data/spec/extensions/s_spec.rb +1 -1
  59. data/spec/extensions/schema_dumper_spec.rb +4 -2
  60. data/spec/integration/plugin_test.rb +15 -0
  61. data/spec/integration/transaction_test.rb +50 -0
  62. data/spec/model/associations_spec.rb +84 -4
  63. data/spec/model/plugins_spec.rb +111 -0
  64. metadata +7 -3
@@ -90,11 +90,6 @@ class Sequel::ConnectionPool
90
90
  # connection object (and server argument if the callable accepts 2 arguments),
91
91
  # useful for customizations that you want to apply to all connections.
92
92
  # :connect_sqls :: An array of sql strings to execute on each new connection, after :after_connect runs.
93
- # :preconnect :: Automatically create the maximum number of connections, so that they don't
94
- # need to be created as needed. This is useful when connecting takes a long time
95
- # and you want to avoid possible latency during runtime.
96
- # Set to :concurrently to create the connections in separate threads. Otherwise
97
- # they'll be created sequentially.
98
93
  def initialize(db, opts=OPTS)
99
94
  @db = db
100
95
  @after_connect = opts[:after_connect]
@@ -106,8 +106,11 @@ module Sequel
106
106
  # :log_connection_info :: Whether connection information should be logged when logging queries.
107
107
  # :log_warn_duration :: The number of elapsed seconds after which queries should be logged at warn level.
108
108
  # :name :: A name to use for the Database object, displayed in PoolTimeout .
109
- # :preconnect :: Whether to setup the maximum number of connections during initialization.
110
- # Can use a value of 'concurrently' to preconnect in separate threads.
109
+ # :preconnect :: Automatically create the maximum number of connections, so that they don't
110
+ # need to be created as needed. This is useful when connecting takes a long time
111
+ # and you want to avoid possible latency during runtime.
112
+ # Set to :concurrently to create the connections in separate threads. Otherwise
113
+ # they'll be created sequentially.
111
114
  # :preconnect_extensions :: Similar to the :extensions option, but loads the extensions before the
112
115
  # connections are made by the :preconnect option.
113
116
  # :quote_identifiers :: Whether to quote identifiers.
@@ -115,7 +118,9 @@ module Sequel
115
118
  # :single_threaded :: Whether to use a single-threaded connection pool.
116
119
  # :sql_log_level :: Method to use to log SQL to a logger, :info by default.
117
120
  #
118
- # All options given are also passed to the connection pool.
121
+ # All options given are also passed to the connection pool. Additional options respected by
122
+ # the connection pool are :after_connect, :connect_sqls, :max_connections, :pool_timeout,
123
+ # :servers, and :servers_hash. See the connection pool documentation for details.
119
124
  def initialize(opts = OPTS)
120
125
  @opts ||= opts
121
126
  @opts = connection_pool_default_options.merge(@opts)
@@ -473,9 +478,7 @@ module Sequel
473
478
 
474
479
  if RUBY_VERSION >= '2.4'
475
480
  # Typecast a string to a BigDecimal
476
- def _typecast_value_string_to_decimal(value)
477
- BigDecimal(value)
478
- end
481
+ alias _typecast_value_string_to_decimal BigDecimal
479
482
  else
480
483
  # :nocov:
481
484
  def _typecast_value_string_to_decimal(value)
@@ -510,9 +513,7 @@ module Sequel
510
513
  end
511
514
 
512
515
  # Typecast the value to a Float
513
- def typecast_value_float(value)
514
- Float(value)
515
- end
516
+ alias typecast_value_float Float
516
517
 
517
518
  # Typecast the value to an Integer
518
519
  def typecast_value_integer(value)
@@ -331,7 +331,7 @@ module Sequel
331
331
  :time
332
332
  when /\A(bool(ean)?)\z/io
333
333
  :boolean
334
- when /\A(real|float|double( precision)?|double\(\d+,\d+\)( unsigned)?)\z/io
334
+ when /\A(real|float( unsigned)?|double( precision)?|double\(\d+,\d+\)( unsigned)?)\z/io
335
335
  :float
336
336
  when /\A(?:(?:(?:num(?:ber|eric)?|decimal)(?:\(\d+,\s*(\d+|false|true)\))?))\z/io
337
337
  $1 && ['0', 'false'].include?($1) ? :integer : :decimal
@@ -634,7 +634,7 @@ module Sequel
634
634
 
635
635
  # Drop a composite foreign key constraint
636
636
  def drop_composite_foreign_key(columns, opts)
637
- @operations << {:op => :drop_constraint, :type => :foreign_key, :columns => columns}.merge!(opts)
637
+ @operations << opts.merge(:op => :drop_constraint, :type => :foreign_key, :columns => columns)
638
638
  nil
639
639
  end
640
640
  end
@@ -25,13 +25,19 @@ module Sequel
25
25
  # Otherwise, add the block to the list of blocks to call after the currently
26
26
  # in progress transaction commits (and only if it commits).
27
27
  # Options:
28
+ # :savepoint :: If currently inside a savepoint, only run this hook on transaction
29
+ # commit if all enclosing savepoints have been released.
28
30
  # :server :: The server/shard to use.
29
31
  def after_commit(opts=OPTS, &block)
30
32
  raise Error, "must provide block to after_commit" unless block
31
33
  synchronize(opts[:server]) do |conn|
32
34
  if h = _trans(conn)
33
35
  raise Error, "cannot call after_commit in a prepared transaction" if h[:prepare]
34
- add_transaction_hook(conn, :after_commit, block)
36
+ if opts[:savepoint] && in_savepoint?(conn)
37
+ add_savepoint_hook(conn, :after_commit, block)
38
+ else
39
+ add_transaction_hook(conn, :after_commit, block)
40
+ end
35
41
  else
36
42
  yield
37
43
  end
@@ -42,13 +48,20 @@ module Sequel
42
48
  # Otherwise, add the block to the list of the blocks to call after the currently
43
49
  # in progress transaction rolls back (and only if it rolls back).
44
50
  # Options:
51
+ # :savepoint :: If currently inside a savepoint, run this hook immediately when
52
+ # any enclosing savepoint is rolled back, which may be before the transaction
53
+ # commits or rollsback.
45
54
  # :server :: The server/shard to use.
46
55
  def after_rollback(opts=OPTS, &block)
47
56
  raise Error, "must provide block to after_rollback" unless block
48
57
  synchronize(opts[:server]) do |conn|
49
58
  if h = _trans(conn)
50
59
  raise Error, "cannot call after_rollback in a prepared transaction" if h[:prepare]
51
- add_transaction_hook(conn, :after_rollback, block)
60
+ if opts[:savepoint] && in_savepoint?(conn)
61
+ add_savepoint_hook(conn, :after_rollback, block)
62
+ else
63
+ add_transaction_hook(conn, :after_rollback, block)
64
+ end
52
65
  end
53
66
  end
54
67
  end
@@ -298,6 +311,13 @@ module Sequel
298
311
  Sequel.synchronize{@transactions[conn] = hash}
299
312
  end
300
313
 
314
+ # Set the given callable as a hook to be called. Type should be either
315
+ # :after_commit or :after_rollback.
316
+ def add_savepoint_hook(conn, type, block)
317
+ savepoint = _trans(conn)[:savepoints].last
318
+ (savepoint[type] ||= []) << block
319
+ end
320
+
301
321
  # Set the given callable as a hook to be called. Type should be either
302
322
  # :after_commit or :after_rollback.
303
323
  def add_transaction_hook(conn, type, block)
@@ -401,6 +421,14 @@ module Sequel
401
421
  supports_savepoints? && savepoint_level(conn) > 1
402
422
  end
403
423
 
424
+ # Retrieve the savepoint hooks that should be run for the given
425
+ # connection and commit status.
426
+ def savepoint_hooks(conn, committed)
427
+ if in_savepoint?(conn)
428
+ _trans(conn)[:savepoints].last[committed ? :after_commit : :after_rollback]
429
+ end
430
+ end
431
+
404
432
  # Retrieve the transaction hooks that should be run for the given
405
433
  # connection and commit status.
406
434
  def transaction_hooks(conn, committed)
@@ -411,16 +439,40 @@ module Sequel
411
439
 
412
440
  # Remove the current thread from the list of active transactions
413
441
  def remove_transaction(conn, committed)
414
- callbacks = transaction_hooks(conn, committed)
442
+ if in_savepoint?(conn)
443
+ savepoint_callbacks = savepoint_hooks(conn, committed)
444
+ if committed
445
+ savepoint_rollback_callbacks = savepoint_hooks(conn, false)
446
+ end
447
+ else
448
+ callbacks = transaction_hooks(conn, committed)
449
+ end
415
450
 
416
451
  if transaction_finished?(conn)
417
452
  h = _trans(conn)
418
453
  rolled_back = !committed
419
454
  Sequel.synchronize{h[:rolled_back] = rolled_back}
420
455
  Sequel.synchronize{@transactions.delete(conn)}
456
+ callbacks.each(&:call) if callbacks
457
+ elsif savepoint_callbacks || savepoint_rollback_callbacks
458
+ if committed
459
+ meth = in_savepoint?(conn) ? :add_savepoint_hook : :add_transaction_hook
460
+
461
+ if savepoint_callbacks
462
+ savepoint_callbacks.each do |block|
463
+ send(meth, conn, :after_commit, block)
464
+ end
465
+ end
466
+
467
+ if savepoint_rollback_callbacks
468
+ savepoint_rollback_callbacks.each do |block|
469
+ send(meth, conn, :after_rollback, block)
470
+ end
471
+ end
472
+ else
473
+ savepoint_callbacks.each(&:call)
474
+ end
421
475
  end
422
-
423
- callbacks.each(&:call) if callbacks
424
476
  end
425
477
 
426
478
  # SQL to rollback to a savepoint
@@ -18,7 +18,7 @@ module Sequel
18
18
  where_all where_each where_single_value
19
19
  METHS
20
20
 
21
- # The clone options to use when retriveing columns for a dataset.
21
+ # The clone options to use when retrieving columns for a dataset.
22
22
  COLUMNS_CLONE_OPTIONS = {:distinct => nil, :limit => 1, :offset=>nil, :where=>nil, :having=>nil, :order=>nil, :row_proc=>nil, :graph=>nil, :eager_graph=>nil}.freeze
23
23
 
24
24
  # Inserts the given argument into the database. Returns self so it
@@ -358,7 +358,7 @@ module Sequel
358
358
 
359
359
  # Inserts values into the associated table. The returned value is generally
360
360
  # the value of the autoincremented primary key for the inserted row, assuming that
361
- # the a single row is inserted and the table has an autoincrementing primary key.
361
+ # a single row is inserted and the table has an autoincrementing primary key.
362
362
  #
363
363
  # +insert+ handles a number of different argument formats:
364
364
  # no arguments or single empty hash :: Uses DEFAULT VALUES
@@ -486,7 +486,7 @@ module Sequel
486
486
  import(columns, hashes.map{|h| columns.map{|c| h[c]}}, opts)
487
487
  end
488
488
 
489
- # Yields each row in the dataset, but interally uses multiple queries as needed to
489
+ # Yields each row in the dataset, but internally uses multiple queries as needed to
490
490
  # process the entire result set without keeping all rows in the dataset in memory,
491
491
  # even if the underlying driver buffers all query results in memory.
492
492
  #
@@ -512,7 +512,7 @@ module Sequel
512
512
  # NULLs. Note that some Sequel adapters have optimized implementations that will
513
513
  # use cursors or streaming regardless of the :strategy option used.
514
514
  # :filter_values :: If the strategy: :filter option is used, this option should be a proc
515
- # that accepts the last retreived row for the previous page and an array of
515
+ # that accepts the last retrieved row for the previous page and an array of
516
516
  # ORDER BY expressions, and returns an array of values relating to those
517
517
  # expressions for the last retrieved row. You will need to use this option
518
518
  # if your ORDER BY expressions are not simple columns, if they contain
@@ -971,7 +971,8 @@ module Sequel
971
971
  # separate insert commands for each row. Otherwise, call #multi_insert_sql
972
972
  # and execute each statement it gives separately.
973
973
  def _import(columns, values, opts)
974
- trans_opts = Hash[opts].merge!(:server=>@opts[:server])
974
+ trans_opts = Hash[opts]
975
+ trans_opts[:server] = @opts[:server]
975
976
  if opts[:return] == :primary_key
976
977
  @db.transaction(trans_opts){values.map{|v| insert(columns, v)}}
977
978
  else
@@ -21,7 +21,7 @@ module Sequel
21
21
  raise Error, "cannot call add_graph_aliases on a dataset that has not been called with graph or set_graph_aliases"
22
22
  end
23
23
  columns, graph_aliases = graph_alias_columns(graph_aliases)
24
- select_append(*columns).clone(:graph => Hash[graph].merge!(:column_aliases=>Hash[ga].merge!(graph_aliases).freeze).freeze)
24
+ select_append(*columns).clone(:graph => graph.merge(:column_aliases=>ga.merge(graph_aliases).freeze).freeze)
25
25
  end
26
26
 
27
27
  # Similar to Dataset#join_table, but uses unambiguous aliases for selected
@@ -244,7 +244,7 @@ module Sequel
244
244
  def set_graph_aliases(graph_aliases)
245
245
  columns, graph_aliases = graph_alias_columns(graph_aliases)
246
246
  if graph = opts[:graph]
247
- select(*columns).clone(:graph => Hash[graph].merge!(:column_aliases=>graph_aliases.freeze).freeze)
247
+ select(*columns).clone(:graph => graph.merge(:column_aliases=>graph_aliases.freeze).freeze)
248
248
  else
249
249
  raise Error, "cannot call #set_graph_aliases on an ungraphed dataset"
250
250
  end
@@ -170,7 +170,10 @@ module Sequel
170
170
  # receiver's dataset to the block, and the block should return the new dataset
171
171
  # to use.
172
172
  def with_dataset
173
- dup.instance_exec{@dataset = yield @dataset; self}.freeze
173
+ dataset = yield @dataset
174
+ other = dup
175
+ other.instance_variable_set(:@dataset, dataset)
176
+ other.freeze
174
177
  end
175
178
 
176
179
  # Return an array of all objects by running the SQL query for the given arguments.
@@ -329,7 +329,7 @@ module Sequel
329
329
  # # => {:id=>1}
330
330
  def bind(bind_vars=OPTS)
331
331
  bind_vars = if bv = @opts[:bind_vars]
332
- Hash[bv].merge!(bind_vars).freeze
332
+ bv.merge(bind_vars).freeze
333
333
  else
334
334
  if bind_vars.frozen?
335
335
  bind_vars
@@ -1091,7 +1091,7 @@ module Sequel
1091
1091
  # # SELECT i1.id, i1.parent_id FROM i1 INNER JOIN t ON (t.id = i1.parent_id)
1092
1092
  # # ) SELECT * FROM t
1093
1093
  def with_recursive(name, nonrecursive, recursive, opts=OPTS)
1094
- raise(Error, 'This datatset does not support common table expressions') unless supports_cte?
1094
+ raise(Error, 'This dataset does not support common table expressions') unless supports_cte?
1095
1095
  if hoist_cte?(nonrecursive)
1096
1096
  s, ds = hoist_cte(nonrecursive)
1097
1097
  s.with_recursive(name, ds, recursive, opts)
@@ -130,6 +130,10 @@
130
130
  # readd all constraints you want to use inside the alter table block,
131
131
  # making no other changes inside the alter_table block.
132
132
  #
133
+ # Dropping a table will automatically delete all constraint validations for
134
+ # that table. However, altering a table (e.g. to drop a column) will not
135
+ # currently make any changes to the constraint validations metadata.
136
+ #
133
137
  # Related module: Sequel::ConstraintValidations
134
138
 
135
139
  #
@@ -264,6 +268,16 @@ module Sequel
264
268
  end
265
269
  end
266
270
 
271
+ # Drop all constraint validations for a table if dropping the table.
272
+ def drop_table(*names)
273
+ names.each do |name|
274
+ if !name.is_a?(Hash) && table_exists?(constraint_validations_table)
275
+ drop_constraint_validations_for(:table=>name)
276
+ end
277
+ end
278
+ super
279
+ end
280
+
267
281
  # Drop the constraint validations table.
268
282
  def drop_constraint_validations_table
269
283
  drop_table(constraint_validations_table)
@@ -17,6 +17,12 @@
17
17
  #
18
18
  # DB.rename_enum(:enum_type_name, :enum_type_another_name)
19
19
  #
20
+ # If you want to rename an enum value, you can use rename_enum_value:
21
+ #
22
+ # DB.rename_enum_value(
23
+ # :enum_type_name, :enum_value_name, :enum_value_another_name
24
+ # )
25
+ #
20
26
  # If you want to drop an enum type, you can use drop_enum:
21
27
  #
22
28
  # DB.drop_enum(:enum_type_name)
@@ -86,26 +92,24 @@ module Sequel
86
92
  elsif v = opts[:after]
87
93
  sql << " AFTER #{literal(v.to_s)}"
88
94
  end
89
- run sql
90
- parse_enum_labels
91
- nil
95
+ _process_enum_change_sql(sql)
92
96
  end
93
97
 
94
98
  # Run the SQL to create an enum type with the given name and values.
95
99
  def create_enum(enum, values)
96
- sql = "CREATE TYPE #{quote_schema_table(enum)} AS ENUM (#{values.map{|v| literal(v.to_s)}.join(', ')})"
97
- run sql
98
- parse_enum_labels
99
- nil
100
+ _process_enum_change_sql("CREATE TYPE #{quote_schema_table(enum)} AS ENUM (#{values.map{|v| literal(v.to_s)}.join(', ')})")
100
101
  end
101
102
 
102
103
  # Run the SQL to rename the enum type with the given name
103
104
  # to the another given name.
104
105
  def rename_enum(enum, new_name)
105
- sql = "ALTER TYPE #{quote_schema_table(enum)} RENAME TO #{quote_schema_table(new_name)}"
106
- run sql
107
- parse_enum_labels
108
- nil
106
+ _process_enum_change_sql("ALTER TYPE #{quote_schema_table(enum)} RENAME TO #{quote_schema_table(new_name)}")
107
+ end
108
+
109
+ # Run the SQL to rename the enum value with the given name
110
+ # to the another given name.
111
+ def rename_enum_value(enum, old_name, new_name)
112
+ _process_enum_change_sql("ALTER TYPE #{quote_schema_table(enum)} RENAME VALUE #{literal(old_name.to_s)} TO #{literal(new_name.to_s)}")
109
113
  end
110
114
 
111
115
  # Run the SQL to drop the enum type with the given name.
@@ -113,14 +117,18 @@ module Sequel
113
117
  # :if_exists :: Do not raise an error if the enum type does not exist
114
118
  # :cascade :: Also drop other objects that depend on the enum type
115
119
  def drop_enum(enum, opts=OPTS)
116
- sql = "DROP TYPE#{' IF EXISTS' if opts[:if_exists]} #{quote_schema_table(enum)}#{' CASCADE' if opts[:cascade]}"
117
- run sql
118
- parse_enum_labels
119
- nil
120
+ _process_enum_change_sql("DROP TYPE#{' IF EXISTS' if opts[:if_exists]} #{quote_schema_table(enum)}#{' CASCADE' if opts[:cascade]}")
120
121
  end
121
122
 
122
123
  private
123
124
 
125
+ # Run the SQL on the database, reparsing the enum labels after it is run.
126
+ def _process_enum_change_sql(sql)
127
+ run(sql)
128
+ parse_enum_labels
129
+ nil
130
+ end
131
+
124
132
  # Parse the pg_enum table to get enum values, and
125
133
  # the pg_type table to get names and array oids for
126
134
  # enums.
@@ -37,7 +37,7 @@ module Sequel
37
37
  {:type =>schema[:type] == :boolean ? TrueClass : Integer}
38
38
  when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/
39
39
  {:type=>:Bignum}
40
- when /\A(?:real|float|double(?: precision)?|double\(\d+,\d+\)(?: unsigned)?)\z/
40
+ when /\A(?:real|float(?: unsigned)?|double(?: precision)?|double\(\d+,\d+\)(?: unsigned)?)\z/
41
41
  {:type=>Float}
42
42
  when 'boolean', 'bit', 'bool'
43
43
  {:type=>TrueClass}
@@ -1617,9 +1617,10 @@ module Sequel
1617
1617
  # is hash or array of two element arrays. Consider also specifying the :graph_block
1618
1618
  # option if the value for this option is not a hash or array of two element arrays
1619
1619
  # and you plan to use this association in eager_graph or association_join.
1620
- # :dataset :: A proc that is instance_execed to get the base dataset to use (before the other
1620
+ # :dataset :: A proc that is used to define the method to get the base dataset to use (before the other
1621
1621
  # options are applied). If the proc accepts an argument, it is passed the related
1622
- # association reflection.
1622
+ # association reflection. It is a best practice to always have the dataset accept an argument
1623
+ # and use the argument to return the appropriate dataset.
1623
1624
  # :distinct :: Use the DISTINCT clause when selecting associating object, both when
1624
1625
  # lazy loading and eager loading via .eager (but not when using .eager_graph).
1625
1626
  # :eager :: The associations to eagerly load via +eager+ when loading the associated object(s).
@@ -1909,7 +1910,7 @@ module Sequel
1909
1910
  # can be easily overridden in the class itself while allowing for
1910
1911
  # super to be called.
1911
1912
  def association_module_def(name, opts=OPTS, &block)
1912
- association_module(opts).module_eval{define_method(name, &block)}
1913
+ association_module(opts).send(:define_method, name, &block)
1913
1914
  end
1914
1915
 
1915
1916
  # Add a private method to the module included in the class.
@@ -1944,6 +1945,13 @@ module Sequel
1944
1945
  end
1945
1946
 
1946
1947
  association_module_def(opts.dataset_method, opts){_dataset(opts)}
1948
+ if opts[:block]
1949
+ opts[:block_method] = Plugins.def_sequel_method(association_module(opts), "#{opts[:name]}_block", 1, &opts[:block])
1950
+ end
1951
+ if opts[:dataset]
1952
+ opts[:dataset_opt_arity] = opts[:dataset].arity == 0 ? 0 : 1
1953
+ opts[:dataset_opt_method] = Plugins.def_sequel_method(association_module(opts), "#{opts[:name]}_dataset_opt", opts[:dataset_opt_arity], &opts[:dataset])
1954
+ end
1947
1955
  def_association_method(opts)
1948
1956
 
1949
1957
  return if opts[:read_only]
@@ -2129,7 +2137,7 @@ module Sequel
2129
2137
  graph_cks = opts[:graph_keys]
2130
2138
  opts[:eager_grapher] ||= proc do |eo|
2131
2139
  ds = eo[:self]
2132
- ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, Hash[eo].merge!(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep, :from_self_alias=>eo[:from_self_alias]), &graph_block)
2140
+ ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
2133
2141
  end
2134
2142
 
2135
2143
  return if opts[:read_only]
@@ -2189,7 +2197,7 @@ module Sequel
2189
2197
  graph_block = opts[:graph_block]
2190
2198
  opts[:eager_grapher] ||= proc do |eo|
2191
2199
  ds = eo[:self]
2192
- ds = ds.graph(opts.apply_eager_graph_limit_strategy(eo[:limit_strategy], eager_graph_dataset(opts, eo)), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, Hash[eo].merge!(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep, :from_self_alias=>eo[:from_self_alias]), &graph_block)
2200
+ ds = ds.graph(opts.apply_eager_graph_limit_strategy(eo[:limit_strategy], eager_graph_dataset(opts, eo)), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
2193
2201
  # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
2194
2202
  ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal
2195
2203
  ds
@@ -2204,12 +2212,28 @@ module Sequel
2204
2212
  if one_to_one
2205
2213
  opts[:setter] ||= proc do |o|
2206
2214
  up_ds = _apply_association_options(opts, opts.associated_dataset.where(cks.zip(cpks.map{|k| get_column_value(k)})))
2215
+
2216
+ if (froms = up_ds.opts[:from]) && (from = froms[0]) && (from.is_a?(Sequel::Dataset) || (from.is_a?(Sequel::SQL::AliasedExpression) && from.expression.is_a?(Sequel::Dataset)))
2217
+ if old = up_ds.first
2218
+ cks.each{|k| old.set_column_value(:"#{k}=", nil)}
2219
+ end
2220
+ save_old = true
2221
+ end
2222
+
2207
2223
  if o
2208
- up_ds = up_ds.exclude(o.pk_hash) unless o.new?
2224
+ if !o.new? && !save_old
2225
+ up_ds = up_ds.exclude(o.pk_hash)
2226
+ end
2209
2227
  cks.zip(cpks).each{|k, pk| o.set_column_value(:"#{k}=", get_column_value(pk))}
2210
2228
  end
2229
+
2211
2230
  checked_transaction do
2212
- up_ds.skip_limit_check.update(ck_nil_hash)
2231
+ if save_old
2232
+ old.save(save_opts) || raise(Sequel::Error, "invalid previously associated object, cannot save") if old
2233
+ else
2234
+ up_ds.skip_limit_check.update(ck_nil_hash)
2235
+ end
2236
+
2213
2237
  o.save(save_opts) || raise(Sequel::Error, "invalid associated object, cannot save") if o
2214
2238
  end
2215
2239
  end
@@ -2287,7 +2311,8 @@ module Sequel
2287
2311
  end
2288
2312
  ds = ds.clone(:model_object => self)
2289
2313
  ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] && opts.eager_graph_lazy_dataset?
2290
- ds = instance_exec(ds, &opts[:block]) if opts[:block]
2314
+ # block method is private
2315
+ ds = send(opts[:block_method], ds) if opts[:block_method]
2291
2316
  ds
2292
2317
  end
2293
2318
 
@@ -2310,10 +2335,11 @@ module Sequel
2310
2335
  # Return an association dataset for the given association reflection
2311
2336
  def _dataset(opts)
2312
2337
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
2313
- ds = if opts[:dataset].arity == 1
2314
- instance_exec(opts, &opts[:dataset])
2338
+ ds = if opts[:dataset_opt_arity] == 1
2339
+ # dataset_opt_method is private
2340
+ send(opts[:dataset_opt_method], opts)
2315
2341
  else
2316
- instance_exec(&opts[:dataset])
2342
+ send(opts[:dataset_opt_method])
2317
2343
  end
2318
2344
  _apply_association_options(opts, ds)
2319
2345
  end
@@ -2908,7 +2934,7 @@ module Sequel
2908
2934
  def eager(*associations)
2909
2935
  opts = @opts[:eager]
2910
2936
  association_opts = eager_options_for_associations(associations)
2911
- opts = opts ? Hash[opts].merge!(association_opts) : association_opts
2937
+ opts = opts ? opts.merge(association_opts) : association_opts
2912
2938
  clone(:eager=>opts.freeze)
2913
2939
  end
2914
2940