sequel 5.18.0 → 5.20.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 (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