sequel 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. data/CHANGELOG +56 -0
  2. data/README.rdoc +1 -1
  3. data/Rakefile +1 -1
  4. data/doc/advanced_associations.rdoc +7 -10
  5. data/doc/release_notes/3.10.0.txt +286 -0
  6. data/lib/sequel/adapters/do/mysql.rb +4 -0
  7. data/lib/sequel/adapters/jdbc.rb +5 -0
  8. data/lib/sequel/adapters/jdbc/as400.rb +58 -0
  9. data/lib/sequel/adapters/jdbc/oracle.rb +30 -0
  10. data/lib/sequel/adapters/shared/mssql.rb +23 -9
  11. data/lib/sequel/adapters/shared/mysql.rb +12 -1
  12. data/lib/sequel/adapters/shared/postgres.rb +7 -18
  13. data/lib/sequel/adapters/shared/sqlite.rb +5 -0
  14. data/lib/sequel/adapters/sqlite.rb +5 -0
  15. data/lib/sequel/connection_pool/single.rb +3 -3
  16. data/lib/sequel/database.rb +3 -2
  17. data/lib/sequel/dataset.rb +6 -5
  18. data/lib/sequel/dataset/convenience.rb +3 -3
  19. data/lib/sequel/dataset/query.rb +13 -0
  20. data/lib/sequel/dataset/sql.rb +31 -1
  21. data/lib/sequel/extensions/schema_dumper.rb +3 -3
  22. data/lib/sequel/model.rb +8 -6
  23. data/lib/sequel/model/associations.rb +144 -102
  24. data/lib/sequel/model/base.rb +21 -1
  25. data/lib/sequel/model/plugins.rb +3 -1
  26. data/lib/sequel/plugins/association_dependencies.rb +14 -7
  27. data/lib/sequel/plugins/caching.rb +4 -0
  28. data/lib/sequel/plugins/composition.rb +138 -0
  29. data/lib/sequel/plugins/identity_map.rb +2 -2
  30. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  31. data/lib/sequel/plugins/nested_attributes.rb +3 -2
  32. data/lib/sequel/plugins/rcte_tree.rb +281 -0
  33. data/lib/sequel/plugins/typecast_on_load.rb +16 -5
  34. data/lib/sequel/sql.rb +18 -1
  35. data/lib/sequel/version.rb +1 -1
  36. data/spec/adapters/mssql_spec.rb +4 -0
  37. data/spec/adapters/mysql_spec.rb +4 -0
  38. data/spec/adapters/postgres_spec.rb +55 -5
  39. data/spec/core/database_spec.rb +5 -3
  40. data/spec/core/dataset_spec.rb +86 -15
  41. data/spec/core/expression_filters_spec.rb +23 -6
  42. data/spec/extensions/association_dependencies_spec.rb +24 -5
  43. data/spec/extensions/association_proxies_spec.rb +3 -0
  44. data/spec/extensions/composition_spec.rb +194 -0
  45. data/spec/extensions/identity_map_spec.rb +16 -0
  46. data/spec/extensions/nested_attributes_spec.rb +44 -1
  47. data/spec/extensions/rcte_tree_spec.rb +205 -0
  48. data/spec/extensions/schema_dumper_spec.rb +6 -0
  49. data/spec/extensions/spec_helper.rb +6 -0
  50. data/spec/extensions/typecast_on_load_spec.rb +9 -0
  51. data/spec/extensions/validation_helpers_spec.rb +5 -5
  52. data/spec/integration/dataset_test.rb +13 -9
  53. data/spec/integration/eager_loader_test.rb +56 -1
  54. data/spec/integration/model_test.rb +8 -0
  55. data/spec/integration/plugin_test.rb +270 -0
  56. data/spec/integration/schema_test.rb +1 -1
  57. data/spec/model/associations_spec.rb +541 -118
  58. data/spec/model/eager_loading_spec.rb +24 -3
  59. data/spec/model/record_spec.rb +34 -0
  60. metadata +9 -2
@@ -7,11 +7,41 @@ module Sequel
7
7
  # Instance methods for Oracle Database objects accessed via JDBC.
8
8
  module DatabaseMethods
9
9
  include Sequel::Oracle::DatabaseMethods
10
+ TRANSACTION_BEGIN = 'Transaction.begin'.freeze
11
+ TRANSACTION_COMMIT = 'Transaction.commit'.freeze
12
+ TRANSACTION_ROLLBACK = 'Transaction.rollback'.freeze
10
13
 
11
14
  # Return Sequel::JDBC::Oracle::Dataset object with the given opts.
12
15
  def dataset(opts=nil)
13
16
  Sequel::JDBC::Oracle::Dataset.new(self, opts)
14
17
  end
18
+
19
+ private
20
+
21
+ # Use JDBC connection's setAutoCommit to false to start transactions
22
+ def begin_transaction(conn)
23
+ log_info(TRANSACTION_BEGIN)
24
+ conn.setAutoCommit(false)
25
+ conn
26
+ end
27
+
28
+ # Use JDBC connection's commit method to commit transactions
29
+ def commit_transaction(conn)
30
+ log_info(TRANSACTION_COMMIT)
31
+ conn.commit
32
+ end
33
+
34
+ # Use JDBC connection's setAutoCommit to true to enable non-transactional behavior
35
+ def remove_transaction(conn)
36
+ conn.setAutoCommit(true) if conn
37
+ super
38
+ end
39
+
40
+ # Use JDBC connection's rollback method to rollback transactions
41
+ def rollback_transaction(conn)
42
+ log_info(TRANSACTION_ROLLBACK)
43
+ conn.rollback
44
+ end
15
45
  end
16
46
 
17
47
  # Dataset class for Oracle datasets accessed via JDBC.
@@ -183,8 +183,10 @@ module Sequel
183
183
  COMMA_SEPARATOR = ', '.freeze
184
184
  DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'with from output from2 where')
185
185
  INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'with into columns output values')
186
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with limit distinct columns into from table_options join where group having order compounds')
186
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with limit distinct columns into from lock join where group having order compounds')
187
187
  UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'with table set output from where')
188
+ NOLOCK = ' WITH (NOLOCK)'.freeze
189
+ UPDLOCK = ' WITH (UPDLOCK)'.freeze
188
190
  WILDCARD = LiteralString.new('*').freeze
189
191
  CONSTANT_MAP = {:CURRENT_DATE=>'CAST(CURRENT_TIMESTAMP AS DATE)'.freeze, :CURRENT_TIME=>'CAST(CURRENT_TIMESTAMP AS TIME)'.freeze}
190
192
 
@@ -244,9 +246,9 @@ module Sequel
244
246
  [insert_sql(columns, LiteralString.new(values.map {|r| "SELECT #{expression_list(r)}" }.join(" UNION ALL ")))]
245
247
  end
246
248
 
247
- # Allows you to do .nolock on a query
249
+ # Allows you to do a dirty read of uncommitted data using WITH (NOLOCK).
248
250
  def nolock
249
- clone(:table_options => "(NOLOCK)")
251
+ lock_style(:dirty)
250
252
  end
251
253
 
252
254
  # Include an OUTPUT clause in the eventual INSERT, UPDATE, or DELETE query.
@@ -299,7 +301,7 @@ module Sequel
299
301
  raise(Error, 'MSSQL requires an order be provided if using an offset') unless order = @opts[:order]
300
302
  dsa1 = dataset_alias(1)
301
303
  rn = row_number_column
302
- sel = [Sequel::SQL::WindowFunction.new(:ROW_NUMBER.sql_function, Sequel::SQL::Window.new(:order=>order)).as(rn)]
304
+ sel = [Sequel::SQL::WindowFunction.new(SQL::Function.new(:ROW_NUMBER), Sequel::SQL::Window.new(:order=>order)).as(rn)]
303
305
  sel.unshift(WILDCARD) unless osel = @opts[:select] and !osel.empty?
304
306
  subselect_sql(unlimited.
305
307
  unordered.
@@ -428,14 +430,26 @@ module Sequel
428
430
  sql << " INTO #{table_ref(@opts[:into])}" if @opts[:into]
429
431
  end
430
432
 
431
- # MSSQL uses TOP for limit
433
+ # MSSQL uses TOP N for limit. For MSSQL 2005+ TOP (N) is used
434
+ # to allow the limit to be a bound variable.
432
435
  def select_limit_sql(sql)
433
- sql << " TOP (#{literal(@opts[:limit])})" if @opts[:limit]
436
+ if l = @opts[:limit]
437
+ l = literal(l)
438
+ l = "(#{l})" if server_version >= 9000000
439
+ sql << " TOP #{l}"
440
+ end
434
441
  end
435
442
 
436
- # MSSQL uses the WITH statement to lock tables
437
- def select_table_options_sql(sql)
438
- sql << " WITH #{@opts[:table_options]}" if @opts[:table_options]
443
+ # Support different types of locking styles
444
+ def select_lock_sql(sql)
445
+ case @opts[:lock]
446
+ when :update
447
+ sql << UPDLOCK
448
+ when :dirty
449
+ sql << NOLOCK
450
+ else
451
+ super
452
+ end
439
453
  end
440
454
 
441
455
  # SQL fragment for MSSQL's OUTPUT clause.
@@ -227,9 +227,10 @@ module Sequel
227
227
  BOOL_TRUE = '1'.freeze
228
228
  BOOL_FALSE = '0'.freeze
229
229
  COMMA_SEPARATOR = ', '.freeze
230
+ FOR_SHARE = ' LOCK IN SHARE MODE'.freeze
230
231
  DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'from where order limit')
231
232
  INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'ignore into columns values on_duplicate_key_update')
232
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'distinct columns from join where group having compounds order limit')
233
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'distinct columns from join where group having compounds order limit lock')
233
234
  UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'table set where order limit')
234
235
 
235
236
  # MySQL specific syntax for LIKE/REGEXP searches, as well as
@@ -248,6 +249,11 @@ module Sequel
248
249
  super(op, args)
249
250
  end
250
251
  end
252
+
253
+ # Return a cloned dataset which will use LOCK IN SHARE MODE to lock returned rows.
254
+ def for_share
255
+ lock_style(:share)
256
+ end
251
257
 
252
258
  # Adds full text filter
253
259
  def full_text_search(cols, terms, opts = {})
@@ -465,6 +471,11 @@ module Sequel
465
471
  def select_clause_methods
466
472
  SELECT_CLAUSE_METHODS
467
473
  end
474
+
475
+ # Support FOR SHARE locking when using the :share lock style.
476
+ def select_lock_sql(sql)
477
+ @opts[:lock] == :share ? (sql << FOR_SHARE) : super
478
+ end
468
479
 
469
480
  # MySQL supports the ORDER BY and LIMIT clauses for UPDATE statements
470
481
  def update_clause_methods
@@ -593,7 +593,6 @@ module Sequel
593
593
  EXPLAIN = 'EXPLAIN '.freeze
594
594
  EXPLAIN_ANALYZE = 'EXPLAIN ANALYZE '.freeze
595
595
  FOR_SHARE = ' FOR SHARE'.freeze
596
- FOR_UPDATE = ' FOR UPDATE'.freeze
597
596
  LOCK = 'LOCK TABLE %s IN %s MODE'.freeze
598
597
  NULL = LiteralString.new('NULL').freeze
599
598
  PG_TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S".freeze
@@ -647,14 +646,9 @@ module Sequel
647
646
  with_sql((opts[:analyze] ? EXPLAIN_ANALYZE : EXPLAIN) + select_sql).map(QUERY_PLAN).join("\r\n")
648
647
  end
649
648
 
650
- # Return a cloned dataset with a :share lock type.
649
+ # Return a cloned dataset which will use FOR SHARE to lock returned rows.
651
650
  def for_share
652
- clone(:lock => :share)
653
- end
654
-
655
- # Return a cloned dataset with a :update lock type.
656
- def for_update
657
- clone(:lock => :update)
651
+ lock_style(:share)
658
652
  end
659
653
 
660
654
  # PostgreSQL specific full text search syntax, using tsearch2 (included
@@ -786,21 +780,16 @@ module Sequel
786
780
  def select_clause_methods
787
781
  server_version >= 80400 ? SELECT_CLAUSE_METHODS_84 : SELECT_CLAUSE_METHODS
788
782
  end
783
+
784
+ # Support FOR SHARE locking when using the :share lock style.
785
+ def select_lock_sql(sql)
786
+ @opts[:lock] == :share ? (sql << FOR_SHARE) : super
787
+ end
789
788
 
790
789
  # SQL fragment for named window specifications
791
790
  def select_window_sql(sql)
792
791
  sql << " WINDOW #{@opts[:window].map{|name, window| "#{literal(name)} AS #{literal(window)}"}.join(', ')}" if @opts[:window]
793
792
  end
794
-
795
- # Support lock mode, allowing FOR SHARE and FOR UPDATE queries.
796
- def select_lock_sql(sql)
797
- case @opts[:lock]
798
- when :update
799
- sql << FOR_UPDATE
800
- when :share
801
- sql << FOR_SHARE
802
- end
803
- end
804
793
 
805
794
  # Use WITH RECURSIVE instead of WITH if any of the CTEs is recursive
806
795
  def select_with_sql_base
@@ -329,6 +329,11 @@ module Sequel
329
329
  SELECT_CLAUSE_METHODS
330
330
  end
331
331
 
332
+ # Support FOR SHARE locking when using the :share lock style.
333
+ def select_lock_sql(sql)
334
+ super unless @opts[:lock] == :update
335
+ end
336
+
332
337
  # SQLite treats a DELETE with no WHERE clause as a TRUNCATE
333
338
  def _truncate_sql(table)
334
339
  "DELETE FROM #{table}"
@@ -1,4 +1,9 @@
1
1
  require 'sqlite3'
2
+ begin
3
+ SQLite3::Database.instance_method(:type_translation)
4
+ rescue
5
+ raise(Sequel::Error, "SQLite3::Database#type_translation is not defined. If you are using the sqlite3 gem, please install the sqlite3-ruby gem.")
6
+ end
2
7
  Sequel.require 'adapters/shared/sqlite'
3
8
 
4
9
  module Sequel
@@ -2,13 +2,13 @@
2
2
  # It is just a wrapper around a single connection that uses the connection pool
3
3
  # API.
4
4
  class Sequel::SingleConnectionPool < Sequel::ConnectionPool
5
- # The SingleConnectionPool always has a size of 1, since the connection
6
- # is always available.
5
+ # The SingleConnectionPool always has a size of 1 if connected
6
+ # and 0 if not.
7
7
  def size
8
8
  @conn ? 1 : 0
9
9
  end
10
10
 
11
- # Disconnect and immediately reconnect from the database.
11
+ # Disconnect the connection from the database.
12
12
  def disconnect(opts=nil, &block)
13
13
  block ||= @disconnection_proc
14
14
  block.call(@conn) if block
@@ -158,13 +158,14 @@ module Sequel
158
158
  m
159
159
  end
160
160
  if block
161
+ result = nil
161
162
  begin
162
- yield(db = c.new(opts))
163
+ result = yield(db = c.new(opts))
163
164
  ensure
164
165
  db.disconnect if db
165
166
  ::Sequel::DATABASES.delete(db)
166
167
  end
167
- nil
168
+ result
168
169
  else
169
170
  c.new(opts)
170
171
  end
@@ -31,11 +31,12 @@ module Sequel
31
31
 
32
32
  # All methods that should have a ! method added that modifies
33
33
  # the receiver.
34
- MUTATION_METHODS = %w'add_graph_aliases and distinct except exclude
35
- filter from from_self full_outer_join graph
36
- group group_and_count group_by having inner_join intersect invert join join_table
37
- left_outer_join limit naked or order order_by order_more paginate qualify query
38
- reverse reverse_order right_outer_join select select_all select_more server
34
+ MUTATION_METHODS = %w'add_graph_aliases and cross_join distinct except exclude
35
+ filter for_update from from_self full_join full_outer_join graph
36
+ group group_and_count group_by having inner_join intersect invert join join_table left_join
37
+ left_outer_join limit lock_style naked natural_full_join natural_join
38
+ natural_left_join natural_right_join or order order_by order_more paginate qualify query
39
+ reverse reverse_order right_join right_outer_join select select_all select_more server
39
40
  set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
40
41
  unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym}
41
42
 
@@ -81,8 +81,8 @@ module Sequel
81
81
  end
82
82
 
83
83
  # Returns a dataset grouped by the given column with count by group,
84
- # order by the count of records (in ascending order). Column aliases
85
- # may be supplied, and will be included in the select clause.
84
+ # order by the count of records. Column aliases may be supplied, and will
85
+ # be included in the select clause.
86
86
  #
87
87
  # Examples:
88
88
  #
@@ -90,7 +90,7 @@ module Sequel
90
90
  # ds.group_and_count(:first_name, :last_name).all => [{:first_name=>'a', :last_name=>'b', :count=>1}, ...]
91
91
  # ds.group_and_count(:first_name___name).all => [{:name=>'a', :count=>1}, ...]
92
92
  def group_and_count(*columns)
93
- group(*columns.map{|c| unaliased_identifier(c)}).select(*(columns + [COUNT_OF_ALL_AS_COUNT])).order(:count)
93
+ group(*columns.map{|c| unaliased_identifier(c)}).select(*(columns + [COUNT_OF_ALL_AS_COUNT]))
94
94
  end
95
95
 
96
96
  # Inserts multiple records into the associated table. This method can be
@@ -104,6 +104,11 @@ module Sequel
104
104
  def filter(*cond, &block)
105
105
  _filter(@opts[:having] ? :having : :where, *cond, &block)
106
106
  end
107
+
108
+ # Returns a cloned dataset with a :update lock style.
109
+ def for_update
110
+ lock_style(:update)
111
+ end
107
112
 
108
113
  # Returns a copy of the dataset with the source changed.
109
114
  #
@@ -236,6 +241,14 @@ module Sequel
236
241
  clone(opts)
237
242
  end
238
243
 
244
+ # Returns a cloned dataset with the given lock style. If style is a
245
+ # string, it will be used directly. Otherwise, a symbol may be used
246
+ # for database independent locking. Currently :update is respected
247
+ # by most databases, and :share is supported by some.
248
+ def lock_style(style)
249
+ clone(:lock => style)
250
+ end
251
+
239
252
  # Adds an alternate filter to an existing filter using OR. If no filter
240
253
  # exists an error is raised.
241
254
  #
@@ -25,6 +25,7 @@ module Sequel
25
25
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
26
26
  COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
27
27
  DATASET_ALIAS_BASE_NAME = 't'.freeze
28
+ FOR_UPDATE = ' FOR UPDATE'.freeze
28
29
  IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
29
30
  IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
30
31
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
@@ -33,7 +34,7 @@ module Sequel
33
34
  QUESTION_MARK = '?'.freeze
34
35
  DELETE_CLAUSE_METHODS = clause_methods(:delete, %w'from where')
35
36
  INSERT_CLAUSE_METHODS = clause_methods(:insert, %w'into columns values')
36
- SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct columns from join where group having compounds order limit')
37
+ SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct columns from join where group having compounds order limit lock')
37
38
  UPDATE_CLAUSE_METHODS = clause_methods(:update, %w'table set where')
38
39
  TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S%N%z'".freeze
39
40
  STANDARD_TIMESTAMP_FORMAT = "TIMESTAMP #{TIMESTAMP_FORMAT}".freeze
@@ -189,6 +190,25 @@ module Sequel
189
190
  end
190
191
  end
191
192
  alias first_source first_source_alias
193
+
194
+ # The first source (primary table) for this dataset. If the dataset doesn't
195
+ # have a table, raises an error. If the table is aliased, returns the original
196
+ # table, not the alias
197
+ def first_source_table
198
+ source = @opts[:from]
199
+ if source.nil? || source.empty?
200
+ raise Error, 'No source specified for query'
201
+ end
202
+ case s = source.first
203
+ when SQL::AliasedExpression
204
+ s.expression
205
+ when Symbol
206
+ sch, table, aliaz = split_symbol(s)
207
+ aliaz ? (sch ? SQL::QualifiedIdentifier.new(sch, table) : table.to_sym) : s
208
+ else
209
+ s
210
+ end
211
+ end
192
212
 
193
213
  # SQL fragment specifying an SQL function call
194
214
  def function_sql(f)
@@ -1074,6 +1094,16 @@ module Sequel
1074
1094
  sql << " LIMIT #{literal(@opts[:limit])}" if @opts[:limit]
1075
1095
  sql << " OFFSET #{literal(@opts[:offset])}" if @opts[:offset]
1076
1096
  end
1097
+
1098
+ # Modify the sql to support the different types of locking modes.
1099
+ def select_lock_sql(sql)
1100
+ case @opts[:lock]
1101
+ when :update
1102
+ sql << FOR_UPDATE
1103
+ when String
1104
+ sql << " #{@opts[:lock]}"
1105
+ end
1106
+ end
1077
1107
 
1078
1108
  # Modify the sql to add the expressions to ORDER BY
1079
1109
  def select_order_sql(sql)
@@ -86,8 +86,8 @@ END_MIG
86
86
  # name and arguments to it to pass to a Schema::Generator to recreate the column.
87
87
  def column_schema_to_generator_opts(name, schema, options)
88
88
  if options[:single_pk] && schema_autoincrementing_primary_key?(schema)
89
- type_hash = column_schema_to_ruby_type(schema)
90
- if type_hash == {:type=>Integer}
89
+ type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
90
+ if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"}
91
91
  [:primary_key, name]
92
92
  else
93
93
  [:primary_key, name, type_hash]
@@ -115,7 +115,7 @@ END_MIG
115
115
  when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?\z/o
116
116
  {:type=>Integer}
117
117
  when /\Atinyint(?:\((\d+)\))?\z/o
118
- {:type=>(self.class.adapter_scheme == :mysql && $1 == '1' && Sequel::MySQL.convert_tinyint_to_bool ? TrueClass : Integer)}
118
+ {:type =>schema[:type] == :boolean ? TrueClass : Integer}
119
119
  when /\Abigint(?:\((?:\d+)\))?\z/o
120
120
  {:type=>Bignum}
121
121
  when /\A(?:real|float|double(?: precision)?)\z/o
@@ -44,13 +44,15 @@ module Sequel
44
44
  ANONYMOUS_MODEL_CLASSES = {}
45
45
 
46
46
  # Class methods added to model that call the method of the same name on the dataset
47
- DATASET_METHODS = %w'<< add_graph_aliases all avg count delete distinct
48
- each each_page eager eager_graph empty? except exclude filter first from from_self
49
- full_outer_join get graph grep group group_and_count group_by having import
47
+ DATASET_METHODS = %w'<< add_graph_aliases all avg count cross_join delete distinct
48
+ each each_page each_server eager eager_graph empty? except exclude filter first for_update from from_self
49
+ full_join full_outer_join get graph grep group group_and_count group_by having import
50
50
  inner_join insert insert_multiple intersect interval invert join join_table
51
- last left_outer_join limit map max min multi_insert naked order order_by
52
- order_more paginate print qualify query range reverse reverse_order right_outer_join
53
- select select_all select_more server set set_defaults set_graph_aliases set_overrides
51
+ last left_join left_outer_join limit lock_style map max min multi_insert naked
52
+ natural_full_join natural_join natural_left_join natural_right_join order order_by
53
+ order_more paginate print qualify query range reverse reverse_order right_join right_outer_join
54
+ select select_all select_hash select_map select_more select_order_map
55
+ server set set_defaults set_graph_aliases set_overrides
54
56
  single_value sum to_csv to_hash truncate unfiltered ungraphed ungrouped union unlimited unordered
55
57
  update where with with_recursive with_sql'.map{|x| x.to_sym}
56
58
 
@@ -106,10 +106,11 @@ module Sequel
106
106
  # it sets album.artist to this_artist.
107
107
  def reciprocal
108
108
  return self[:reciprocal] if include?(:reciprocal)
109
- r_type = reciprocal_type
109
+ r_types = Array(reciprocal_type)
110
110
  keys = self[:keys]
111
111
  associated_class.all_association_reflections.each do |assoc_reflect|
112
- if assoc_reflect[:type] == r_type && assoc_reflect[:keys] == keys && assoc_reflect.associated_class == self[:model]
112
+ if r_types.include?(assoc_reflect[:type]) && assoc_reflect[:keys] == keys && assoc_reflect.associated_class == self[:model]
113
+ self[:reciprocal_type] = assoc_reflect[:type]
113
114
  return self[:reciprocal] = assoc_reflect[:name]
114
115
  end
115
116
  end
@@ -188,7 +189,7 @@ module Sequel
188
189
 
189
190
  # The key to use for the key hash when eager loading
190
191
  def eager_loader_key
191
- self[:key]
192
+ self[:eager_loader_key] ||= self[:key]
192
193
  end
193
194
 
194
195
  # The column(s) in the associated table that the key in the current table references (either a symbol or an array).
@@ -201,18 +202,30 @@ module Sequel
201
202
  self[:primary_keys] ||= Array(primary_key)
202
203
  end
203
204
  alias associated_object_keys primary_keys
205
+
206
+ # True only if the reciprocal is a one_to_many association.
207
+ def reciprocal_array?
208
+ !set_reciprocal_to_self?
209
+ end
204
210
 
205
211
  # Whether this association returns an array of objects instead of a single object,
206
212
  # false for a many_to_one association.
207
213
  def returns_array?
208
214
  false
209
215
  end
216
+
217
+ # True only if the reciprocal is a one_to_one association.
218
+ def set_reciprocal_to_self?
219
+ reciprocal
220
+ self[:reciprocal_type] == :one_to_one
221
+ end
210
222
 
211
223
  private
212
224
 
213
- # The reciprocal type of a many_to_one association is a one_to_many association.
225
+ # The reciprocal type of a many_to_one association is either
226
+ # a one_to_many or a one_to_one association.
214
227
  def reciprocal_type
215
- :one_to_many
228
+ self[:reciprocal_type] ||= [:one_to_many, :one_to_one]
216
229
  end
217
230
  end
218
231
 
@@ -235,12 +248,16 @@ module Sequel
235
248
  def default_key
236
249
  :"#{underscore(demodulize(self[:model].name))}_id"
237
250
  end
251
+
252
+ # The key to use for the key hash when eager loading
253
+ def eager_loader_key
254
+ self[:eager_loader_key] ||= primary_key
255
+ end
238
256
 
239
257
  # The column in the current table that the key in the associated table references.
240
258
  def primary_key
241
259
  self[:primary_key] ||= self[:model].primary_key
242
260
  end
243
- alias eager_loader_key primary_key
244
261
 
245
262
  # One to many associations set the reciprocal to self when loading associated records.
246
263
  def set_reciprocal_to_self?
@@ -265,6 +282,15 @@ module Sequel
265
282
  :many_to_one
266
283
  end
267
284
  end
285
+
286
+ class OneToOneAssociationReflection < OneToManyAssociationReflection
287
+ ASSOCIATION_TYPES[:one_to_one] = self
288
+
289
+ # one_to_one associations return a single object, not an array
290
+ def returns_array?
291
+ false
292
+ end
293
+ end
268
294
 
269
295
  class ManyToManyAssociationReflection < AssociationReflection
270
296
  ASSOCIATION_TYPES[:many_to_many] = self
@@ -315,7 +341,7 @@ module Sequel
315
341
 
316
342
  # The key to use for the key hash when eager loading
317
343
  def eager_loader_key
318
- self[:left_primary_key]
344
+ self[:eager_loader_key] ||= self[:left_primary_key]
319
345
  end
320
346
 
321
347
  # many_to_many associations need to select a key in an associated table to eagerly load
@@ -378,6 +404,7 @@ module Sequel
378
404
  #
379
405
  # class Project < Sequel::Model
380
406
  # many_to_one :portfolio
407
+ # # or: one_to_one :portfolio
381
408
  # one_to_many :milestones
382
409
  # # or: many_to_many :milestones
383
410
  # end
@@ -385,7 +412,7 @@ module Sequel
385
412
  # The project class now has the following instance methods:
386
413
  # * portfolio - Returns the associated portfolio.
387
414
  # * portfolio=(obj) - Sets the associated portfolio to the object,
388
- # but the change is not persisted until you save the record.
415
+ # but the change is not persisted until you save the record (for many_to_one associations).
389
416
  # * portfolio_dataset - Returns a dataset that would return the associated
390
417
  # portfolio, only useful in fairly specific circumstances.
391
418
  # * milestones - Returns an array of associated milestones
@@ -395,16 +422,16 @@ module Sequel
395
422
  # * milestones_dataset - Returns a dataset that would return the associated
396
423
  # milestones, allowing for further filtering/limiting/etc.
397
424
  #
398
- # If you want to override the behavior of the add_/remove_/remove_all_ methods,
399
- # there are private instance methods created that a prepended with an
400
- # underscore (e.g. _add_milestone). The private instance methods can be
425
+ # If you want to override the behavior of the add_/remove_/remove_all_/ methods
426
+ # or the association setter method, there are private instance methods created that are prepended
427
+ # with an underscore (e.g. _add_milestone or _portfolio=). The private instance methods can be
401
428
  # easily overridden, but you shouldn't override the public instance methods without
402
429
  # calling super, as they deal with callbacks and caching.
403
430
  #
404
431
  # By default the classes for the associations are inferred from the association
405
432
  # name, so for example the Project#portfolio will return an instance of
406
433
  # Portfolio, and Project#milestones will return an array of Milestone
407
- # instances.
434
+ # instances. You can use the :class option to change which class is used.
408
435
  #
409
436
  # Association definitions are also reflected by the class, e.g.:
410
437
  #
@@ -432,20 +459,16 @@ module Sequel
432
459
  # model's primary key. Each current model object can be associated with
433
460
  # more than one associated model objects. Each associated model object
434
461
  # can be associated with only one current model object.
462
+ # * :one_to_one - Similar to one_to_many in terms of foreign keys, but
463
+ # only one object is associated to the current object through the
464
+ # association. The methods created are similar to many_to_one, except
465
+ # that the one_to_one setter method saves the passed object.
435
466
  # * :many_to_many - A join table is used that has a foreign key that points
436
467
  # to this model's primary key and a foreign key that points to the
437
468
  # associated model's primary key. Each current model object can be
438
469
  # associated with many associated model objects, and each associated
439
470
  # model object can be associated with many current model objects.
440
471
  #
441
- # A one to one relationship can be set up with a many_to_one association
442
- # on the table with the foreign key, and a one_to_many association with the
443
- # :one_to_one option specified on the table without the foreign key. The
444
- # two associations will operate similarly, except that the many_to_one
445
- # association setter doesn't update the database until you call save manually.
446
- # Also, in most cases you need to specify the plural association name when using
447
- # one_to_many with the :one_to_one option.
448
- #
449
472
  # The following options can be supplied:
450
473
  # * *ALL types*:
451
474
  # - :after_add - Symbol, Proc, or array of both/either specifying a callback to call
@@ -455,12 +478,16 @@ module Sequel
455
478
  # when eager loading via eager_graph, but called when eager loading via eager.
456
479
  # - :after_remove - Symbol, Proc, or array of both/either specifying a callback to call
457
480
  # after an item is removed from the association.
481
+ # - :after_set - Symbol, Proc, or array of both/either specifying a callback to call
482
+ # after an item is set using the association setter method.
458
483
  # - :allow_eager - If set to false, you cannot load the association eagerly
459
484
  # via eager or eager_graph
460
485
  # - :before_add - Symbol, Proc, or array of both/either specifying a callback to call
461
486
  # before a new item is added to the association.
462
487
  # - :before_remove - Symbol, Proc, or array of both/either specifying a callback to call
463
488
  # before an item is removed from the association.
489
+ # - :before_set - Symbol, Proc, or array of both/either specifying a callback to call
490
+ # before an item is set using the association setter method.
464
491
  # - :cartesian_product_number - the number of joins completed by this association that could cause more
465
492
  # than one row for each row in the current table (default: 0 for many_to_one associations,
466
493
  # 1 for *_to_many associations).
@@ -495,6 +522,8 @@ module Sequel
495
522
  # and a hash of dependent associations. The associated records should
496
523
  # be queried from the database and the associations cache for each
497
524
  # record should be populated for this to work correctly.
525
+ # - :eager_loader_key - A symbol for the key column to use to populate the key hash
526
+ # for the eager loader.
498
527
  # - :extend - A module or array of modules to extend the dataset with.
499
528
  # - :graph_block - The block to pass to join_table when eagerly loading
500
529
  # the association via eager_graph.
@@ -540,15 +569,6 @@ module Sequel
540
569
  # current model's primary key, as a symbol. Defaults to
541
570
  # :"#{self.name.underscore}_id". Can use an
542
571
  # array of symbols for a composite key association.
543
- # - :one_to_one: Create a getter and setter similar to those of many_to_one
544
- # associations. The getter returns a singular matching record, or raises an
545
- # error if multiple records match. The setter updates the record given and removes
546
- # associations with all other records. When this option is used, the other
547
- # association methods usually added are either removed or made private,
548
- # so using this is similar to using many_to_one, in terms of the methods
549
- # it adds, the main difference is that the foreign key is in the associated
550
- # table instead of the current table. Note that using this option still requires
551
- # you to use a plural name when creating and using the association (e.g. for reflections, eager loading, etc.).
552
572
  # - :primary_key - column in the current table that :key option references, as a symbol.
553
573
  # Defaults to primary key of the current table. Can use an
554
574
  # array of symbols for a composite key association.
@@ -583,6 +603,7 @@ module Sequel
583
603
  # array of symbols for a composite key association.
584
604
  # - :uniq - Adds a after_load callback that makes the array of objects unique.
585
605
  def associate(type, name, opts = {}, &block)
606
+ raise(Error, 'one_to_many association type with :one_to_one option removed, used one_to_one association type') if opts[:one_to_one] && type == :one_to_many
586
607
  raise(Error, 'invalid association type') unless assoc_class = ASSOCIATION_TYPES[type]
587
608
  raise(Error, 'Model.associate name argument must be a symbol') unless Symbol === name
588
609
 
@@ -599,20 +620,11 @@ module Sequel
599
620
  opts[:graph_conditions] = conds if !opts.include?(:graph_conditions) and Sequel.condition_specifier?(conds)
600
621
  opts[:graph_conditions] = opts[:graph_conditions] ? opts[:graph_conditions].to_a : []
601
622
  opts[:graph_select] = Array(opts[:graph_select]) if opts[:graph_select]
602
- [:before_add, :before_remove, :after_add, :after_remove, :after_load, :extend].each do |cb_type|
623
+ [:before_add, :before_remove, :after_add, :after_remove, :after_load, :before_set, :after_set, :extend].each do |cb_type|
603
624
  opts[cb_type] = Array(opts[cb_type])
604
625
  end
605
-
606
- # find class
607
- case opts[:class]
608
- when String, Symbol
609
- # Delete :class to allow late binding
610
- opts[:class_name] ||= opts.delete(:class).to_s
611
- when Class
612
- opts[:class_name] ||= opts[:class].name
613
- end
614
- opts[:class_name] ||= ((self.name || '').split("::")[0..-2] + [camelize(opts.returns_array? ? singularize(name) : name)]).join('::')
615
-
626
+ late_binding_class_option(opts, opts.returns_array? ? singularize(name) : name)
627
+
616
628
  send(:"def_#{type}", opts)
617
629
 
618
630
  orig_opts.delete(:clone)
@@ -664,18 +676,23 @@ module Sequel
664
676
  end
665
677
 
666
678
  # Shortcut for adding a many_to_many association, see associate
667
- def many_to_many(*args, &block)
668
- associate(:many_to_many, *args, &block)
679
+ def many_to_many(name, opts={}, &block)
680
+ associate(:many_to_many, name, opts, &block)
669
681
  end
670
682
 
671
683
  # Shortcut for adding a many_to_one association, see associate
672
- def many_to_one(*args, &block)
673
- associate(:many_to_one, *args, &block)
684
+ def many_to_one(name, opts={}, &block)
685
+ associate(:many_to_one, name, opts, &block)
674
686
  end
675
687
 
676
688
  # Shortcut for adding a one_to_many association, see associate
677
- def one_to_many(*args, &block)
678
- associate(:one_to_many, *args, &block)
689
+ def one_to_many(name, opts={}, &block)
690
+ associate(:one_to_many, name, opts, &block)
691
+ end
692
+
693
+ # Shortcut for adding a one_to_one association, see associate.
694
+ def one_to_one(name, opts={}, &block)
695
+ associate(:one_to_one, name, opts, &block)
679
696
  end
680
697
 
681
698
  private
@@ -781,7 +798,7 @@ module Sequel
781
798
  database.dataset.from(join_table).filter(lcks.zip(lcpks.map{|k| send(k)}) + rcks.zip(opts.right_primary_keys.map{|k| o.send(k)})).delete
782
799
  end
783
800
  association_module_private_def(opts._remove_all_method) do
784
- database.dataset.from(join_table).filter(lcks.zip(lcpks.map{|k| send(k)})).delete
801
+ _apply_association_options(opts, database.dataset.from(join_table).filter(lcks.zip(lcpks.map{|k| send(k)}))).delete
785
802
  end
786
803
 
787
804
  def_add_method(opts)
@@ -839,6 +856,7 @@ module Sequel
839
856
 
840
857
  # Adds one_to_many association instance methods
841
858
  def def_one_to_many(opts)
859
+ one_to_one = opts[:type] == :one_to_one
842
860
  name = opts[:name]
843
861
  model = self
844
862
  key = (opts[:key] ||= opts.default_key)
@@ -853,15 +871,26 @@ module Sequel
853
871
  end
854
872
  opts[:eager_loader] ||= proc do |key_hash, records, associations|
855
873
  h = key_hash[primary_key]
856
- records.each{|object| object.associations[name] = []}
874
+ if one_to_one
875
+ records.each{|object| object.associations[name] = nil}
876
+ else
877
+ records.each{|object| object.associations[name] = []}
878
+ end
857
879
  reciprocal = opts.reciprocal
858
880
  klass = opts.associated_class
859
881
  model.eager_loading_dataset(opts, klass.filter(uses_cks ? {cks.map{|k| SQL::QualifiedIdentifier.new(klass.table_name, k)}=>SQL::SQLArray.new(h.keys)} : {SQL::QualifiedIdentifier.new(klass.table_name, key)=>h.keys}), opts.select, associations).all do |assoc_record|
860
882
  hash_key = uses_cks ? cks.map{|k| assoc_record.send(k)} : assoc_record.send(key)
861
883
  next unless objects = h[hash_key]
862
- objects.each do |object|
863
- object.associations[name].push(assoc_record)
864
- assoc_record.associations[reciprocal] = object if reciprocal
884
+ if one_to_one
885
+ objects.each do |object|
886
+ object.associations[name] = assoc_record
887
+ assoc_record.associations[reciprocal] = object if reciprocal
888
+ end
889
+ else
890
+ objects.each do |object|
891
+ object.associations[name].push(assoc_record)
892
+ assoc_record.associations[reciprocal] = object if reciprocal
893
+ end
865
894
  end
866
895
  end
867
896
  end
@@ -871,7 +900,7 @@ module Sequel
871
900
  use_only_conditions = opts.include?(:graph_only_conditions)
872
901
  only_conditions = opts[:graph_only_conditions]
873
902
  conditions = opts[:graph_conditions]
874
- opts[:cartesian_product_number] ||= 1
903
+ opts[:cartesian_product_number] ||= one_to_one ? 0 : 1
875
904
  graph_block = opts[:graph_block]
876
905
  opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
877
906
  ds = ds.graph(opts.associated_class, use_only_conditions ? only_conditions : cks.zip(cpks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &graph_block)
@@ -884,48 +913,43 @@ module Sequel
884
913
 
885
914
  ck_nil_hash ={}
886
915
  cks.each{|k| ck_nil_hash[k] = nil}
887
-
916
+
888
917
  unless opts[:read_only]
889
918
  validate = opts[:validate]
890
- association_module_private_def(opts._add_method) do |o|
891
- cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
892
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
893
- end
894
- def_add_method(opts)
919
+
920
+ if one_to_one
921
+ association_module_private_def(opts._setter_method) do |o|
922
+ up_ds = _apply_association_options(opts, opts.associated_class.filter(cks.zip(cpks.map{|k| send(k)})))
923
+ if o
924
+ up_ds = up_ds.exclude(o.pk_hash)
925
+ cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
926
+ end
927
+ update_database = lambda do
928
+ up_ds.update(ck_nil_hash)
929
+ o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save") if o
930
+ end
931
+ use_transactions && o ? db.transaction(opts){update_database.call} : update_database.call
932
+ end
933
+ association_module_def(opts.setter_method){|o| set_one_to_one_associated_object(opts, o)}
934
+ else
935
+ association_module_private_def(opts._add_method) do |o|
936
+ cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
937
+ o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
938
+ end
939
+ def_add_method(opts)
895
940
 
896
- unless opts[:one_to_one]
897
941
  association_module_private_def(opts._remove_method) do |o|
898
942
  cks.each{|k| o.send(:"#{k}=", nil)}
899
943
  o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
900
944
  end
901
945
  association_module_private_def(opts._remove_all_method) do
902
- opts.associated_class.filter(cks.zip(cpks.map{|k| send(k)})).update(ck_nil_hash)
946
+ _apply_association_options(opts, opts.associated_class.filter(cks.zip(cpks.map{|k| send(k)}))).update(ck_nil_hash)
903
947
  end
904
948
  def_remove_methods(opts)
905
949
  end
906
950
  end
907
- if opts[:one_to_one]
908
- overridable_methods_module.send(:private, opts.association_method, opts.dataset_method)
909
- n = singularize(name).to_sym
910
- raise(Sequel::Error, "one_to_many association names should still be plural even when using the :one_to_one option") if n == name
911
- association_module_def(n) do |*o|
912
- objs = send(name, *o)
913
- raise(Sequel::Error, "multiple values found for a one-to-one relationship") if objs.length > 1
914
- objs.first
915
- end
916
- unless opts[:read_only]
917
- overridable_methods_module.send(:private, opts.add_method)
918
- association_module_def(:"#{n}=") do |o|
919
- klass = opts.associated_class
920
- update_database = lambda do
921
- send(opts.add_method, o)
922
- klass.filter(cks.zip(cpks.map{|k| send(k)})).exclude(o.pk_hash).update(ck_nil_hash)
923
- end
924
- use_transactions ? db.transaction(opts){update_database.call} : update_database.call
925
- end
926
- end
927
- end
928
951
  end
952
+ alias def_one_to_one def_one_to_many
929
953
 
930
954
  # Add the remove_ and remove_all instance methods
931
955
  def def_remove_methods(opts)
@@ -944,11 +968,8 @@ module Sequel
944
968
  end
945
969
 
946
970
  private
947
-
948
- # Backbone behind association dataset methods
949
- def _dataset(opts)
950
- raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
951
- ds = send(opts._dataset_method)
971
+
972
+ def _apply_association_options(opts, ds)
952
973
  ds.extend(AssociationDatasetMethods)
953
974
  ds.model_object = self
954
975
  ds.association_reflection = opts
@@ -959,6 +980,7 @@ module Sequel
959
980
  end
960
981
  ds = ds.order(*opts[:order]) if opts[:order]
961
982
  ds = ds.limit(*opts[:limit]) if opts[:limit]
983
+ ds = ds.limit(1) if !opts.returns_array? && opts[:key]
962
984
  ds = ds.eager(*opts[:eager]) if opts[:eager]
963
985
  ds = ds.distinct if opts[:distinct]
964
986
  ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] && opts.eager_graph_lazy_dataset?
@@ -966,15 +988,19 @@ module Sequel
966
988
  ds
967
989
  end
968
990
 
991
+ # Backbone behind association dataset methods
992
+ def _dataset(opts)
993
+ raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
994
+ _apply_association_options(opts, send(opts._dataset_method))
995
+ end
996
+
969
997
  # Return the associated objects from the dataset, without callbacks, reciprocals, and caching.
970
998
  def _load_associated_objects(opts)
971
999
  if opts.returns_array?
972
1000
  opts.can_have_associated_objects?(self) ? send(opts.dataset_method).all : []
973
1001
  else
974
- if !opts[:key]
1002
+ if opts.can_have_associated_objects?(self)
975
1003
  send(opts.dataset_method).all.first
976
- elsif opts.can_have_associated_objects?(self)
977
- send(opts.dataset_method).first
978
1004
  end
979
1005
  end
980
1006
  end
@@ -1028,7 +1054,13 @@ module Sequel
1028
1054
  else
1029
1055
  objs = _load_associated_objects(opts)
1030
1056
  run_association_callbacks(opts, :after_load, objs)
1031
- objs.each{|o| add_reciprocal_object(opts, o)} if opts.set_reciprocal_to_self?
1057
+ if opts.set_reciprocal_to_self?
1058
+ if opts.returns_array?
1059
+ objs.each{|o| add_reciprocal_object(opts, o)}
1060
+ elsif objs
1061
+ add_reciprocal_object(opts, objs)
1062
+ end
1063
+ end
1032
1064
  associations[name] = objs
1033
1065
  end
1034
1066
  end
@@ -1079,7 +1111,7 @@ module Sequel
1079
1111
  # Run the callback for the association with the object.
1080
1112
  def run_association_callbacks(reflection, callback_type, object)
1081
1113
  raise_error = raise_on_save_failure || !reflection.returns_array?
1082
- stop_on_false = [:before_add, :before_remove].include?(callback_type)
1114
+ stop_on_false = [:before_add, :before_remove, :before_set].include?(callback_type)
1083
1115
  reflection[callback_type].each do |cb|
1084
1116
  res = case cb
1085
1117
  when Symbol
@@ -1098,19 +1130,29 @@ module Sequel
1098
1130
 
1099
1131
  # Set the given object as the associated object for the given association
1100
1132
  def set_associated_object(opts, o)
1101
- raise(Sequel::Error, "associated object #{o.inspect} does not have a primary key") if o && !o.pk
1102
- old_val = send(opts.association_method)
1103
- return o if old_val == o
1104
- return if old_val and run_association_callbacks(opts, :before_remove, old_val) == false
1105
- return if o and run_association_callbacks(opts, :before_add, o) == false
1133
+ raise(Error, "associated object #{o.inspect} does not have a primary key") if o && !o.pk
1134
+ run_association_callbacks(opts, :before_set, o)
1135
+ if a = associations[opts[:name]]
1136
+ remove_reciprocal_object(opts, a)
1137
+ end
1106
1138
  send(opts._setter_method, o)
1107
1139
  associations[opts[:name]] = o
1108
- remove_reciprocal_object(opts, old_val) if old_val
1109
- if o
1110
- add_reciprocal_object(opts, o)
1111
- run_association_callbacks(opts, :after_add, o)
1140
+ add_reciprocal_object(opts, o) if o
1141
+ run_association_callbacks(opts, :after_set, o)
1142
+ o
1143
+ end
1144
+
1145
+ # Set the given object as the associated object for the given association
1146
+ def set_one_to_one_associated_object(opts, o)
1147
+ raise(Error, "object #{inspect} does not have a primary key") unless pk
1148
+ run_association_callbacks(opts, :before_set, o)
1149
+ if a = associations[opts[:name]]
1150
+ remove_reciprocal_object(opts, a)
1112
1151
  end
1113
- run_association_callbacks(opts, :after_remove, old_val) if old_val
1152
+ send(opts._setter_method, o)
1153
+ associations[opts[:name]] = o
1154
+ add_reciprocal_object(opts, o) if o
1155
+ run_association_callbacks(opts, :after_set, o)
1114
1156
  o
1115
1157
  end
1116
1158
  end