sequel 4.31.0 → 4.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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')