sequel 5.24.0 → 5.29.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +58 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/doc/cheat_sheet.rdoc +1 -0
  6. data/doc/postgresql.rdoc +2 -2
  7. data/doc/release_notes/5.25.0.txt +32 -0
  8. data/doc/release_notes/5.26.0.txt +35 -0
  9. data/doc/release_notes/5.27.0.txt +21 -0
  10. data/doc/release_notes/5.28.0.txt +16 -0
  11. data/doc/release_notes/5.29.0.txt +22 -0
  12. data/doc/testing.rdoc +11 -6
  13. data/lib/sequel/adapters/jdbc/postgresql.rb +6 -0
  14. data/lib/sequel/adapters/postgres.rb +5 -1
  15. data/lib/sequel/adapters/shared/mssql.rb +4 -2
  16. data/lib/sequel/adapters/shared/mysql.rb +1 -1
  17. data/lib/sequel/adapters/shared/postgres.rb +15 -0
  18. data/lib/sequel/adapters/shared/sqlite.rb +7 -2
  19. data/lib/sequel/adapters/tinytds.rb +1 -1
  20. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
  21. data/lib/sequel/database/schema_generator.rb +1 -1
  22. data/lib/sequel/database/transactions.rb +3 -3
  23. data/lib/sequel/dataset/features.rb +6 -0
  24. data/lib/sequel/dataset/misc.rb +2 -2
  25. data/lib/sequel/dataset/query.rb +15 -2
  26. data/lib/sequel/dataset/sql.rb +17 -4
  27. data/lib/sequel/extensions/any_not_empty.rb +45 -0
  28. data/lib/sequel/extensions/exclude_or_null.rb +68 -0
  29. data/lib/sequel/extensions/pg_array_ops.rb +10 -6
  30. data/lib/sequel/extensions/pg_enum.rb +4 -1
  31. data/lib/sequel/extensions/pg_json.rb +1 -1
  32. data/lib/sequel/extensions/pg_json_ops.rb +124 -0
  33. data/lib/sequel/extensions/pg_range.rb +9 -0
  34. data/lib/sequel/extensions/sql_comments.rb +2 -2
  35. data/lib/sequel/model/base.rb +12 -5
  36. data/lib/sequel/plugins/association_multi_add_remove.rb +83 -0
  37. data/lib/sequel/plugins/caching.rb +3 -0
  38. data/lib/sequel/plugins/csv_serializer.rb +26 -9
  39. data/lib/sequel/plugins/dirty.rb +3 -9
  40. data/lib/sequel/plugins/empty_failure_backtraces.rb +38 -0
  41. data/lib/sequel/plugins/json_serializer.rb +15 -4
  42. data/lib/sequel/plugins/nested_attributes.rb +7 -0
  43. data/lib/sequel/plugins/sharding.rb +11 -5
  44. data/lib/sequel/plugins/throw_failures.rb +1 -1
  45. data/lib/sequel/plugins/typecast_on_load.rb +3 -2
  46. data/lib/sequel/sql.rb +4 -1
  47. data/lib/sequel/version.rb +1 -1
  48. data/spec/adapters/postgres_spec.rb +82 -17
  49. data/spec/adapters/sqlite_spec.rb +1 -1
  50. data/spec/bin_spec.rb +1 -1
  51. data/spec/core/database_spec.rb +1 -1
  52. data/spec/core/dataset_spec.rb +0 -3
  53. data/spec/core/expression_filters_spec.rb +26 -7
  54. data/spec/core/spec_helper.rb +1 -1
  55. data/spec/core_extensions_spec.rb +1 -1
  56. data/spec/extensions/any_not_empty_spec.rb +23 -0
  57. data/spec/extensions/association_multi_add_remove_spec.rb +1041 -0
  58. data/spec/extensions/caller_logging_spec.rb +1 -1
  59. data/spec/extensions/dirty_spec.rb +33 -0
  60. data/spec/extensions/empty_failure_backtraces_spec.rb +60 -0
  61. data/spec/extensions/exclude_or_null_spec.rb +15 -0
  62. data/spec/extensions/json_serializer_spec.rb +10 -0
  63. data/spec/extensions/named_timezones_spec.rb +5 -5
  64. data/spec/extensions/nested_attributes_spec.rb +48 -0
  65. data/spec/extensions/pg_array_ops_spec.rb +3 -3
  66. data/spec/extensions/pg_json_ops_spec.rb +67 -0
  67. data/spec/extensions/pg_range_spec.rb +35 -21
  68. data/spec/extensions/sharding_spec.rb +8 -0
  69. data/spec/extensions/spec_helper.rb +1 -1
  70. data/spec/guards_helper.rb +1 -1
  71. data/spec/integration/associations_test.rb +1 -1
  72. data/spec/integration/dataset_test.rb +57 -17
  73. data/spec/integration/plugin_test.rb +1 -1
  74. data/spec/integration/schema_test.rb +9 -0
  75. data/spec/integration/spec_helper.rb +7 -1
  76. data/spec/model/plugins_spec.rb +2 -2
  77. data/spec/model/spec_helper.rb +1 -1
  78. data/spec/sequel_warning.rb +1 -0
  79. metadata +35 -3
@@ -623,7 +623,7 @@ describe "SQLite", 'INSERT ON CONFLICT' do
623
623
  @ds.insert_conflict(:target=>:a).insert(1, 3, 4, false)
624
624
  @ds.insert_conflict(:target=>:c, :conflict_where=>:c_is_unique).insert(11, 12, 3, true)
625
625
  @ds.all.must_equal [{:a=>1, :b=>2, :c=>3, :c_is_unique=>false}, {:a=>10, :b=>11, :c=>3, :c_is_unique=>true}]
626
- end
626
+ end unless DB.adapter_scheme == :amalgalite
627
627
 
628
628
  it "Dataset#insert_ignore and insert_conflict should work with multi_insert/import" do
629
629
  @ds.insert(1, 2, 3, false)
@@ -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={})
@@ -2467,7 +2467,7 @@ describe "Database#typecast_value" do
2467
2467
  @db.typecast_value(:date, 'a')
2468
2468
  true.must_equal false
2469
2469
  rescue Sequel::InvalidValue => e
2470
- e.inspect.must_equal '#<Sequel::InvalidValue: ArgumentError: invalid date>'
2470
+ e.inspect.must_match(/\A#\<Sequel::InvalidValue: (Argument|Date::)Error: invalid date\>\z/)
2471
2471
  end
2472
2472
  end
2473
2473
  end
@@ -3065,7 +3065,6 @@ describe "Dataset#get" do
3065
3065
  end
3066
3066
 
3067
3067
  it "should select the specified column and fetch its value" do
3068
- @d
3069
3068
  5.times do
3070
3069
  @d.get(:name).must_equal "SELECT name FROM test LIMIT 1"
3071
3070
  @d.get(:abc).must_equal "SELECT abc FROM test LIMIT 1"
@@ -3077,14 +3076,12 @@ describe "Dataset#get" do
3077
3076
  end
3078
3077
 
3079
3078
  it "should work with aliased fields" do
3080
- @d
3081
3079
  5.times do
3082
3080
  @d.get(Sequel.expr(Sequel[:x][:b]).as(:name)).must_equal "SELECT x.b AS name FROM test LIMIT 1"
3083
3081
  end
3084
3082
  end
3085
3083
 
3086
3084
  it "should work with plain strings" do
3087
- @d
3088
3085
  5.times do
3089
3086
  @d.get('a').must_equal "SELECT 'a' AS v FROM test LIMIT 1"
3090
3087
  end
@@ -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
@@ -348,6 +353,11 @@ describe "Blockless Ruby Filters" do
348
353
  @d.l({:x => Sequel::FALSE}).must_equal '(x IS FALSE)'
349
354
  @d.l({:x => Sequel::SQLTRUE}).must_equal '(x IS TRUE)'
350
355
  @d.l({:x => Sequel::SQLFALSE}).must_equal '(x IS FALSE)'
356
+
357
+ @d.l({:x => Sequel::CURRENT_DATE}).must_equal '(x = CURRENT_DATE)'
358
+ @d.l({:x => Sequel::CURRENT_TIME}).must_equal '(x = CURRENT_TIME)'
359
+ @d.l({:x => Sequel::CURRENT_TIMESTAMP}).must_equal '(x = CURRENT_TIMESTAMP)'
360
+ @d.l({:x => Sequel::DEFAULT}).must_equal '(x = DEFAULT)'
351
361
  end
352
362
 
353
363
  it "should support negation of SQL::Constants" do
@@ -527,21 +537,21 @@ describe "Blockless Ruby Filters" do
527
537
  end
528
538
 
529
539
  it "should handle endless ranges" do
530
- endless = eval('1..')
540
+ endless = eval('(1..)')
531
541
  @d.l{x =~ endless}.must_equal '(x >= 1)'
532
542
  @d.l(:x => endless).must_equal '(x >= 1)'
533
543
 
534
- endless = eval('1...')
544
+ endless = eval('(1...)')
535
545
  @d.l{x =~ endless}.must_equal '(x >= 1)'
536
546
  @d.l(:x => endless).must_equal '(x >= 1)'
537
547
  end if RUBY_VERSION >= '2.6'
538
548
 
539
549
  it "should handle startless ranges" do
540
- endless = eval('..1')
550
+ endless = eval('(..1)')
541
551
  @d.l{x =~ endless}.must_equal '(x <= 1)'
542
552
  @d.l(:x => endless).must_equal '(x <= 1)'
543
553
 
544
- endless = eval('...1')
554
+ endless = eval('(...1)')
545
555
  @d.l{x =~ endless}.must_equal '(x < 1)'
546
556
  @d.l(:x => endless).must_equal '(x < 1)'
547
557
  end if RUBY_VERSION >= '2.7'
@@ -736,14 +746,23 @@ describe Sequel::SQL::VirtualRow do
736
746
  @d.l{mode.function.within_group(:a, :b)}.must_equal 'mode() WITHIN GROUP (ORDER BY "a", "b")'
737
747
  end
738
748
 
749
+ it "should handle emualted filtered aggregate function calls" do
750
+ @d.l{count.function.*.filter(Sequel.&(:a, :b))}.must_equal 'count((CASE WHEN ("a" AND "b") THEN 1 ELSE NULL END))'
751
+ @d.l{count.function.*.filter(:a=>1)}.must_equal 'count((CASE WHEN ("a" = 1) THEN 1 ELSE NULL END))'
752
+ @d.l{count(:a).filter{b > 1}}.must_equal 'count((CASE WHEN ("b" > 1) THEN "a" ELSE NULL END))'
753
+ @d.l{count(:a).filter(:a=>1){b > 1}}.must_equal 'count((CASE WHEN (("a" = 1) AND ("b" > 1)) THEN "a" ELSE NULL END))'
754
+ end
755
+
739
756
  it "should handle filtered aggregate function calls" do
757
+ @d = @d.with_extend{def supports_filtered_aggregates?; true end}
740
758
  @d.l{count.function.*.filter(Sequel.&(:a, :b))}.must_equal 'count(*) FILTER (WHERE ("a" AND "b"))'
741
759
  @d.l{count.function.*.filter(:a=>1)}.must_equal 'count(*) FILTER (WHERE ("a" = 1))'
742
760
  @d.l{count.function.*.filter{b > 1}}.must_equal 'count(*) FILTER (WHERE ("b" > 1))'
743
761
  @d.l{count.function.*.filter(:a=>1){b > 1}}.must_equal 'count(*) FILTER (WHERE (("a" = 1) AND ("b" > 1)))'
744
762
  end
745
763
 
746
- it "should handle fitlered ordered-set and hypothetical-set function calls" do
764
+ it "should handle filtered ordered-set and hypothetical-set function calls" do
765
+ @d = @d.with_extend{def supports_filtered_aggregates?; true end}
747
766
  @d.l{mode.function.within_group(:a).filter(:a=>1)}.must_equal 'mode() WITHIN GROUP (ORDER BY "a") FILTER (WHERE ("a" = 1))'
748
767
  end
749
768
 
@@ -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,23 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe "any_not_empty extension" do
4
+ before do
5
+ @ds = Sequel.mock[:t].extension(:any_not_empty)
6
+ end
7
+
8
+ it "should use a limited query if no block is given" do
9
+ @ds.with_fetch(:one=>1).any?.must_equal true
10
+ @ds.db.sqls.must_equal ["SELECT 1 AS one FROM t LIMIT 1"]
11
+ @ds.with_fetch([]).any?.must_equal false
12
+ @ds.db.sqls.must_equal ["SELECT 1 AS one FROM t LIMIT 1"]
13
+ end
14
+
15
+ it "should use default behavior if block is given" do
16
+ @ds.with_fetch(:one=>1).any?{|x| x[:one] == 1}.must_equal true
17
+ @ds.db.sqls.must_equal ["SELECT * FROM t"]
18
+ @ds.with_fetch(:one=>1).any?{|x| x[:one] != 1}.must_equal false
19
+ @ds.db.sqls.must_equal ["SELECT * FROM t"]
20
+ @ds.with_fetch([]).any?{|x| x[:one] == 1}.must_equal false
21
+ @ds.db.sqls.must_equal ["SELECT * FROM t"]
22
+ end
23
+ end
@@ -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