sequel 5.24.0 → 5.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ea0327d7fbbc76458fc9dc6a088069f04bd100b37f91288e9db9f37db84bbb7
4
- data.tar.gz: 6195027a34de796b5109ee2b2ed57407ce9a58f75b1661a3be0ab9b2acbba885
3
+ metadata.gz: 7a91737db8ac64d56de1b6ac6ffcd48c657b3e52cd42c310872ed7935cf1ef24
4
+ data.tar.gz: c650010eaa68c3f0b086ad9d283e363c30d2c4667ffe8d660801f3d65e06f243
5
5
  SHA512:
6
- metadata.gz: aaa9a71ee0562ccedaeee6144c4764a8024699214dfd4fd7ce46e8a301d8df72f1a709d373f7f5d47c455ff70407b61e1e0bf05ee7f30e20c49a3be51a1bbea2
7
- data.tar.gz: d8343280ef3c09e930df1db0e12eeac349ce23f396fccf1dfcc055a5e760d5dbb50fd5d0a4f8ce591ac4d12644833b9afea6a94fa1b52991c580baba28c26926
6
+ metadata.gz: 6779429490fea9527a7b920616db24bbd10f5daab731516b396053cd9626a3b9d44413b337576c1bffa0f04e511fa090ef536b270bddc3931b88519224514596
7
+ data.tar.gz: c0376415c7a8eafe79ceadc66940944d64e9261209738905790cdd8821c377eea31270026730a2cb4a17adce921c60f5519591c31fb4d62250328bb085b5cfaa
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ === 5.25.0 (2019-10-01)
2
+
3
+ * Fix Sequel::SQL::NumericMethods#coerce to not raise NoMethodError if super method is not defined (jeremyevans) (#1645)
4
+
5
+ * Allow setting a default for a column that already has a default on Microsoft SQL Server (jeremyevans)
6
+
7
+ * Fix keyword argument separation warnings on Ruby master branch in csv_serializer plugin (jeremyevans)
8
+
9
+ * Add association_multi_add_remove plugin for adding/removing multiple associated objects in a single method call (AlexWayfer, jeremyevans) (#1641, #1643)
10
+
11
+ * Make sharding plugin integrate with server_block extension (jeremyevans)
12
+
1
13
  === 5.24.0 (2019-09-01)
2
14
 
3
15
  * Add Database#skip_logging? private method designed for extensions to force query timing even if no logger is present (adam12) (#1640)
@@ -0,0 +1,32 @@
1
+ = New Features
2
+
3
+ * An association_multi_add_remove plugin has been added. This plugin
4
+ adds a shortcut for adding or removing multiple associated objects
5
+ in a single method call:
6
+
7
+ Artist.plugin :association_multi_add_remove
8
+ Artist.many_to_one :albums
9
+ Artist[1].add_albums([Album[2], Album[3]])
10
+ Artist[1].remove_albums([Album[4], Album[5]])
11
+
12
+ It also offers a setter method, which will add and remove associated
13
+ objects as necessary:
14
+
15
+ Artist[1].albums = [Album[3], Album[4]]
16
+
17
+ = Other Improvements
18
+
19
+ * The sharding plugin now integrates with the server_block extension.
20
+ This makes it so if you retrieve a model instance inside a
21
+ with_server block, saving the model instance will save it back to
22
+ the shard from which it was retrieved.
23
+
24
+ * Setting a default for a column on Microsoft SQL Server now works
25
+ correctly if the column already has a default.
26
+
27
+ * Sequel::SQL::NumericMethods#coerce no longer raises NoMethodError
28
+ if the super method is not defined. This fixes some cases when
29
+ comparing Date/DateTime instances to Sequel objects.
30
+
31
+ * The csv_serializer plugin now avoids keyword argument separation
32
+ issues on Ruby 2.7+.
@@ -279,7 +279,7 @@ module Sequel
279
279
  end
280
280
  end
281
281
  sqls << "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{column_definition_sql(op)}"
282
- sqls << alter_table_sql(table, op.merge(:op=>:set_column_default, :default=>default)) if default
282
+ sqls << alter_table_sql(table, op.merge(:op=>:set_column_default, :default=>default, :skip_drop_default=>true)) if default
283
283
  sqls
284
284
  when :set_column_null
285
285
  sch = schema(table).find{|k,v| k.to_s == op[:name].to_s}.last
@@ -290,7 +290,9 @@ module Sequel
290
290
  end
291
291
  "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(:type=>type)} #{'NOT ' unless op[:null]}NULL"
292
292
  when :set_column_default
293
- "ALTER TABLE #{quote_schema_table(table)} ADD CONSTRAINT #{quote_identifier("sequel_#{table}_#{op[:name]}_def")} DEFAULT #{literal(op[:default])} FOR #{quote_identifier(op[:name])}"
293
+ sqls = []
294
+ add_drop_default_constraint_sql(sqls, table, op[:name]) unless op[:skip_drop_default]
295
+ sqls << "ALTER TABLE #{quote_schema_table(table)} ADD CONSTRAINT #{quote_identifier("sequel_#{table}_#{op[:name]}_def")} DEFAULT #{literal(op[:default])} FOR #{quote_identifier(op[:name])}"
294
296
  else
295
297
  super(table, op)
296
298
  end
@@ -0,0 +1,83 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The association_multi_add_remove plugin allows adding, removing and setting
6
+ # multiple associated objects in a single method call.
7
+ # By default Sequel::Model defines singular <tt>add_*</tt> and <tt>remove_*</tt>
8
+ # methods that operate on a single associated object, this adds plural forms
9
+ # that operate on multiple associated objects. Example:
10
+ #
11
+ # artist.albums # => [album1]
12
+ # artist.add_albums([album2, album3])
13
+ # artist.albums # => [album1, album2, album3]
14
+ # artist.remove_albums([album3, album1])
15
+ # artist.albums # => [album2]
16
+ # artist.albums = [album2, album3]
17
+ # artist.albums # => [album2, album3]
18
+ #
19
+ # It can handle all situations that the normal singular methods handle, but there is
20
+ # no attempt to optimize behavior, so using these methods will not improve performance.
21
+ #
22
+ # The add/remove/set methods defined by this plugin use a transaction,
23
+ # so if one add/remove/set fails and raises an exception, all adds/removes/set
24
+ # will be rolled back. If you are using database sharding and want to save
25
+ # to a specific shard, call Model#set_server to set the server for this instance,
26
+ # as the transaction will be opened on that server.
27
+ #
28
+ # You can customize the method names used for adding/removing multiple associated
29
+ # objects using the :multi_add_method and :multi_remove_method association options.
30
+ #
31
+ # Usage:
32
+ #
33
+ # # Allow adding/removing/setting multiple associated objects in a single call
34
+ # # for all model subclass instances (called before loading subclasses):
35
+ # Sequel::Model.plugin :association_multi_add_remove
36
+ #
37
+ # # Allow adding/removing/setting multiple associated objects in a single call
38
+ # # for Album instances (called before defining associations in the class):
39
+ # Album.plugin :association_multi_add_remove
40
+ module AssociationMultiAddRemove
41
+ module ClassMethods
42
+ # Define the methods use to add/remove/set multiple associated objects
43
+ # in a single method call.
44
+ def def_association_instance_methods(opts)
45
+ super
46
+
47
+ if opts[:adder]
48
+ add_method = opts[:add_method]
49
+ multi_add_method = opts[:multi_add_method] || :"add_#{opts[:name]}"
50
+ multi_add_method = nil if add_method == multi_add_method
51
+ if multi_add_method
52
+ association_module_def(multi_add_method, opts) do |objs, *args|
53
+ db.transaction(:server=>@server){objs.map{|obj| send(add_method, obj, *args)}.compact}
54
+ end
55
+ end
56
+ end
57
+
58
+ if opts[:remover]
59
+ remove_method = opts[:remove_method]
60
+ multi_remove_method = opts[:multi_remove_method] || :"remove_#{opts[:name]}"
61
+ multi_remove_method = nil if remove_method == multi_remove_method
62
+ if multi_remove_method
63
+ association_module_def(multi_remove_method, opts) do |objs, *args|
64
+ db.transaction(:server=>@server){objs.map{|obj| send(remove_method, obj, *args)}.compact}
65
+ end
66
+ end
67
+ end
68
+
69
+ if multi_add_method && multi_remove_method
70
+ association_module_def(:"#{opts[:name]}=", opts) do |objs, *args|
71
+ db.transaction(:server=>@server) do
72
+ existing_objs = send(opts.association_method)
73
+ send(multi_remove_method, (existing_objs - objs), *args)
74
+ send(multi_add_method, (objs - existing_objs), *args)
75
+ nil
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -65,8 +65,6 @@ module Sequel
65
65
  # # Add CSV output capability to Album class instances
66
66
  # Album.plugin :csv_serializer
67
67
  module CsvSerializer
68
- CSV = Object.const_defined?(:CSV) ? ::CSV : ::FasterCSV
69
-
70
68
  # Set up the column readers to do deserialization and the column writers
71
69
  # to save the value in deserialized_values
72
70
  def self.configure(model, opts = OPTS)
@@ -75,13 +73,27 @@ module Sequel
75
73
  end
76
74
  end
77
75
 
76
+ # Avoid keyword argument separation warnings on Ruby 2.7, while still
77
+ # being compatible with 1.9.
78
+ if RUBY_VERSION >= "2.0"
79
+ instance_eval(<<-END, __FILE__, __LINE__+1)
80
+ def self.csv_call(*args, opts, &block)
81
+ CSV.send(*args, **opts, &block)
82
+ end
83
+ END
84
+ else
85
+ def self.csv_call(*args, opts, &block)
86
+ CSV.send(*args, opts, &block)
87
+ end
88
+ end
89
+
78
90
  module ClassMethods
79
91
  # The default opts to use when serializing model objects to CSV
80
92
  attr_reader :csv_serializer_opts
81
93
 
82
94
  # Attempt to parse an array of instances from the given CSV string
83
95
  def array_from_csv(csv, opts = OPTS)
84
- CSV.parse(csv, process_csv_serializer_opts(opts)).map do |row|
96
+ CsvSerializer.csv_call(:parse, csv, process_csv_serializer_opts(opts)).map do |row|
85
97
  row = row.to_hash
86
98
  row.delete(nil)
87
99
  new(row)
@@ -108,7 +120,8 @@ module Sequel
108
120
  opts_cols = opts.delete(:columns)
109
121
  opts_include = opts.delete(:include)
110
122
  opts_except = opts.delete(:except)
111
- opts[:headers] ||= Array(opts.delete(:only) || opts_cols || columns) + Array(opts_include) - Array(opts_except)
123
+ only = opts.delete(:only)
124
+ opts[:headers] ||= Array(only || opts_cols || columns) + Array(opts_include) - Array(opts_except)
112
125
  opts
113
126
  end
114
127
 
@@ -130,7 +143,7 @@ module Sequel
130
143
  # :headers :: The headers to use for the CSV line. Use nil for a header
131
144
  # to specify the column should be ignored.
132
145
  def from_csv(csv, opts = OPTS)
133
- row = CSV.parse_line(csv, model.process_csv_serializer_opts(opts)).to_hash
146
+ row = CsvSerializer.csv_call(:parse_line, csv, model.process_csv_serializer_opts(opts)).to_hash
134
147
  row.delete(nil)
135
148
  set(row)
136
149
  end
@@ -146,9 +159,10 @@ module Sequel
146
159
  # attributes to include in the CSV output.
147
160
  def to_csv(opts = OPTS)
148
161
  opts = model.process_csv_serializer_opts(opts)
162
+ headers = opts[:headers]
149
163
 
150
- CSV.generate(opts) do |csv|
151
- csv << opts[:headers].map{|k| public_send(k)}
164
+ CsvSerializer.csv_call(:generate, model.process_csv_serializer_opts(opts)) do |csv|
165
+ csv << headers.map{|k| public_send(k)}
152
166
  end
153
167
  end
154
168
  end
@@ -164,10 +178,11 @@ module Sequel
164
178
  def to_csv(opts = OPTS)
165
179
  opts = model.process_csv_serializer_opts({:columns=>columns}.merge!(opts))
166
180
  items = opts.delete(:array) || self
181
+ headers = opts[:headers]
167
182
 
168
- CSV.generate(opts) do |csv|
183
+ CsvSerializer.csv_call(:generate, opts) do |csv|
169
184
  items.each do |object|
170
- csv << opts[:headers].map{|header| object.public_send(header) }
185
+ csv << headers.map{|header| object.public_send(header)}
171
186
  end
172
187
  end
173
188
  end
@@ -107,12 +107,18 @@ module Sequel
107
107
  # previous row_proc, but calls set_server on the output of that row_proc,
108
108
  # ensuring that objects retrieved by a specific shard know which shard they
109
109
  # are tied to.
110
- def server(s)
111
- ds = super
112
- if rp = row_proc
113
- ds = ds.with_row_proc(proc{|r| rp.call(r).set_server(s)})
110
+ def row_proc
111
+ rp = super
112
+ if rp
113
+ case server = db.pool.send(:pick_server, opts[:server])
114
+ when nil, :default, :read_only
115
+ # nothing
116
+ else
117
+ old_rp = rp
118
+ rp = proc{|r| old_rp.call(r).set_server(server)}
119
+ end
114
120
  end
115
- ds
121
+ rp
116
122
  end
117
123
  end
118
124
  end
@@ -788,8 +788,10 @@ module Sequel
788
788
  def coerce(other)
789
789
  if other.is_a?(Numeric)
790
790
  [SQL::NumericExpression.new(:NOOP, other), self]
791
- else
791
+ elsif defined?(super)
792
792
  super
793
+ else
794
+ [self, other]
793
795
  end
794
796
  end
795
797
 
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 24
9
+ MINOR = 25
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
@@ -25,7 +25,7 @@ DB2 = Sequel.connect("#{CONN_PREFIX}#{BIN_SPEC_DB2}", :test=>false)
25
25
 
26
26
  ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
27
27
  gem 'minitest'
28
- require 'minitest/autorun'
28
+ require 'minitest/global_expectations/autorun'
29
29
 
30
30
  describe "bin/sequel" do
31
31
  def bin(opts={})
@@ -214,8 +214,13 @@ describe "Blockless Ruby Filters" do
214
214
  @d.lit(1 + Sequel.lit('?', :x)).must_equal '(1 + x)'
215
215
  end
216
216
 
217
- it "should raise a NoMethodError if coerce is called with a non-Numeric" do
218
- proc{Sequel.expr(:x).coerce(:a)}.must_raise NoMethodError
217
+ it "should not break Date/DateTime equality" do
218
+ (Date.today == Sequel.expr(:x)).must_equal false
219
+ (DateTime.now == Sequel.expr(:x)).must_equal false
220
+ end
221
+
222
+ it "should have coerce return array if called on a non-numeric" do
223
+ Sequel.expr(:x).coerce(:a).must_equal [Sequel.expr(:x), :a]
219
224
  end
220
225
 
221
226
  it "should support AND conditions via &" do
@@ -10,7 +10,7 @@ require_relative "../../lib/sequel/core"
10
10
 
11
11
  ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
12
12
  gem 'minitest'
13
- require 'minitest/autorun'
13
+ require 'minitest/global_expectations/autorun'
14
14
  require 'minitest/hooks/default'
15
15
  require 'minitest/shared_description'
16
16
 
@@ -16,7 +16,7 @@ Sequel.extension :virtual_row_method_block
16
16
 
17
17
  ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
18
18
  gem 'minitest'
19
- require 'minitest/autorun'
19
+ require 'minitest/global_expectations/autorun'
20
20
  require 'minitest/hooks/default'
21
21
 
22
22
  require_relative "deprecation_helper.rb"
@@ -0,0 +1,1041 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe "association_multi_add_remove plugin - one_to_many" do
4
+ before do
5
+ @c1 = Class.new(Sequel::Model(:attributes)) do
6
+ unrestrict_primary_key
7
+ columns :id, :node_id, :y, :z
8
+ end
9
+
10
+ @c2 = Class.new(Sequel::Model(:nodes)) do
11
+ plugin :association_multi_add_remove
12
+
13
+ def _refresh(ds); end
14
+ unrestrict_primary_key
15
+ attr_accessor :xxx
16
+
17
+ def self.name; 'Node'; end
18
+ def self.to_s; 'Node'; end
19
+
20
+ columns :id, :x
21
+ end
22
+ @dataset = @c2.dataset = @c2.dataset.with_fetch({})
23
+ @c1.dataset = @c1.dataset.with_fetch(proc { |sql| sql =~ /SELECT 1/ ? { a: 1 } : {} })
24
+ DB.reset
25
+ end
26
+
27
+ it "should define an add_*s method that works on existing records" do
28
+ @c2.one_to_many :attributes, class: @c1
29
+
30
+ n = @c2.load(id: 1234)
31
+ a1 = @c1.load(id: 2345)
32
+ a2 = @c1.load(id: 3456)
33
+ [a1, a2].must_equal n.add_attributes([a1, a2])
34
+ a1.values.must_equal(:node_id => 1234, id: 2345)
35
+ a2.values.must_equal(:node_id => 1234, id: 3456)
36
+ DB.sqls.must_equal [
37
+ 'BEGIN',
38
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 2345)',
39
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 3456)',
40
+ 'COMMIT'
41
+ ]
42
+ end
43
+
44
+ it "should not define add/remove methods with the same name as the ones defined by default " do
45
+ @c2.one_to_many :sheep, class: @c1, :key=>:node_id
46
+
47
+ n = @c2.load(id: 1234)
48
+ a1 = @c1.load(id: 2345)
49
+ a1.must_be_same_as n.add_sheep(a1)
50
+ a1.values.must_equal(:node_id => 1234, id: 2345)
51
+ DB.sqls.must_equal ['UPDATE attributes SET node_id = 1234 WHERE (id = 2345)']
52
+ a1.must_be_same_as n.remove_sheep(a1)
53
+ a1.values.must_equal(:node_id => nil, id: 2345)
54
+ DB.sqls.must_equal [
55
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 2345)) LIMIT 1",
56
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 2345)',
57
+ ]
58
+ n.respond_to?(:sheep=).must_equal false
59
+ end
60
+
61
+ it "should support :multi_add_method" do
62
+ @c2.one_to_many :attributes, class: @c1, :multi_add_method=>:add_multiple_attributes
63
+
64
+ n = @c2.load(id: 1234)
65
+ a1 = @c1.load(id: 2345)
66
+ a2 = @c1.load(id: 3456)
67
+ [a1, a2].must_equal n.add_multiple_attributes([a1, a2])
68
+ a1.values.must_equal(:node_id => 1234, id: 2345)
69
+ a2.values.must_equal(:node_id => 1234, id: 3456)
70
+ DB.sqls.must_equal [
71
+ 'BEGIN',
72
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 2345)',
73
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 3456)',
74
+ 'COMMIT'
75
+ ]
76
+ end
77
+
78
+ it "should define an add_*s method that works on new records" do
79
+ @c2.one_to_many :attributes, :class => @c1
80
+
81
+ n = @c2.load(:id => 1234)
82
+ a1 = @c1.new(:id => 234)
83
+ a2 = @c1.new(:id => 345)
84
+ @c1.dataset = @c1.dataset.with_fetch([
85
+ [{ :id=>234, :node_id=>1234 }], [{ :id=>345, :node_id=>1234 }]
86
+ ])
87
+ [a1, a2].must_equal n.add_attributes([a1, a2])
88
+ DB.sqls.must_equal [
89
+ 'BEGIN',
90
+ "INSERT INTO attributes (id, node_id) VALUES (234, 1234)",
91
+ "SELECT * FROM attributes WHERE id = 234",
92
+ "INSERT INTO attributes (id, node_id) VALUES (345, 1234)",
93
+ "SELECT * FROM attributes WHERE id = 345",
94
+ 'COMMIT'
95
+ ]
96
+ a1.values.must_equal(:node_id => 1234, :id => 234)
97
+ a2.values.must_equal(:node_id => 1234, :id => 345)
98
+ end
99
+
100
+ it "should define a remove_*s method that works on existing records" do
101
+ @c2.one_to_many :attributes, :class => @c1
102
+
103
+ n = @c2.load(:id => 1234)
104
+ a1 = @c1.load(:id => 2345, :node_id => 1234)
105
+ a2 = @c1.load(:id => 3456, :node_id => 1234)
106
+ [a1, a2].must_equal n.remove_attributes([a1, a2])
107
+ a1.values.must_equal(:node_id => nil, :id => 2345)
108
+ a2.values.must_equal(:node_id => nil, :id => 3456)
109
+ DB.sqls.must_equal [
110
+ 'BEGIN',
111
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 2345)) LIMIT 1",
112
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 2345)',
113
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 3456)) LIMIT 1",
114
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 3456)',
115
+ 'COMMIT'
116
+ ]
117
+ end
118
+
119
+ it "should support :multi_remove_method" do
120
+ @c2.one_to_many :attributes, :class => @c1, :multi_remove_method=>:remove_multiple_attributes
121
+
122
+ n = @c2.load(:id => 1234)
123
+ a1 = @c1.load(:id => 2345, :node_id => 1234)
124
+ a2 = @c1.load(:id => 3456, :node_id => 1234)
125
+ [a1, a2].must_equal n.remove_multiple_attributes([a1, a2])
126
+ a1.values.must_equal(:node_id => nil, :id => 2345)
127
+ a2.values.must_equal(:node_id => nil, :id => 3456)
128
+ DB.sqls.must_equal [
129
+ 'BEGIN',
130
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 2345)) LIMIT 1",
131
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 2345)',
132
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 3456)) LIMIT 1",
133
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 3456)',
134
+ 'COMMIT'
135
+ ]
136
+ end
137
+
138
+ it "should have the remove_*s method raise an error if the passed objects are not already associated" do
139
+ @c2.one_to_many :attributes, :class => @c1
140
+
141
+ n = @c2.new(:id => 1234)
142
+ a1 = @c1.load(:id => 2345, :node_id => 1234)
143
+ a2 = @c1.load(:id => 3456, :node_id => 1234)
144
+ @c1.dataset = @c1.dataset.with_fetch([])
145
+ proc{n.remove_attributes([a1, a2])}.must_raise(Sequel::Error)
146
+ DB.sqls.must_equal [
147
+ 'BEGIN',
148
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 2345)) LIMIT 1",
149
+ 'ROLLBACK'
150
+ ]
151
+ end
152
+
153
+ it "should accept hashes for the add_*s method and create a new records" do
154
+ @c2.one_to_many :attributes, :class => @c1
155
+ n = @c2.new(:id => 1234)
156
+ DB.reset
157
+ @c1.dataset = @c1.dataset.with_fetch([
158
+ [{ :node_id => 1234, :id => 234 }], [{ :node_id => 1234, :id => 345 }]
159
+ ])
160
+ n.add_attributes([{ :id => 234 }, { :id => 345 }]).must_equal [
161
+ @c1.load(:node_id => 1234, :id => 234),
162
+ @c1.load(:node_id => 1234, :id => 345)
163
+ ]
164
+ DB.sqls.must_equal [
165
+ 'BEGIN',
166
+ "INSERT INTO attributes (id, node_id) VALUES (234, 1234)",
167
+ "SELECT * FROM attributes WHERE id = 234",
168
+ "INSERT INTO attributes (id, node_id) VALUES (345, 1234)",
169
+ "SELECT * FROM attributes WHERE id = 345",
170
+ 'COMMIT'
171
+ ]
172
+ end
173
+
174
+ it "should accept primary keys for the add_*s method" do
175
+ @c2.one_to_many :attributes, :class => @c1
176
+ n = @c2.new(:id => 1234)
177
+ @c1.dataset = @c1.dataset.with_fetch([
178
+ [{ :node_id => nil, :id => 234 }], [{ :node_id => nil, :id => 345 }]
179
+ ])
180
+ n.add_attributes([234, 345]).must_equal [
181
+ @c1.load(:node_id => 1234, :id => 234),
182
+ @c1.load(:node_id => 1234, :id => 345)
183
+ ]
184
+ DB.sqls.must_equal [
185
+ 'BEGIN',
186
+ "SELECT * FROM attributes WHERE id = 234",
187
+ "UPDATE attributes SET node_id = 1234 WHERE (id = 234)",
188
+ "SELECT * FROM attributes WHERE id = 345",
189
+ "UPDATE attributes SET node_id = 1234 WHERE (id = 345)",
190
+ 'COMMIT'
191
+ ]
192
+ end
193
+
194
+ it "should raise an error if the primary key passed to the add_*s method does not match an existing record" do
195
+ @c2.one_to_many :attributes, :class => @c1
196
+ n = @c2.new(:id => 1234)
197
+ @c1.dataset = @c1.dataset.with_fetch([])
198
+ proc{n.add_attributes([234, 345])}.must_raise(Sequel::NoMatchingRow)
199
+ DB.sqls.must_equal [
200
+ 'BEGIN',
201
+ "SELECT * FROM attributes WHERE id = 234",
202
+ 'ROLLBACK'
203
+ ]
204
+ end
205
+
206
+ it "should raise an error in the add_*s method if the passed associated objects are not of the correct type" do
207
+ @c2.one_to_many :attributes, :class => @c1
208
+ proc{@c2.new(:id => 1234).add_attributes([@c2.new, @c2.new])}.must_raise(Sequel::Error)
209
+ end
210
+
211
+ it "should accept primary keys for the remove_*s method and remove existing records" do
212
+ @c2.one_to_many :attributes, :class => @c1
213
+ n = @c2.new(:id => 1234)
214
+ @c1.dataset = @c1.dataset.with_fetch([
215
+ [{ :id=>234, :node_id=>1234 }], [{ :id=>345, :node_id=>1234 }]
216
+ ])
217
+ n.remove_attributes([234, 345]).must_equal [
218
+ @c1.load(:node_id => nil, :id => 234),
219
+ @c1.load(:node_id => nil, :id => 345)
220
+ ]
221
+ DB.sqls.must_equal [
222
+ 'BEGIN',
223
+ 'SELECT * FROM attributes WHERE ((attributes.node_id = 1234) AND (attributes.id = 234)) LIMIT 1',
224
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 234)',
225
+ 'SELECT * FROM attributes WHERE ((attributes.node_id = 1234) AND (attributes.id = 345)) LIMIT 1',
226
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 345)',
227
+ 'COMMIT'
228
+ ]
229
+ end
230
+
231
+ it "should raise an error in the remove_*s method if the passed associated objects are not of the correct type" do
232
+ @c2.one_to_many :attributes, :class => @c1
233
+ proc{@c2.new(:id => 1234).remove_attributes([@c2.new, @c2.new])}.must_raise(Sequel::Error)
234
+ end
235
+
236
+ it "should have add_*s method respect the :primary_key option" do
237
+ @c2.one_to_many :attributes, :class => @c1, :primary_key=>:xxx
238
+
239
+ n = @c2.new(:id => 1234, :xxx=>5)
240
+ a1 = @c1.load(:id => 2345)
241
+ a2 = @c1.load(:id => 3456)
242
+ n.add_attributes([a1, a2]).must_equal [a1, a2]
243
+ DB.sqls.must_equal [
244
+ 'BEGIN',
245
+ 'UPDATE attributes SET node_id = 5 WHERE (id = 2345)',
246
+ 'UPDATE attributes SET node_id = 5 WHERE (id = 3456)',
247
+ 'COMMIT'
248
+ ]
249
+ end
250
+
251
+ it "should have add_*s method not add the same objects to the cached association array if the objects are already in the array" do
252
+ @c2.one_to_many :attributes, :class => @c1
253
+
254
+ n = @c2.new(:id => 1234)
255
+ a1 = @c1.load(:id => 2345)
256
+ a2 = @c1.load(:id => 3456)
257
+ n.associations[:attributes] = []
258
+ [a1, a2].must_equal n.add_attributes([a1, a2])
259
+ [a1, a2].must_equal n.add_attributes([a1, a2])
260
+ a1.values.must_equal(:node_id => 1234, :id => 2345)
261
+ a2.values.must_equal(:node_id => 1234, :id => 3456)
262
+ n.attributes.must_equal [a1, a2]
263
+ DB.sqls.must_equal [
264
+ 'BEGIN',
265
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 2345)',
266
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 3456)',
267
+ 'COMMIT'
268
+ ] * 2
269
+ end
270
+
271
+ it "should have add_*s method respect composite keys" do
272
+ @c2.one_to_many :attributes, :class => @c1, :key =>[:node_id, :y], :primary_key=>[:id, :x]
273
+
274
+ n = @c2.load(:id => 1234, :x=>5)
275
+ a1 = @c1.load(:id => 2345)
276
+ a2 = @c1.load(:id => 3456)
277
+ n.add_attributes([a1, a2]).must_equal [a1, a2]
278
+ DB.sqls.must_equal [
279
+ 'BEGIN',
280
+ "UPDATE attributes SET node_id = 1234, y = 5 WHERE (id = 2345)",
281
+ "UPDATE attributes SET node_id = 1234, y = 5 WHERE (id = 3456)",
282
+ 'COMMIT'
283
+ ]
284
+ end
285
+
286
+ it "should have add_*s method accept composite keys" do
287
+ @c1.dataset = @c1.dataset.with_fetch([
288
+ [{ :id=>2345, :node_id=>1234, :z=>8, :y=>5 }],
289
+ [{ :id=>3456, :node_id=>1234, :z=>9, :y=>5 }]
290
+ ])
291
+ @c1.set_primary_key [:id, :z]
292
+ @c2.one_to_many :attributes, :class => @c1, :key =>[:node_id, :y], :primary_key=>[:id, :x]
293
+
294
+ n = @c2.load(:id => 1234, :x=>5)
295
+ a1 = @c1.load(:id => 2345, :z => 8, :node_id => 1234, :y=>5)
296
+ a2 = @c1.load(:id => 3456, :z => 9, :node_id => 1234, :y=>5)
297
+ n.add_attributes([[2345, 8], [3456, 9]]).must_equal [a1, a2]
298
+ DB.sqls.must_equal [
299
+ 'BEGIN',
300
+ "SELECT * FROM attributes WHERE ((id = 2345) AND (z = 8)) LIMIT 1",
301
+ "UPDATE attributes SET node_id = 1234, y = 5 WHERE ((id = 2345) AND (z = 8))",
302
+ "SELECT * FROM attributes WHERE ((id = 3456) AND (z = 9)) LIMIT 1",
303
+ "UPDATE attributes SET node_id = 1234, y = 5 WHERE ((id = 3456) AND (z = 9))",
304
+ 'COMMIT'
305
+ ]
306
+ end
307
+
308
+ it "should have remove_*s method respect composite keys" do
309
+ @c2.one_to_many :attributes, :class => @c1, :key =>[:node_id, :y], :primary_key=>[:id, :x]
310
+
311
+ n = @c2.load(:id => 1234, :x=>5)
312
+ a1 = @c1.load(:id => 2345, :node_id=>1234, :y=>5)
313
+ a2 = @c1.load(:id => 3456, :node_id=>1234, :y=>5)
314
+ n.remove_attributes([a1, a2]).must_equal [a1, a2]
315
+ DB.sqls.must_equal [
316
+ 'BEGIN',
317
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (attributes.y = 5) AND (id = 2345)) LIMIT 1",
318
+ "UPDATE attributes SET node_id = NULL, y = NULL WHERE (id = 2345)",
319
+ "SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (attributes.y = 5) AND (id = 3456)) LIMIT 1",
320
+ "UPDATE attributes SET node_id = NULL, y = NULL WHERE (id = 3456)",
321
+ 'COMMIT'
322
+ ]
323
+ end
324
+
325
+ it "should accept a array of composite primary keys values for the remove_*s method and remove existing records" do
326
+ @c1.dataset = @c1.dataset.with_fetch([
327
+ [{ :id=>234, :node_id=>123, :y=>5 }], [{ :id=>345, :node_id=>123, :y=>6 }]
328
+ ])
329
+ @c1.set_primary_key [:id, :y]
330
+ @c2.one_to_many :attributes, :class => @c1, :key=>:node_id, :primary_key=>:id
331
+ n = @c2.new(:id => 123)
332
+ n.remove_attributes([[234, 5], [345, 6]]).must_equal [
333
+ @c1.load(:node_id => nil, :y => 5, :id => 234),
334
+ @c1.load(:node_id => nil, :y => 6, :id => 345)
335
+ ]
336
+ DB.sqls.must_equal [
337
+ 'BEGIN',
338
+ "SELECT * FROM attributes WHERE ((attributes.node_id = 123) AND (attributes.id = 234) AND (attributes.y = 5)) LIMIT 1",
339
+ "UPDATE attributes SET node_id = NULL WHERE ((id = 234) AND (y = 5))",
340
+ "SELECT * FROM attributes WHERE ((attributes.node_id = 123) AND (attributes.id = 345) AND (attributes.y = 6)) LIMIT 1",
341
+ "UPDATE attributes SET node_id = NULL WHERE ((id = 345) AND (y = 6))",
342
+ 'COMMIT'
343
+ ]
344
+ end
345
+
346
+ it "should raise an error in add_*s and remove_*s if the passed objects return false to save (are not valid)" do
347
+ @c2.one_to_many :attributes, :class => @c1
348
+ n = @c2.new(:id => 1234)
349
+ a1 = @c1.new(:id => 2345)
350
+ a2 = @c1.new(:id => 3456)
351
+ def a1.validate() errors.add(:id, 'foo') end
352
+ def a2.validate() errors.add(:id, 'bar') end
353
+ proc{n.add_attributes([a1, a2])}.must_raise(Sequel::ValidationFailed)
354
+ proc{n.remove_attributes([a1, a2])}.must_raise(Sequel::ValidationFailed)
355
+ end
356
+
357
+ it "should not validate the associated objects in add_*s and remove_*s if the :validate=>false option is used" do
358
+ @c2.one_to_many :attributes, :class => @c1, :validate=>false
359
+ n = @c2.new(:id => 1234)
360
+ a1 = @c1.new(:id => 2345)
361
+ a2 = @c1.new(:id => 3456)
362
+ def a1.validate() errors.add(:id, 'foo') end
363
+ def a2.validate() errors.add(:id, 'bar') end
364
+ n.add_attributes([a1, a2]).must_equal [a1, a2]
365
+ n.remove_attributes([a1, a2]).must_equal [a1, a2]
366
+ end
367
+
368
+ it "should not raise exception in add_*s and remove_*s if the :raise_on_save_failure=>false option is used" do
369
+ @c2.one_to_many :attributes, :class => @c1, :raise_on_save_failure=>false
370
+ n = @c2.new(:id => 1234)
371
+ a1 = @c1.new(:id => 2345)
372
+ a2 = @c1.new(:id => 3456)
373
+ def a1.validate() errors.add(:id, 'foo') end
374
+ def a2.validate() errors.add(:id, 'bar') end
375
+ n.associations[:attributes] = []
376
+ n.add_attributes([a1, a2]).must_equal []
377
+ n.associations[:attributes].must_equal []
378
+ n.remove_attributes([a1, a2]).must_equal []
379
+ n.associations[:attributes].must_equal []
380
+ end
381
+
382
+ it "should add item to cache if it exists when calling add_*s" do
383
+ @c2.one_to_many :attributes, :class => @c1
384
+ n = @c2.new(:id => 123)
385
+ a1 = @c1.load(:id => 234)
386
+ a2 = @c1.load(:id => 345)
387
+ arr = []
388
+ n.associations[:attributes] = arr
389
+ n.add_attributes([a1, a2])
390
+ arr.must_equal [a1, a2]
391
+ end
392
+
393
+ it "should set object to item's reciprocal cache when calling add_*s" do
394
+ @c2.one_to_many :attributes, :class => @c1
395
+ @c1.many_to_one :node, :class => @c2
396
+
397
+ n = @c2.new(:id => 123)
398
+ a1 = @c1.new(:id => 234)
399
+ a2 = @c1.new(:id => 345)
400
+ n.add_attributes([a1, a2])
401
+ a1.node.must_equal n
402
+ a2.node.must_equal n
403
+ end
404
+
405
+ it "should remove item from cache if it exists when calling remove_*s" do
406
+ @c2.one_to_many :attributes, :class => @c1
407
+
408
+ n = @c2.load(:id => 123)
409
+ a1 = @c1.load(:id => 234)
410
+ a2 = @c1.load(:id => 345)
411
+ arr = [a1, a2]
412
+ n.associations[:attributes] = arr
413
+ n.remove_attributes([a1, a2])
414
+ arr.must_equal []
415
+ end
416
+
417
+ it "should remove item's reciprocal cache calling remove_*s" do
418
+ @c2.one_to_many :attributes, :class => @c1
419
+ @c1.many_to_one :node, :class => @c2
420
+
421
+ n = @c2.new(:id => 123)
422
+ a1 = @c1.new(:id => 234)
423
+ a2 = @c1.new(:id => 345)
424
+ a1.associations[:node] = n
425
+ a2.associations[:node] = n
426
+ a1.node.must_equal n
427
+ a2.node.must_equal n
428
+ n.remove_attributes([a1, a2])
429
+ a1.node.must_be_nil
430
+ a2.node.must_be_nil
431
+ end
432
+
433
+ it "should not create the add_*s or remove_*s methods if :read_only option is used" do
434
+ @c2.one_to_many :attributes, :class => @c1, :read_only=>true
435
+ im = @c2.instance_methods
436
+ im.wont_include(:add_attributes)
437
+ im.wont_include(:remove_attributes)
438
+ end
439
+
440
+ it "should not add associations methods directly to class" do
441
+ @c2.one_to_many :attributes, :class => @c1
442
+ im = @c2.instance_methods
443
+ im.must_include(:add_attributes)
444
+ im.must_include(:remove_attributes)
445
+ im2 = @c2.instance_methods(false)
446
+ im2.wont_include(:add_attributes)
447
+ im2.wont_include(:remove_attributes)
448
+ end
449
+
450
+ it "should call an _add_ method internally to add attributes" do
451
+ @c2.one_to_many :attributes, :class => @c1
452
+ @c2.private_instance_methods.must_include(:_add_attribute)
453
+ p = @c2.load(:id=>10)
454
+ c1 = @c1.load(:id=>123)
455
+ c2 = @c1.load(:id=>234)
456
+ def p._add_attribute(x)
457
+ (@x ||= []) << x
458
+ end
459
+ def c1._node_id=; raise; end
460
+ def c2._node_id=; raise; end
461
+ p.add_attributes([c1, c2])
462
+ p.instance_variable_get(:@x).must_equal [c1, c2]
463
+ end
464
+
465
+ it "should allow additional arguments given to the add_*s method and pass them onwards to the _add_ method" do
466
+ @c2.one_to_many :attributes, :class => @c1
467
+ p = @c2.load(:id=>10)
468
+ c1 = @c1.load(:id=>123)
469
+ c2 = @c1.load(:id=>234)
470
+ def p._add_attribute(x,*y)
471
+ (@x ||= []) << x
472
+ (@y ||= []) << y
473
+ end
474
+ def c1._node_id=; raise; end
475
+ def c2._node_id=; raise; end
476
+ p.add_attributes([c1, c2], :foo, :bar=>:baz)
477
+ p.instance_variable_get(:@x).must_equal [c1, c2]
478
+ p.instance_variable_get(:@y).must_equal [
479
+ [:foo,{:bar=>:baz}], [:foo,{:bar=>:baz}]
480
+ ]
481
+ end
482
+
483
+ it "should call a _remove_ method internally to remove attributes" do
484
+ @c2.one_to_many :attributes, :class => @c1
485
+ @c2.private_instance_methods.must_include(:_remove_attribute)
486
+ p = @c2.load(:id=>10)
487
+ c1 = @c1.load(:id=>123)
488
+ c2 = @c1.load(:id=>234)
489
+ def p._remove_attribute(x)
490
+ (@x ||= []) << x
491
+ end
492
+ def c1._node_id=; raise; end
493
+ def c2._node_id=; raise; end
494
+ p.remove_attributes([c1, c2])
495
+ p.instance_variable_get(:@x).must_equal [c1, c2]
496
+ end
497
+
498
+ it "should allow additional arguments given to the remove_*s method and pass them onwards to the _remove_ method" do
499
+ @c2.one_to_many :attributes, :class => @c1, :reciprocal=>nil
500
+ p = @c2.load(:id=>10)
501
+ c1 = @c1.load(:id=>123)
502
+ c2 = @c1.load(:id=>234)
503
+ def p._remove_attribute(x,*y)
504
+ (@x ||= []) << x
505
+ (@y ||= []) << y
506
+ end
507
+ def c1._node_id=; raise; end
508
+ def c2._node_id=; raise; end
509
+ p.remove_attributes([c1, c2], :foo, :bar=>:baz)
510
+ p.instance_variable_get(:@x).must_equal [c1, c2]
511
+ p.instance_variable_get(:@y).must_equal [
512
+ [:foo,{:bar=>:baz}], [:foo,{:bar=>:baz}]
513
+ ]
514
+ end
515
+
516
+ it "should support (before|after)_(add|remove) callbacks for (add|remove)_*s methods" do
517
+ h = []
518
+ @c2.one_to_many :attributes, :class => @c1, :before_add=>[proc{|x,y| h << x.pk; h << -y.pk}, :blah], :after_add=>proc{h << 3}, :before_remove=>:blah, :after_remove=>[:blahr]
519
+ @c2.class_eval do
520
+ self::Foo = h
521
+ def _add_attribute(v)
522
+ model::Foo << 4
523
+ end
524
+ def _remove_attribute(v)
525
+ model::Foo << 5
526
+ end
527
+ def blah(x)
528
+ model::Foo << x.pk
529
+ end
530
+ def blahr(x)
531
+ model::Foo << 6
532
+ end
533
+ end
534
+ p = @c2.load(:id=>10)
535
+ c1 = @c1.load(:id=>123)
536
+ c2 = @c1.load(:id=>234)
537
+ h.must_equal []
538
+ p.add_attributes([c1, c2])
539
+ h.must_equal [
540
+ 10, -123, 123, 4, 3,
541
+ 10, -234, 234, 4, 3
542
+ ]
543
+ p.remove_attributes([c1, c2])
544
+ h.must_equal [
545
+ 10, -123, 123, 4, 3,
546
+ 10, -234, 234, 4, 3,
547
+ 123, 5, 6,
548
+ 234, 5, 6
549
+ ]
550
+ end
551
+
552
+ it "should raise error and not call internal add_*s or remove_*s method if before callback calls cancel_action if raise_on_save_failure is true" do
553
+ p = @c2.load(:id=>10)
554
+ c1 = @c1.load(:id=>123)
555
+ c2 = @c1.load(:id=>234)
556
+ @c2.one_to_many :attributes, :class => @c1, :before_add=>:ba, :before_remove=>:br
557
+ def p.ba(o); cancel_action; end
558
+ def p._add_attribute; raise; end
559
+ def p._remove_attribute; raise; end
560
+ p.associations[:attributes] = []
561
+ proc{p.add_attributes([c1, c2])}.must_raise(Sequel::HookFailed)
562
+ p.attributes.must_equal []
563
+ p.associations[:attributes] = [c1, c2]
564
+ def p.br(o); cancel_action; end
565
+ proc{p.remove_attributes([c1, c2])}.must_raise(Sequel::HookFailed)
566
+ p.attributes.must_equal [c1, c2]
567
+ end
568
+
569
+ it "should return nil and not call internal add_*s or remove_*s method if before callback calls cancel_action if raise_on_save_failure is false" do
570
+ p = @c2.load(:id=>10)
571
+ c1 = @c1.load(:id=>123)
572
+ c2 = @c1.load(:id=>234)
573
+ p.raise_on_save_failure = false
574
+ @c2.one_to_many :attributes, :class => @c1, :before_add=>:ba, :before_remove=>:br
575
+ def p.ba(o); cancel_action; end
576
+ def p._add_attribute; raise; end
577
+ def p._remove_attribute; raise; end
578
+ p.associations[:attributes] = []
579
+ p.add_attributes([c1, c2]).must_equal []
580
+ p.attributes.must_equal []
581
+ p.associations[:attributes] = [c1, c2]
582
+ def p.br(o); cancel_action; end
583
+ p.remove_attributes([c1, c2]).must_equal []
584
+ p.attributes.must_equal [c1, c2]
585
+ end
586
+
587
+ it "should define a setter that works on existing records" do
588
+ @c2.one_to_many :attributes, class: @c1
589
+
590
+ n = @c2.load(id: 1234)
591
+ a1 = @c1.load(id: 2345, node_id: 1234)
592
+ a2 = @c1.load(id: 3456, node_id: 1234)
593
+ a3 = @c1.load(id: 4567)
594
+
595
+ n.associations[:attributes] = [a1, a2]
596
+
597
+ [a2, a3].must_equal(n.attributes = [a2, a3])
598
+ a1.values.must_equal(node_id: nil, id: 2345)
599
+ a2.values.must_equal(node_id: 1234, id: 3456)
600
+ a3.values.must_equal(node_id: 1234, id: 4567)
601
+ DB.sqls.must_equal [
602
+ 'BEGIN',
603
+ 'SELECT 1 AS one FROM attributes WHERE ((attributes.node_id = 1234) AND (id = 2345)) LIMIT 1',
604
+ 'UPDATE attributes SET node_id = NULL WHERE (id = 2345)',
605
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 4567)',
606
+ 'COMMIT'
607
+ ]
608
+ end
609
+ end
610
+
611
+ describe "association_multi_add_remove plugin - many_to_many" do
612
+ before do
613
+ @c1 = Class.new(Sequel::Model(:attributes)) do
614
+ unrestrict_primary_key
615
+ attr_accessor :yyy
616
+ def self.name; 'Attribute'; end
617
+ def self.to_s; 'Attribute'; end
618
+ columns :id, :y, :z
619
+ end
620
+
621
+ @c2 = Class.new(Sequel::Model(:nodes)) do
622
+ unrestrict_primary_key
623
+
624
+ plugin :association_multi_add_remove
625
+
626
+ attr_accessor :xxx
627
+
628
+ def self.name; 'Node'; end
629
+ def self.to_s; 'Node'; end
630
+ columns :id, :x
631
+ end
632
+ @dataset = @c2.dataset
633
+ @c1.dataset = @c1.dataset.with_autoid(1)
634
+
635
+ [@c1, @c2].each{|c| c.dataset = c.dataset.with_fetch({})}
636
+ DB.reset
637
+ end
638
+
639
+ it "should define an add_*s method that works on existing records" do
640
+ @c2.many_to_many :attributes, :class => @c1
641
+
642
+ n = @c2.load(:id => 1234)
643
+ a1 = @c1.load(:id => 2345)
644
+ a2 = @c1.load(:id => 3456)
645
+ n.add_attributes([a1, a2]).must_equal [a1, a2]
646
+ DB.sqls.must_equal [
647
+ 'BEGIN',
648
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 2345)",
649
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 3456)",
650
+ 'COMMIT'
651
+ ]
652
+ end
653
+
654
+ it "should define an add_*s method that works with a primary key" do
655
+ @c2.many_to_many :attributes, :class => @c1
656
+
657
+ n = @c2.load(:id => 1234)
658
+ a1 = @c1.load(:id => 2345)
659
+ a2 = @c1.load(:id => 3456)
660
+ @c1.dataset = @c1.dataset.with_fetch([[{ :id=>2345 }], [{ :id=>3456 }]])
661
+ n.add_attributes([2345, 3456]).must_equal [a1, a2]
662
+ DB.sqls.must_equal [
663
+ 'BEGIN',
664
+ "SELECT * FROM attributes WHERE id = 2345",
665
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 2345)",
666
+ "SELECT * FROM attributes WHERE id = 3456",
667
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 3456)",
668
+ 'COMMIT'
669
+ ]
670
+ end
671
+
672
+ it "should allow passing hashes to the add_*s method which creates new records" do
673
+ @c2.many_to_many :attributes, :class => @c1
674
+
675
+ n = @c2.load(:id => 1234)
676
+ @c1.dataset = @c1.dataset.with_fetch([[{ :id=>1 }], [{ :id=>2 }]])
677
+ n.add_attributes([{ :id => 1 }, { :id => 2 }]).must_equal [
678
+ @c1.load(:id => 1), @c1.load(:id => 2)
679
+ ]
680
+ DB.sqls.must_equal [
681
+ 'BEGIN',
682
+ 'INSERT INTO attributes (id) VALUES (1)',
683
+ "SELECT * FROM attributes WHERE id = 1",
684
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 1)",
685
+ 'INSERT INTO attributes (id) VALUES (2)',
686
+ "SELECT * FROM attributes WHERE id = 2",
687
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 2)",
688
+ 'COMMIT'
689
+ ]
690
+ end
691
+
692
+ it "should define a remove_*s method that works on existing records" do
693
+ @c2.many_to_many :attributes, :class => @c1
694
+
695
+ n = @c2.new(:id => 1234)
696
+ a1 = @c1.new(:id => 2345)
697
+ a2 = @c1.new(:id => 3456)
698
+ n.remove_attributes([a1, a2]).must_equal [a1, a2]
699
+ DB.sqls.must_equal [
700
+ 'BEGIN',
701
+ 'DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 2345))',
702
+ 'DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 3456))',
703
+ 'COMMIT'
704
+ ]
705
+ end
706
+
707
+ it "should accept primary keys for the remove_*s method and remove existing records" do
708
+ @c2.many_to_many :attributes, :class => @c1
709
+ n = @c2.new(:id => 1234)
710
+ @c1.dataset = @c1.dataset.with_fetch([[{ :id=>234 }], [{ :id=>345 }]])
711
+ n.remove_attributes([234, 345]).must_equal [
712
+ @c1.load(:id => 234), @c1.load(:id => 345)
713
+ ]
714
+ DB.sqls.must_equal [
715
+ 'BEGIN',
716
+ "SELECT attributes.* FROM attributes INNER JOIN attributes_nodes ON (attributes_nodes.attribute_id = attributes.id) WHERE ((attributes_nodes.node_id = 1234) AND (attributes.id = 234)) LIMIT 1",
717
+ "DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 234))",
718
+ "SELECT attributes.* FROM attributes INNER JOIN attributes_nodes ON (attributes_nodes.attribute_id = attributes.id) WHERE ((attributes_nodes.node_id = 1234) AND (attributes.id = 345)) LIMIT 1",
719
+ "DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 345))",
720
+ 'COMMIT'
721
+ ]
722
+ end
723
+
724
+ it "should have the add_*s method respect the :left_primary_key and :right_primary_key options" do
725
+ @c2.many_to_many :attributes, :class => @c1, :left_primary_key=>:xxx, :right_primary_key=>:yyy
726
+
727
+ n = @c2.load(:id => 1234).set(:xxx=>5)
728
+ a1 = @c1.load(:id => 2345).set(:yyy=>8)
729
+ a2 = @c1.load(:id => 3456).set(:yyy=>9)
730
+ n.add_attributes([a1, a2]).must_equal [a1, a2]
731
+ DB.sqls.must_equal [
732
+ 'BEGIN',
733
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (5, 8)",
734
+ "INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (5, 9)",
735
+ 'COMMIT'
736
+ ]
737
+ end
738
+
739
+ it "should have the add_*s method respect composite keys" do
740
+ @c2.many_to_many :attributes, :class => @c1, :left_key=>[:l1, :l2], :right_key=>[:r1, :r2], :left_primary_key=>[:id, :x], :right_primary_key=>[:id, :z]
741
+ @c1.dataset = @c1.dataset.with_fetch([
742
+ [{ :id=>2345, :z=>8 }], [{ :id=>3456, :z=>9 }]
743
+ ])
744
+ @c1.set_primary_key [:id, :z]
745
+ n = @c2.load(:id => 1234, :x=>5)
746
+ a1 = @c1.load(:id => 2345, :z=>8)
747
+ a2 = @c1.load(:id => 3456, :z=>9)
748
+ n.add_attributes([[2345, 8], [3456, 9]]).must_equal [a1, a2]
749
+ DB.sqls.must_equal [
750
+ 'BEGIN',
751
+ "SELECT * FROM attributes WHERE ((id = 2345) AND (z = 8)) LIMIT 1",
752
+ "INSERT INTO attributes_nodes (l1, l2, r1, r2) VALUES (1234, 5, 2345, 8)",
753
+ "SELECT * FROM attributes WHERE ((id = 3456) AND (z = 9)) LIMIT 1",
754
+ "INSERT INTO attributes_nodes (l1, l2, r1, r2) VALUES (1234, 5, 3456, 9)",
755
+ 'COMMIT'
756
+ ]
757
+ end
758
+
759
+ it "should have the remove_*s method respect the :left_primary_key and :right_primary_key options" do
760
+ @c2.many_to_many :attributes, :class => @c1, :left_primary_key=>:xxx, :right_primary_key=>:yyy
761
+
762
+ n = @c2.new(:id => 1234, :xxx=>5)
763
+ a1 = @c1.new(:id => 2345, :yyy=>8)
764
+ a2 = @c1.new(:id => 3456, :yyy=>9)
765
+ n.remove_attributes([a1, a2]).must_equal [a1, a2]
766
+ DB.sqls.must_equal [
767
+ 'BEGIN',
768
+ 'DELETE FROM attributes_nodes WHERE ((node_id = 5) AND (attribute_id = 8))',
769
+ 'DELETE FROM attributes_nodes WHERE ((node_id = 5) AND (attribute_id = 9))',
770
+ 'COMMIT'
771
+ ]
772
+ end
773
+
774
+ it "should have the remove_*s method respect composite keys" do
775
+ @c2.many_to_many :attributes, :class => @c1, :left_key=>[:l1, :l2], :right_key=>[:r1, :r2], :left_primary_key=>[:id, :x], :right_primary_key=>[:id, :z]
776
+ n = @c2.load(:id => 1234, :x=>5)
777
+ a1 = @c1.load(:id => 2345, :z=>8)
778
+ a2 = @c1.load(:id => 3456, :z=>9)
779
+ [a1, a2].must_equal n.remove_attributes([a1, a2])
780
+ DB.sqls.must_equal [
781
+ 'BEGIN',
782
+ "DELETE FROM attributes_nodes WHERE ((l1 = 1234) AND (l2 = 5) AND (r1 = 2345) AND (r2 = 8))",
783
+ "DELETE FROM attributes_nodes WHERE ((l1 = 1234) AND (l2 = 5) AND (r1 = 3456) AND (r2 = 9))",
784
+ 'COMMIT'
785
+ ]
786
+ end
787
+
788
+ it "should accept an array of arrays of composite primary key values for the remove_*s method and remove existing records" do
789
+ @c1.dataset = @c1.dataset.with_fetch([
790
+ [{ :id=>234, :y=>8 }], [{ :id=>345, :y=>9 }]
791
+ ])
792
+ @c1.set_primary_key [:id, :y]
793
+ @c2.many_to_many :attributes, :class => @c1
794
+ n = @c2.new(:id => 1234)
795
+ n.remove_attributes([[234, 8], [345, 9]]).must_equal [
796
+ @c1.load(:id => 234, :y=>8), @c1.load(:id => 345, :y=>9)
797
+ ]
798
+ DB.sqls.must_equal [
799
+ 'BEGIN',
800
+ "SELECT attributes.* FROM attributes INNER JOIN attributes_nodes ON (attributes_nodes.attribute_id = attributes.id) WHERE ((attributes_nodes.node_id = 1234) AND (attributes.id = 234) AND (attributes.y = 8)) LIMIT 1",
801
+ "DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 234))",
802
+ "SELECT attributes.* FROM attributes INNER JOIN attributes_nodes ON (attributes_nodes.attribute_id = attributes.id) WHERE ((attributes_nodes.node_id = 1234) AND (attributes.id = 345) AND (attributes.y = 9)) LIMIT 1",
803
+ "DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 345))",
804
+ 'COMMIT'
805
+ ]
806
+ end
807
+
808
+ it "should raise an error if trying to remove model objects that don't have valid primary keys" do
809
+ @c2.many_to_many :attributes, :class => @c1
810
+ n = @c2.new
811
+ a1 = @c1.load(:id=>123)
812
+ a2 = @c1.load(:id=>234)
813
+ proc { n.remove_attributes([a1, a2]) }.must_raise(Sequel::Error)
814
+ end
815
+
816
+ it "should remove items from cache if they exist when calling remove_*s" do
817
+ @c2.many_to_many :attributes, :class => @c1
818
+
819
+ n = @c2.new(:id => 1234)
820
+ a1 = @c1.load(:id => 345)
821
+ a2 = @c1.load(:id => 456)
822
+ arr = [a1, a2]
823
+ n.associations[:attributes] = arr
824
+ n.remove_attributes([a1, a2])
825
+ arr.must_equal []
826
+ end
827
+
828
+ it "should remove items from reciprocal's if they exist when calling remove_*s" do
829
+ @c2.many_to_many :attributes, :class => @c1
830
+ @c1.many_to_many :nodes, :class => @c2
831
+
832
+ n = @c2.new(:id => 1234)
833
+ a1 = @c1.new(:id => 345)
834
+ a2 = @c1.new(:id => 456)
835
+ a1.associations[:nodes] = [n]
836
+ a2.associations[:nodes] = [n]
837
+ n.remove_attributes([a1, a2])
838
+ a1.nodes.must_equal []
839
+ a2.nodes.must_equal []
840
+ end
841
+
842
+ it "should not create the add_*s or remove_*s methods if :read_only option is used" do
843
+ @c2.many_to_many :attributes, :class => @c1, :read_only=>true
844
+ im = @c2.instance_methods
845
+ im.wont_include(:add_attributes)
846
+ im.wont_include(:remove_attributes)
847
+ end
848
+
849
+ it "should not add associations methods directly to class" do
850
+ @c2.many_to_many :attributes, :class => @c1
851
+ im = @c2.instance_methods
852
+ im.must_include(:add_attributes)
853
+ im.must_include(:remove_attributes)
854
+ im2 = @c2.instance_methods(false)
855
+ im2.wont_include(:add_attributes)
856
+ im2.wont_include(:remove_attributes)
857
+ end
858
+
859
+ it "should call a _remove_*s method internally to remove attributes" do
860
+ @c2.many_to_many :attributes, :class => @c1
861
+ @c2.private_instance_methods.must_include(:_remove_attribute)
862
+ p = @c2.load(:id=>10)
863
+ c1 = @c1.load(:id=>123)
864
+ c2 = @c1.load(:id=>234)
865
+ def p._remove_attribute(x)
866
+ (@x ||= []) << x
867
+ end
868
+ p.remove_attributes([c1, c2])
869
+ p.instance_variable_get(:@x).must_equal [c1, c2]
870
+ DB.sqls.must_equal ['BEGIN', 'COMMIT']
871
+ end
872
+
873
+ it "should support a :remover option for defining the _remove_*s method" do
874
+ @c2.many_to_many :attributes, :class => @c1,
875
+ :remover=>proc { |x| (@x ||= []) << x }
876
+ p = @c2.load(:id=>10)
877
+ c1 = @c1.load(:id=>123)
878
+ c2 = @c1.load(:id=>234)
879
+ p.remove_attributes([c1, c2])
880
+ p.instance_variable_get(:@x).must_equal [c1, c2]
881
+ DB.sqls.must_equal ['BEGIN', 'COMMIT']
882
+ end
883
+
884
+ it "should allow additional arguments given to the remove_*s method and pass them onwards to the _remove_ method" do
885
+ @c2.many_to_many :attributes, :class => @c1
886
+ p = @c2.load(:id=>10)
887
+ c1 = @c1.load(:id=>123)
888
+ c2 = @c1.load(:id=>234)
889
+ def p._remove_attribute(x,*y)
890
+ (@x ||= []) << x
891
+ (@y ||= []) << y
892
+ end
893
+ p.remove_attributes([c1, c2], :foo, :bar=>:baz)
894
+ p.instance_variable_get(:@x).must_equal [c1, c2]
895
+ p.instance_variable_get(:@y).must_equal [
896
+ [:foo, { :bar=>:baz }], [:foo, { :bar=>:baz }]
897
+ ]
898
+ end
899
+
900
+ it "should raise an error in the remove_*s method if the passed associated objects are not of the correct type" do
901
+ @c2.many_to_many :attributes, :class => @c1
902
+ proc do
903
+ @c2.new(:id => 1234).remove_attributes([@c2.new, @c2.new])
904
+ end
905
+ .must_raise(Sequel::Error)
906
+ end
907
+
908
+ it "should support (before|after)_(add|remove) callbacks for (add|remove)_* methods" do
909
+ h = []
910
+ @c2.many_to_many :attributes, :class => @c1, :before_add=>[proc{|x,y| h << x.pk; h << -y.pk}, :blah], :after_add=>proc{h << 3}, :before_remove=>:blah, :after_remove=>[:blahr]
911
+ @c2.class_eval do
912
+ self::Foo = h
913
+ def _add_attribute(v)
914
+ model::Foo << 4
915
+ end
916
+ def _remove_attribute(v)
917
+ model::Foo << 5
918
+ end
919
+ def blah(x)
920
+ model::Foo << x.pk
921
+ end
922
+ def blahr(x)
923
+ model::Foo << 6
924
+ end
925
+ end
926
+ p = @c2.load(:id=>10)
927
+ c1 = @c1.load(:id=>123)
928
+ c2 = @c1.load(:id=>234)
929
+ h.must_equal []
930
+ p.add_attributes([c1, c2])
931
+ h.must_equal [
932
+ 10, -123, 123, 4, 3,
933
+ 10, -234, 234, 4, 3
934
+ ]
935
+ p.remove_attributes([c1, c2])
936
+ h.must_equal [
937
+ 10, -123, 123, 4, 3,
938
+ 10, -234, 234, 4, 3,
939
+ 123, 5, 6,
940
+ 234, 5, 6
941
+ ]
942
+ end
943
+
944
+ it "should raise error and not call internal add_*s or remove_*s method if before callback calls cancel_action if raise_on_save_failure is true" do
945
+ p = @c2.load(:id=>10)
946
+ c1 = @c1.load(:id=>123)
947
+ c2 = @c1.load(:id=>234)
948
+ @c2.many_to_many :attributes, :class => @c1, :before_add=>:ba, :before_remove=>:br
949
+ def p.ba(o) cancel_action end
950
+ def p._add_attribute; raise; end
951
+ def p._remove_attribute; raise; end
952
+ p.associations[:attributes] = []
953
+ p.raise_on_save_failure = true
954
+ proc{p.add_attributes([c1, c2])}.must_raise(Sequel::HookFailed)
955
+ p.attributes.must_equal []
956
+ p.associations[:attributes] = [c1, c2]
957
+ def p.br(o) cancel_action end
958
+ proc { p.remove_attributes([c1, c2]) }.must_raise(Sequel::HookFailed)
959
+ p.attributes.must_equal [c1, c2]
960
+ end
961
+
962
+ it "should return nil and not call internal add_*s or remove_*s method if before callback calls cancel_action if raise_on_save_failure is false" do
963
+ p = @c2.load(:id=>10)
964
+ c1 = @c1.load(:id=>123)
965
+ c2 = @c1.load(:id=>234)
966
+ p.raise_on_save_failure = false
967
+ @c2.many_to_many :attributes, :class => @c1, :before_add=>:ba, :before_remove=>:br
968
+ def p.ba(o) cancel_action end
969
+ def p._add_attribute; raise; end
970
+ def p._remove_attribute; raise; end
971
+ p.associations[:attributes] = []
972
+ p.add_attributes([c1, c2]).must_equal []
973
+ p.attributes.must_equal []
974
+ p.associations[:attributes] = [c1, c2]
975
+ def p.br(o) cancel_action end
976
+ p.remove_attributes([c1, c2]).must_equal []
977
+ p.attributes.must_equal [c1, c2]
978
+ end
979
+
980
+ it "should define a setter that works on existing records" do
981
+ @c2.many_to_many :attributes, class: @c1
982
+
983
+ n = @c2.load(id: 1234)
984
+ a1 = @c1.load(id: 2345)
985
+ a2 = @c1.load(id: 3456)
986
+ a3 = @c1.load(id: 4567)
987
+
988
+ n.associations[:attributes] = [a1, a2]
989
+
990
+ [a2, a3].must_equal(n.attributes = [a2, a3])
991
+ DB.sqls.must_equal [
992
+ 'BEGIN',
993
+ 'DELETE FROM attributes_nodes WHERE ((node_id = 1234) AND (attribute_id = 2345))',
994
+ 'INSERT INTO attributes_nodes (node_id, attribute_id) VALUES (1234, 4567)',
995
+ 'COMMIT'
996
+ ]
997
+ end
998
+ end
999
+
1000
+ describe "association_multi_add_remove plugin - sharding" do
1001
+ before do
1002
+ @db = Sequel.mock(:servers=>{:a=>{}}, :numrows=>1)
1003
+ @c1 = Class.new(Sequel::Model(@db[:attributes])) do
1004
+ unrestrict_primary_key
1005
+ columns :id, :node_id, :y, :z
1006
+ end
1007
+
1008
+ @c2 = Class.new(Sequel::Model(@db[:nodes])) do
1009
+ plugin :association_multi_add_remove
1010
+
1011
+ def _refresh(ds); end
1012
+ unrestrict_primary_key
1013
+ attr_accessor :xxx
1014
+
1015
+ def self.name; 'Node'; end
1016
+ def self.to_s; 'Node'; end
1017
+
1018
+ columns :id, :x
1019
+ end
1020
+ @dataset = @c2.dataset = @c2.dataset.with_fetch({})
1021
+ @c1.dataset = @c1.dataset.with_fetch(proc { |sql| sql =~ /SELECT 1/ ? { a: 1 } : {} })
1022
+ @db.sqls
1023
+ end
1024
+
1025
+ it "should handle servers correctly" do
1026
+ @c2.one_to_many :attributes, class: @c1
1027
+
1028
+ n = @c2.load(id: 1234).set_server(:a)
1029
+ a1 = @c1.load(id: 2345).set_server(:a)
1030
+ a2 = @c1.load(id: 3456).set_server(:a)
1031
+ [a1, a2].must_equal n.add_attributes([a1, a2])
1032
+ a1.values.must_equal(:node_id => 1234, id: 2345)
1033
+ a2.values.must_equal(:node_id => 1234, id: 3456)
1034
+ @db.sqls.must_equal [
1035
+ 'BEGIN -- a',
1036
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 2345) -- a',
1037
+ 'UPDATE attributes SET node_id = 1234 WHERE (id = 3456) -- a',
1038
+ 'COMMIT -- a'
1039
+ ]
1040
+ end
1041
+ end