sequel 4.31.0 → 4.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +24 -0
  3. data/Rakefile +17 -15
  4. data/doc/association_basics.rdoc +7 -3
  5. data/doc/opening_databases.rdoc +7 -0
  6. data/doc/release_notes/4.32.0.txt +132 -0
  7. data/doc/schema_modification.rdoc +1 -1
  8. data/doc/security.rdoc +70 -26
  9. data/doc/testing.rdoc +1 -0
  10. data/lib/sequel/adapters/jdbc.rb +2 -1
  11. data/lib/sequel/adapters/postgres.rb +3 -4
  12. data/lib/sequel/adapters/shared/mysql.rb +14 -1
  13. data/lib/sequel/adapters/shared/sqlite.rb +2 -2
  14. data/lib/sequel/extensions/_pretty_table.rb +2 -0
  15. data/lib/sequel/extensions/arbitrary_servers.rb +2 -0
  16. data/lib/sequel/extensions/columns_introspection.rb +2 -0
  17. data/lib/sequel/extensions/connection_validator.rb +2 -0
  18. data/lib/sequel/extensions/constraint_validations.rb +2 -0
  19. data/lib/sequel/extensions/core_extensions.rb +1 -5
  20. data/lib/sequel/extensions/current_datetime_timestamp.rb +2 -0
  21. data/lib/sequel/extensions/dataset_source_alias.rb +2 -0
  22. data/lib/sequel/extensions/date_arithmetic.rb +2 -0
  23. data/lib/sequel/extensions/empty_array_consider_nulls.rb +3 -1
  24. data/lib/sequel/extensions/error_sql.rb +2 -0
  25. data/lib/sequel/extensions/eval_inspect.rb +2 -0
  26. data/lib/sequel/extensions/filter_having.rb +2 -0
  27. data/lib/sequel/extensions/from_block.rb +2 -0
  28. data/lib/sequel/extensions/graph_each.rb +2 -0
  29. data/lib/sequel/extensions/hash_aliases.rb +2 -0
  30. data/lib/sequel/extensions/inflector.rb +2 -0
  31. data/lib/sequel/extensions/looser_typecasting.rb +3 -1
  32. data/lib/sequel/extensions/meta_def.rb +2 -0
  33. data/lib/sequel/extensions/migration.rb +4 -0
  34. data/lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb +2 -0
  35. data/lib/sequel/extensions/named_timezones.rb +2 -0
  36. data/lib/sequel/extensions/no_auto_literal_strings.rb +84 -0
  37. data/lib/sequel/extensions/null_dataset.rb +2 -0
  38. data/lib/sequel/extensions/pagination.rb +2 -0
  39. data/lib/sequel/extensions/pg_array.rb +2 -4
  40. data/lib/sequel/extensions/pg_array_ops.rb +2 -0
  41. data/lib/sequel/extensions/pg_enum.rb +2 -0
  42. data/lib/sequel/extensions/pg_hstore.rb +2 -4
  43. data/lib/sequel/extensions/pg_hstore_ops.rb +2 -0
  44. data/lib/sequel/extensions/pg_inet.rb +2 -4
  45. data/lib/sequel/extensions/pg_inet_ops.rb +2 -0
  46. data/lib/sequel/extensions/pg_interval.rb +2 -4
  47. data/lib/sequel/extensions/pg_json.rb +4 -4
  48. data/lib/sequel/extensions/pg_json_ops.rb +3 -0
  49. data/lib/sequel/extensions/pg_loose_count.rb +2 -0
  50. data/lib/sequel/extensions/pg_range.rb +2 -4
  51. data/lib/sequel/extensions/pg_range_ops.rb +2 -0
  52. data/lib/sequel/extensions/pg_row.rb +2 -4
  53. data/lib/sequel/extensions/pg_row_ops.rb +2 -0
  54. data/lib/sequel/extensions/pg_static_cache_updater.rb +2 -0
  55. data/lib/sequel/extensions/pretty_table.rb +2 -0
  56. data/lib/sequel/extensions/query.rb +3 -0
  57. data/lib/sequel/extensions/query_literals.rb +7 -5
  58. data/lib/sequel/extensions/round_timestamps.rb +4 -3
  59. data/lib/sequel/extensions/schema_caching.rb +2 -0
  60. data/lib/sequel/extensions/schema_dumper.rb +2 -0
  61. data/lib/sequel/extensions/select_remove.rb +2 -0
  62. data/lib/sequel/extensions/sequel_3_dataset_methods.rb +2 -0
  63. data/lib/sequel/extensions/server_block.rb +3 -0
  64. data/lib/sequel/extensions/set_overrides.rb +2 -0
  65. data/lib/sequel/extensions/split_array_nil.rb +2 -0
  66. data/lib/sequel/extensions/thread_local_timezones.rb +2 -0
  67. data/lib/sequel/extensions/to_dot.rb +2 -0
  68. data/lib/sequel/model/associations.rb +95 -55
  69. data/lib/sequel/plugins/association_pks.rb +58 -33
  70. data/lib/sequel/plugins/eager_each.rb +22 -0
  71. data/lib/sequel/plugins/pg_typecast_on_load.rb +3 -2
  72. data/lib/sequel/plugins/tactical_eager_loading.rb +44 -3
  73. data/lib/sequel/version.rb +2 -2
  74. data/spec/adapters/mysql_spec.rb +34 -6
  75. data/spec/adapters/oracle_spec.rb +1 -1
  76. data/spec/bin_spec.rb +2 -2
  77. data/spec/core/dataset_spec.rb +7 -0
  78. data/spec/extensions/association_pks_spec.rb +38 -0
  79. data/spec/extensions/class_table_inheritance_spec.rb +24 -0
  80. data/spec/extensions/eager_each_spec.rb +25 -1
  81. data/spec/extensions/no_auto_literal_strings_spec.rb +65 -0
  82. data/spec/extensions/pg_range_spec.rb +1 -0
  83. data/spec/extensions/spec_helper.rb +5 -5
  84. data/spec/extensions/tactical_eager_loading_spec.rb +71 -17
  85. data/spec/integration/associations_test.rb +77 -62
  86. data/spec/integration/dataset_test.rb +3 -3
  87. data/spec/integration/plugin_test.rb +22 -0
  88. data/spec/integration/prepared_statement_test.rb +8 -8
  89. data/spec/integration/spec_helper.rb +4 -0
  90. data/spec/model/association_reflection_spec.rb +30 -0
  91. data/spec/model/associations_spec.rb +177 -16
  92. metadata +6 -2
@@ -29,6 +29,12 @@ module Sequel
29
29
  # objects, and the setting will not be persisted until after the object has
30
30
  # been saved.
31
31
  #
32
+ # By default, if you pass a nil value to the setter, an exception will be raised.
33
+ # You can change this behavior by using the :association_pks_nil association option.
34
+ # If set to :ignore, the setter will take no action if nil is given.
35
+ # If set to :remove, the setter will treat the nil as an empty array, removing
36
+ # the association all currently associated values.
37
+ #
32
38
  # Usage:
33
39
  #
34
40
  # # Make all model subclass *_to_many associations have association_pks
@@ -74,21 +80,25 @@ module Sequel
74
80
  end
75
81
 
76
82
  opts[:pks_setter] = lambda do |pks|
77
- checked_transaction do
78
- if clpk
79
- lpkv = lpk.map{|k| get_column_value(k)}
80
- cond = lk.zip(lpkv)
81
- else
82
- lpkv = get_column_value(lpk)
83
- cond = {lk=>lpkv}
83
+ if pks.empty?
84
+ send(opts.remove_all_method)
85
+ else
86
+ checked_transaction do
87
+ if clpk
88
+ lpkv = lpk.map{|k| get_column_value(k)}
89
+ cond = lk.zip(lpkv)
90
+ else
91
+ lpkv = get_column_value(lpk)
92
+ cond = {lk=>lpkv}
93
+ end
94
+ ds = _join_table_dataset(opts).filter(cond)
95
+ ds.exclude(rk=>pks).delete
96
+ pks -= ds.select_map(rk)
97
+ lpkv = Array(lpkv)
98
+ key_array = crk ? pks.map{|pk| lpkv + pk} : pks.map{|pk| lpkv + [pk]}
99
+ key_columns = Array(lk) + Array(rk)
100
+ ds.import(key_columns, key_array)
84
101
  end
85
- ds = _join_table_dataset(opts).filter(cond)
86
- ds.exclude(rk=>pks).delete
87
- pks -= ds.select_map(rk)
88
- lpkv = Array(lpkv)
89
- key_array = crk ? pks.map{|pk| lpkv + pk} : pks.map{|pk| lpkv + [pk]}
90
- key_columns = Array(lk) + Array(rk)
91
- ds.import(key_columns, key_array)
92
102
  end
93
103
  end
94
104
 
@@ -109,25 +119,29 @@ module Sequel
109
119
  end
110
120
 
111
121
  opts[:pks_setter] = lambda do |pks|
112
- primary_key = opts.associated_class.primary_key
113
- pkh = {primary_key=>pks}
114
-
115
- if key.is_a?(Array)
116
- h = {}
117
- nh = {}
118
- key.zip(pk).each do|k, v|
119
- h[k] = v
120
- nh[k] = nil
121
- end
122
+ if pks.empty?
123
+ send(opts.remove_all_method)
122
124
  else
123
- h = {key=>pk}
124
- nh = {key=>nil}
125
- end
125
+ primary_key = opts.associated_class.primary_key
126
+ pkh = {primary_key=>pks}
127
+
128
+ if key.is_a?(Array)
129
+ h = {}
130
+ nh = {}
131
+ key.zip(pk).each do|k, v|
132
+ h[k] = v
133
+ nh[k] = nil
134
+ end
135
+ else
136
+ h = {key=>pk}
137
+ nh = {key=>nil}
138
+ end
126
139
 
127
- checked_transaction do
128
- ds = send(opts.dataset_method)
129
- ds.unfiltered.filter(pkh).update(h)
130
- ds.exclude(pkh).update(nh)
140
+ checked_transaction do
141
+ ds = send(opts.dataset_method)
142
+ ds.unfiltered.filter(pkh).update(h)
143
+ ds.exclude(pkh).update(nh)
144
+ end
131
145
  end
132
146
  end
133
147
 
@@ -141,7 +155,7 @@ module Sequel
141
155
  def after_save
142
156
  if assoc_pks = @_association_pks
143
157
  assoc_pks.each do |name, pks|
144
- instance_exec(pks, &model.association_reflection(name)[:pks_setter]) unless pks.empty?
158
+ instance_exec(pks, &model.association_reflection(name)[:pks_setter])
145
159
  end
146
160
  @_association_pks = nil
147
161
  end
@@ -174,7 +188,19 @@ module Sequel
174
188
  # If the receiver is a new object, save the pks
175
189
  # so the update can happen after the received has been saved.
176
190
  def _association_pks_setter(opts, pks)
191
+ if pks.nil?
192
+ case opts[:association_pks_nil]
193
+ when :remove
194
+ pks = []
195
+ when :ignore
196
+ return
197
+ else
198
+ raise Error, "nil value given to association_pks setter"
199
+ end
200
+ end
201
+
177
202
  pks = convert_pk_array(opts, pks)
203
+
178
204
  delay = opts[:delay_pks]
179
205
  if (new? && delay) || (delay == :always)
180
206
  modified!
@@ -190,7 +216,6 @@ module Sequel
190
216
  klass = opts.associated_class
191
217
  primary_key = klass.primary_key
192
218
  sch = klass.db_schema
193
- pks = Array(pks)
194
219
 
195
220
  if primary_key.is_a?(Array)
196
221
  if (cols = sch.values_at(*klass.primary_key)).all? && (convs = cols.map{|c| c[:type] == :integer}).all?
@@ -15,6 +15,11 @@ module Sequel
15
15
  # and setting a new flag in the cloned dataset, so that each can check with the flag to
16
16
  # determine whether it should call all.
17
17
  #
18
+ # This plugin also makes #first and related methods that load single records work with
19
+ # eager loading. Note that when using eager_graph, calling #first or a similar method
20
+ # will result in two queries, one to load the main object, and one to eagerly load all associated
21
+ # objects to that main object.
22
+ #
18
23
  # Usage:
19
24
  #
20
25
  # # Make all model subclass instances eagerly load for each (called before loading subclasses)
@@ -53,6 +58,23 @@ module Sequel
53
58
  end
54
59
  end
55
60
 
61
+ # Handle eager loading when calling first and related methods. For eager_graph,
62
+ # this does an additional query after retrieving a single record, because otherwise
63
+ # the associated records won't get eager loaded correctly.
64
+ def single_record!
65
+ if use_eager_all?
66
+ obj = clone(:all_called=>true).all.first
67
+
68
+ if opts[:eager_graph]
69
+ obj = clone(:all_called=>true).where(obj.qualified_pk_hash).unlimited.all.first
70
+ end
71
+
72
+ obj
73
+ else
74
+ super
75
+ end
76
+ end
77
+
56
78
  private
57
79
 
58
80
  # Wether to use all when each is called, true when eager loading
@@ -3,11 +3,12 @@
3
3
  module Sequel
4
4
  module Plugins
5
5
  # The PgTypecastOnLoad plugin exists because when you connect to PostgreSQL
6
- # using the do, swift, or jdbc adapter, Sequel doesn't have complete
6
+ # using the do and swift adapters, Sequel doesn't have complete
7
7
  # control over typecasting, and may return columns as strings instead of how
8
8
  # the native postgres adapter would typecast them. This is mostly needed for
9
9
  # the additional support that the pg_* extensions add for advanced PostgreSQL
10
- # types such as arrays.
10
+ # types such as arrays. This plugin is not needed if using the postgres or
11
+ # jdbc/postgresql adapters.
11
12
  #
12
13
  # This plugin makes model loading to do the same conversion that the
13
14
  # native postgres adapter would do for all columns given. You can either
@@ -19,7 +19,47 @@ module Sequel
19
19
  # Album.filter{id<100}.all do |a|
20
20
  # a.artists
21
21
  # end
22
+ # # SELECT * FROM albums WHERE (id < 100)
23
+ # # SELECT * FROM artists WHERE id IN (...)
24
+ #
25
+ # Note that if you are passing a callback to the association method via
26
+ # a block or :callback option, or using the :reload option to reload
27
+ # the association, eager loading will not be done.
28
+ #
29
+ # You can use the :eager_reload option to reload the association for all
30
+ # objects that the current object was retrieved with:
31
+ #
32
+ # # SELECT * FROM albums WHERE (id < 100)
33
+ # albums = Album.filter{id<100}.all
34
+ #
35
+ # # Eagerly load all artists for these albums
36
+ # # SELECT * FROM artists WHERE id IN (...)
37
+ # albums.first.artists
38
+ #
39
+ # # Do something that may affect which artists are associated to the albums
40
+ #
41
+ # # Eagerly reload all artists for these albums
42
+ # # SELECT * FROM artists WHERE id IN (...)
43
+ # albums.first.artists(:eager_reload=>true)
22
44
  #
45
+ # You can also use the :eager option to specify dependent associations
46
+ # to eager load:
47
+ #
48
+ # albums = Album.filter{id<100}.all
49
+ #
50
+ # # Eager load all artists for these albums, and all albums for those artists
51
+ # # SELECT * FROM artists WHERE id IN (...)
52
+ # # SELECT * FROM albums WHERE artist_id IN (...)
53
+ # albums.first.artists(:eager=>:albums)
54
+ #
55
+ # You can also use :eager to specify an eager callback. For example:
56
+ #
57
+ # albums = Album.filter{id<100}.all
58
+ #
59
+ # # Eagerly load all artists whose name starts with A-M for these albums
60
+ # # SELECT * FROM artists WHERE name > 'N' AND id IN (...)
61
+ # albums.first.artists(:eager=>proc{|ds| ds.where(:name > 'N')})
62
+ #
23
63
  # Usage:
24
64
  #
25
65
  # # Make all model subclass instances use tactical eager loading (called before loading subclasses)
@@ -51,11 +91,12 @@ module Sequel
51
91
  # If there the association is not in the associations cache and the object
52
92
  # was reteived via Dataset#all, eagerly load the association for all model
53
93
  # objects retrieved with the current object.
54
- def load_associated_objects(opts, reload=false)
94
+ def load_associated_objects(opts, dynamic_opts=nil, &block)
95
+ dynamic_opts = load_association_objects_options(dynamic_opts, &block)
55
96
  name = opts[:name]
56
- if !associations.include?(name) && retrieved_by && !frozen?
97
+ if (!associations.include?(name) || dynamic_opts[:eager_reload]) && retrieved_by && !frozen? && !dynamic_opts[:callback] && !dynamic_opts[:reload]
57
98
  begin
58
- retrieved_by.send(:eager_load, retrieved_with.reject(&:frozen?), name=>{})
99
+ retrieved_by.send(:eager_load, retrieved_with.reject(&:frozen?), name=>dynamic_opts[:eager] || {})
59
100
  rescue Sequel::UndefinedAssociation
60
101
  # This can happen if class table inheritance is used and the association
61
102
  # is only defined in a subclass. This particular instance can use the
@@ -5,13 +5,13 @@ module Sequel
5
5
  MAJOR = 4
6
6
  # The minor version of Sequel. Bumped for every non-patch level
7
7
  # release, generally around once a month.
8
- MINOR = 31
8
+ MINOR = 32
9
9
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
10
10
  # releases that fix regressions from previous versions.
11
11
  TINY = 0
12
12
 
13
13
  # The version of Sequel you are using, as a string (e.g. "2.11.0")
14
- VERSION = [MAJOR, MINOR, TINY].join('.')
14
+ VERSION = [MAJOR, MINOR, TINY].join('.').freeze
15
15
 
16
16
  # The version of Sequel you are using, as a string (e.g. "2.11.0")
17
17
  def self.version
@@ -325,12 +325,6 @@ describe "Joined MySQL dataset" do
325
325
  @ds.join(:attributes, :node_id => :id).sql.must_equal "SELECT * FROM `nodes` INNER JOIN `attributes` ON (`attributes`.`node_id` = `nodes`.`id`)"
326
326
  end
327
327
 
328
- it "should allow a having clause on ungrouped datasets" do
329
- @ds.having('blah')
330
-
331
- @ds.having('blah').sql.must_equal "SELECT * FROM `nodes` HAVING (blah)"
332
- end
333
-
334
328
  it "should put a having clause before an order by clause" do
335
329
  @ds.order(:aaa).having(:bbb => :ccc).sql.must_equal "SELECT * FROM `nodes` HAVING (`bbb` = `ccc`) ORDER BY `aaa`"
336
330
  end
@@ -1289,3 +1283,37 @@ if DB.adapter_scheme == :mysql2
1289
1283
  end
1290
1284
  end
1291
1285
  end
1286
+
1287
+ describe "MySQL joined datasets" do
1288
+ before do
1289
+ @db = DB
1290
+ @db.create_table!(:a) do
1291
+ Integer :id
1292
+ end
1293
+ @db.create_table!(:b) do
1294
+ Integer :id
1295
+ Integer :a_id
1296
+ end
1297
+ @db[:a].insert(1)
1298
+ @db[:a].insert(2)
1299
+ @db[:b].insert(3, 1)
1300
+ @db[:b].insert(4, 1)
1301
+ @db[:b].insert(5, 2)
1302
+ @ds = @db[:a].join(:b, :a_id=>:id)
1303
+ end
1304
+ after do
1305
+ @db.drop_table?(:a, :b)
1306
+ end
1307
+
1308
+ it "should support deletions from a single table" do
1309
+ @ds.where(:a__id=>1).delete
1310
+ @db[:a].select_order_map(:id).must_equal [2]
1311
+ @db[:b].select_order_map(:id).must_equal [3, 4, 5]
1312
+ end
1313
+
1314
+ it "should support deletions from multiple tables" do
1315
+ @ds.delete_from(:a, :b).where(:a__id=>1).delete
1316
+ @db[:a].select_order_map(:id).must_equal [2]
1317
+ @db[:b].select_order_map(:id).must_equal [5]
1318
+ end
1319
+ end
@@ -222,7 +222,7 @@ describe "An Oracle database" do
222
222
  it "should translate values correctly" do
223
223
  @d << {:name => 'abc', :value => 456}
224
224
  @d << {:name => 'def', :value => 789}
225
- @d.filter('value > 500').update(:date_created => Sequel.lit("to_timestamp('2009-09-09', 'YYYY-MM-DD')"))
225
+ @d.filter{value > 500}.update(:date_created => Sequel.lit("to_timestamp('2009-09-09', 'YYYY-MM-DD')"))
226
226
 
227
227
  @d[:name => 'def'][:date_created].strftime('%F').must_equal '2009-09-09'
228
228
  end
data/spec/bin_spec.rb CHANGED
@@ -139,7 +139,7 @@ END
139
139
  end
140
140
 
141
141
  it "-E should echo SQL statements to stdout" do
142
- bin(:args=>'-E -c DB.tables').must_match %r{SELECT \* FROM `sqlite_master` WHERE \(type = 'table' AND NOT name = 'sqlite_sequence'\)\n}
142
+ bin(:args=>'-E -c DB.tables').must_include "SELECT * FROM `sqlite_master` WHERE ((`name` != 'sqlite_sequence') AND (`type` = 'table'))"
143
143
  end
144
144
 
145
145
  it "-I should include directory in load path" do
@@ -148,7 +148,7 @@ END
148
148
 
149
149
  it "-l should log SQL statements to file" do
150
150
  bin(:args=>"-l #{TMP_FILE} -c DB.tables").must_equal ''
151
- File.read(TMP_FILE).must_match %r{SELECT \* FROM `sqlite_master` WHERE \(type = 'table' AND NOT name = 'sqlite_sequence'\)\n}
151
+ File.read(TMP_FILE).must_include "SELECT * FROM `sqlite_master` WHERE ((`name` != 'sqlite_sequence') AND (`type` = 'table'))"
152
152
  end
153
153
 
154
154
  it "-L should load all *.rb files in given directory" do
@@ -3642,6 +3642,13 @@ describe "Dataset prepared statements and bound variables " do
3642
3642
  proc{ps.prepare(:select, :select_n2)}.must_raise Sequel::Error
3643
3643
  end
3644
3644
 
3645
+ it "PreparedStatement#prepare should not raise an error if preparing prepared statements is allowed" do
3646
+ ps = @ds.prepare(:select, :select_n)
3647
+ def ps.allow_preparing_prepared_statements?; true end
3648
+ ps.prepare(:select, :select_n2).call
3649
+ @db.sqls.must_equal ["SELECT * FROM items"]
3650
+ end
3651
+
3645
3652
  it "#call should default to using :all if an invalid type is given" do
3646
3653
  @ds.filter(:num=>:$n).call(:select_all, :n=>1)
3647
3654
  @db.sqls.must_equal ['SELECT * FROM items WHERE (num = 1)']
@@ -336,6 +336,14 @@ describe "Sequel::Plugins::AssociationPks" do
336
336
  "UPDATE albums SET artist_id = NULL WHERE ((albums.artist_id = 1) AND (id NOT IN (2, 4)))"
337
337
  ]
338
338
 
339
+ ar.album_pks = []
340
+ @db.sqls.must_equal []
341
+
342
+ ar.save_changes
343
+ @db.sqls.must_equal [
344
+ "UPDATE albums SET artist_id = NULL WHERE (artist_id = 1)"
345
+ ]
346
+
339
347
  al = @Album.load(:id=>1)
340
348
  al.tag_pks.must_equal [1,2]
341
349
  @db.sqls
@@ -351,6 +359,14 @@ describe "Sequel::Plugins::AssociationPks" do
351
359
  "INSERT INTO albums_tags (album_id, tag_id) VALUES (1, 3)",
352
360
  "COMMIT",
353
361
  ]
362
+
363
+ al.tag_pks = []
364
+ @db.sqls.must_equal []
365
+
366
+ al.save_changes
367
+ @db.sqls.must_equal [
368
+ "DELETE FROM albums_tags WHERE (album_id = 1)",
369
+ ]
354
370
  end
355
371
 
356
372
  it "should clear delayed associated pks if refreshing, if :delay plugin option is used" do
@@ -364,4 +380,26 @@ describe "Sequel::Plugins::AssociationPks" do
364
380
  ar.refresh
365
381
  ar.album_pks.must_equal [1,2,3]
366
382
  end
383
+
384
+ it "should remove all values if nil given to setter and :association_pks_nil=>:remove" do
385
+ @Artist.one_to_many :albums, :clone=>:albums, :association_pks_nil=>:remove
386
+
387
+ ar = @Artist.load(:id=>1)
388
+ ar.album_pks = nil
389
+ @db.sqls.must_equal ["UPDATE albums SET artist_id = NULL WHERE (artist_id = 1)"]
390
+ end
391
+
392
+ it "should take no action if nil given to setter and :association_pks_nil=>:ignore" do
393
+ @Artist.one_to_many :albums, :clone=>:albums, :association_pks_nil=>:ignore
394
+
395
+ ar = @Artist.load(:id=>1)
396
+ ar = @Artist.new
397
+ ar.album_pks = nil
398
+ @db.sqls.must_equal []
399
+ end
400
+
401
+ it "should raise error if nil given to setter by default" do
402
+ ar = @Artist.load(:id=>1)
403
+ proc{ar.album_pks = nil}.must_raise Sequel::Error
404
+ end
367
405
  end
@@ -51,6 +51,14 @@ describe "class_table_inheritance plugin" do
51
51
  Object.send(:remove_const, :Employee)
52
52
  end
53
53
 
54
+ it "#cti_base_model should be the model that loaded the plugin" do
55
+ Executive.cti_base_model.must_equal Employee
56
+ end
57
+
58
+ it "#cti_columns should be a mapping of table names to columns" do
59
+ Executive.cti_columns.must_equal(:employees=>[:id, :name, :kind], :managers=>[:id, :num_staff], :executives=>[:id, :num_managers])
60
+ end
61
+
54
62
  it "should have simple_table = nil for all subclasses" do
55
63
  Manager.simple_table.must_equal nil
56
64
  Executive.simple_table.must_equal nil
@@ -246,7 +254,23 @@ describe "class_table_inheritance plugin" do
246
254
  sqls[0].must_match(/INSERT INTO employees \((name|kind), (name|kind)\) VALUES \('(E|Ceo)', '(E|Ceo)'\)/)
247
255
  sqls[1].must_match(/INSERT INTO managers \((num_staff|id), (num_staff|id)\) VALUES \([12], [12]\)/)
248
256
  sqls[2].must_match(/INSERT INTO executives \((num_managers|id), (num_managers|id)\) VALUES \([13], [13]\)/)
257
+ end
258
+
259
+ it "should insert the correct rows into all tables when inserting when insert select is important" do
260
+ [Ceo, Manager, Employee].each do |klass|
261
+ def (klass.cti_instance_dataset).supports_insert_select?; true; end
262
+ def (klass.cti_instance_dataset).insert_select(v)
263
+ db.run(insert_sql(v) + " RETURNING *")
264
+ v.merge(:id=>1)
265
+ end
249
266
  end
267
+ Ceo.create(:num_managers=>3, :num_staff=>2, :name=>'E')
268
+ sqls = @db.sqls
269
+ sqls.length.must_equal 3
270
+ sqls[0].must_match(/INSERT INTO employees \((name|kind), (name|kind)\) VALUES \('(E|Ceo)', '(E|Ceo)'\) RETURNING \*/)
271
+ sqls[1].must_match(/INSERT INTO managers \((num_staff|id), (num_staff|id)\) VALUES \([12], [12]\) RETURNING \*/)
272
+ sqls[2].must_match(/INSERT INTO executives \((num_managers|id), (num_managers|id)\) VALUES \([13], [13]\) RETURNING \*/)
273
+ end
250
274
 
251
275
  it "should insert the correct rows into all tables with a given primary key" do
252
276
  e = Ceo.new(:num_managers=>3, :num_staff=>2, :name=>'E')