sequel 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +1 -1
  3. data/doc/cheat_sheet.rdoc +5 -2
  4. data/doc/opening_databases.rdoc +2 -0
  5. data/doc/release_notes/3.5.0.txt +510 -0
  6. data/lib/sequel/adapters/ado.rb +3 -1
  7. data/lib/sequel/adapters/ado/mssql.rb +2 -2
  8. data/lib/sequel/adapters/do.rb +2 -11
  9. data/lib/sequel/adapters/do/mysql.rb +7 -0
  10. data/lib/sequel/adapters/do/postgres.rb +2 -2
  11. data/lib/sequel/adapters/firebird.rb +3 -3
  12. data/lib/sequel/adapters/informix.rb +3 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +3 -3
  14. data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
  15. data/lib/sequel/adapters/mysql.rb +60 -21
  16. data/lib/sequel/adapters/odbc.rb +1 -1
  17. data/lib/sequel/adapters/openbase.rb +3 -3
  18. data/lib/sequel/adapters/oracle.rb +1 -5
  19. data/lib/sequel/adapters/postgres.rb +3 -3
  20. data/lib/sequel/adapters/shared/mssql.rb +142 -33
  21. data/lib/sequel/adapters/shared/mysql.rb +54 -31
  22. data/lib/sequel/adapters/shared/oracle.rb +17 -6
  23. data/lib/sequel/adapters/shared/postgres.rb +7 -7
  24. data/lib/sequel/adapters/shared/progress.rb +3 -3
  25. data/lib/sequel/adapters/shared/sqlite.rb +3 -17
  26. data/lib/sequel/connection_pool.rb +4 -6
  27. data/lib/sequel/core.rb +29 -113
  28. data/lib/sequel/database.rb +14 -12
  29. data/lib/sequel/dataset.rb +8 -21
  30. data/lib/sequel/dataset/convenience.rb +1 -1
  31. data/lib/sequel/dataset/graph.rb +9 -2
  32. data/lib/sequel/dataset/sql.rb +170 -104
  33. data/lib/sequel/exceptions.rb +3 -0
  34. data/lib/sequel/extensions/looser_typecasting.rb +21 -0
  35. data/lib/sequel/extensions/named_timezones.rb +61 -0
  36. data/lib/sequel/extensions/schema_dumper.rb +7 -1
  37. data/lib/sequel/extensions/sql_expr.rb +122 -0
  38. data/lib/sequel/extensions/string_date_time.rb +4 -4
  39. data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
  40. data/lib/sequel/model/associations.rb +105 -45
  41. data/lib/sequel/model/base.rb +37 -28
  42. data/lib/sequel/plugins/active_model.rb +35 -0
  43. data/lib/sequel/plugins/association_dependencies.rb +96 -0
  44. data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
  45. data/lib/sequel/plugins/force_encoding.rb +61 -0
  46. data/lib/sequel/plugins/many_through_many.rb +32 -11
  47. data/lib/sequel/plugins/nested_attributes.rb +7 -2
  48. data/lib/sequel/plugins/subclasses.rb +45 -0
  49. data/lib/sequel/plugins/touch.rb +118 -0
  50. data/lib/sequel/plugins/typecast_on_load.rb +61 -0
  51. data/lib/sequel/sql.rb +31 -30
  52. data/lib/sequel/timezones.rb +161 -0
  53. data/lib/sequel/version.rb +1 -1
  54. data/spec/adapters/mssql_spec.rb +262 -0
  55. data/spec/adapters/mysql_spec.rb +46 -8
  56. data/spec/adapters/postgres_spec.rb +6 -3
  57. data/spec/adapters/spec_helper.rb +21 -0
  58. data/spec/adapters/sqlite_spec.rb +1 -1
  59. data/spec/core/connection_pool_spec.rb +1 -1
  60. data/spec/core/database_spec.rb +27 -1
  61. data/spec/core/dataset_spec.rb +63 -1
  62. data/spec/core/object_graph_spec.rb +1 -1
  63. data/spec/core/schema_spec.rb +1 -0
  64. data/spec/extensions/active_model_spec.rb +47 -0
  65. data/spec/extensions/association_dependencies_spec.rb +108 -0
  66. data/spec/extensions/class_table_inheritance_spec.rb +252 -0
  67. data/spec/extensions/force_encoding_spec.rb +75 -0
  68. data/spec/extensions/looser_typecasting_spec.rb +39 -0
  69. data/spec/extensions/many_through_many_spec.rb +60 -2
  70. data/spec/extensions/named_timezones_spec.rb +72 -0
  71. data/spec/extensions/nested_attributes_spec.rb +29 -1
  72. data/spec/extensions/schema_dumper_spec.rb +10 -0
  73. data/spec/extensions/spec_helper.rb +1 -1
  74. data/spec/extensions/sql_expr_spec.rb +89 -0
  75. data/spec/extensions/subclasses_spec.rb +52 -0
  76. data/spec/extensions/thread_local_timezones_spec.rb +45 -0
  77. data/spec/extensions/touch_spec.rb +155 -0
  78. data/spec/extensions/typecast_on_load_spec.rb +60 -0
  79. data/spec/integration/database_test.rb +8 -0
  80. data/spec/integration/dataset_test.rb +9 -9
  81. data/spec/integration/plugin_test.rb +139 -0
  82. data/spec/integration/schema_test.rb +7 -7
  83. data/spec/integration/spec_helper.rb +32 -1
  84. data/spec/integration/timezone_test.rb +3 -3
  85. data/spec/integration/transaction_test.rb +1 -1
  86. data/spec/integration/type_test.rb +6 -6
  87. data/spec/model/association_reflection_spec.rb +18 -0
  88. data/spec/model/associations_spec.rb +169 -9
  89. data/spec/model/base_spec.rb +2 -0
  90. data/spec/model/eager_loading_spec.rb +82 -2
  91. data/spec/model/model_spec.rb +8 -1
  92. data/spec/model/record_spec.rb +52 -9
  93. metadata +33 -23
@@ -137,7 +137,7 @@ context "A PostgreSQL dataset with a timestamp field" do
137
137
  @d.delete
138
138
  end
139
139
 
140
- specify "should store milliseconds in time fields" do
140
+ cspecify "should store milliseconds in time fields", :do do
141
141
  t = Time.now
142
142
  @d << {:value=>1, :time=>t}
143
143
  @d.literal(@d[:value =>'1'][:time]).should == @d.literal(t)
@@ -375,9 +375,8 @@ context "Postgres::Dataset#insert" do
375
375
 
376
376
  specify "should use INSERT RETURNING if server_version >= 80200" do
377
377
  @ds.meta_def(:server_version){80201}
378
- @ds.should_receive(:clone).once.with(:server=>:default, :sql=>'INSERT INTO test5 (value) VALUES (10) RETURNING xid').and_return(@ds)
379
- @ds.should_receive(:single_value).once
380
378
  @ds.insert(:value=>10)
379
+ @db.sqls.last.should == 'INSERT INTO test5 (value) VALUES (10) RETURNING xid'
381
380
  end
382
381
 
383
382
  specify "should have insert_returning_sql use the RETURNING keyword" do
@@ -390,6 +389,10 @@ context "Postgres::Dataset#insert" do
390
389
  @ds.insert_select(:value=>10).should == nil
391
390
  end
392
391
 
392
+ specify "should have insert_select return nil if disable_insert_returning is used" do
393
+ @ds.disable_insert_returning.insert_select(:value=>10).should == nil
394
+ end
395
+
393
396
  specify "should have insert_select insert the record and return the inserted record if server_version < 80200" do
394
397
  @ds.meta_def(:server_version){80201}
395
398
  h = @ds.insert_select(:value=>10)
@@ -8,3 +8,24 @@ begin
8
8
  require File.join(File.dirname(File.dirname(__FILE__)), 'spec_config.rb')
9
9
  rescue LoadError
10
10
  end
11
+
12
+ class Spec::Example::ExampleGroup
13
+ def self.cspecify(message, *checked, &block)
14
+ pending = false
15
+ checked.each do |c|
16
+ case c
17
+ when INTEGRATION_DB.class.adapter_scheme
18
+ pending = c
19
+ when Proc
20
+ pending = c if c.first.call(INTEGRATION_DB)
21
+ when Array
22
+ pending = c if c.first == INTEGRATION_DB.class.adapter_scheme && c.last == INTEGRATION_DB.call(INTEGRATION_DB)
23
+ end
24
+ end
25
+ if pending
26
+ specify(message){pending("Not yet working on #{Array(pending).join(', ')}", &block)}
27
+ else
28
+ specify(message, &block)
29
+ end
30
+ end
31
+ end
@@ -68,7 +68,7 @@ context "An SQLite database" do
68
68
  proc {@db.temp_store = :invalid}.should raise_error(Sequel::Error)
69
69
  end
70
70
 
71
- specify "should support timestamps and datetimes and respect datetime_class" do
71
+ cspecify "should support timestamps and datetimes and respect datetime_class", :do, :jdbc, :amalgalite do
72
72
  @db.create_table!(:time){timestamp :t; datetime :d}
73
73
  t1 = Time.at(1)
74
74
  @db[:time] << {:t => t1, :d => t1.to_i}
@@ -85,7 +85,7 @@ context "A connection pool handling connections" do
85
85
  end
86
86
 
87
87
  specify "#make_new should not make more than max_size connections" do
88
- 50.times{Thread.new{@cpool.hold{sleep 0.01}}}
88
+ 50.times{Thread.new{@cpool.hold{sleep 0.001}}}
89
89
  @cpool.created_count.should == @max_size
90
90
  end
91
91
 
@@ -1251,6 +1251,7 @@ context "Database#typecast_value" do
1251
1251
  before do
1252
1252
  @db = Sequel::Database.new
1253
1253
  end
1254
+
1254
1255
  specify "should raise an InvalidValue when given an invalid value" do
1255
1256
  proc{@db.typecast_value(:integer, "13a")}.should raise_error(Sequel::InvalidValue)
1256
1257
  proc{@db.typecast_value(:float, "4.e2")}.should raise_error(Sequel::InvalidValue)
@@ -1260,6 +1261,24 @@ context "Database#typecast_value" do
1260
1261
  proc{@db.typecast_value(:time, Date.new)}.should raise_error(Sequel::InvalidValue)
1261
1262
  proc{@db.typecast_value(:datetime, 4)}.should raise_error(Sequel::InvalidValue)
1262
1263
  end
1264
+
1265
+ specify "should have an underlying exception class available at wrapped_exception" do
1266
+ begin
1267
+ @db.typecast_value(:date, 'a')
1268
+ true.should == false
1269
+ rescue Sequel::InvalidValue => e
1270
+ e.wrapped_exception.should be_a_kind_of(ArgumentError)
1271
+ end
1272
+ end
1273
+
1274
+ specify "should include underlying exception class in #inspect" do
1275
+ begin
1276
+ @db.typecast_value(:date, 'a')
1277
+ true.should == false
1278
+ rescue Sequel::InvalidValue => e
1279
+ e.inspect.should =~ /\A#<Sequel::InvalidValue: ArgumentError: .*>\z/
1280
+ end
1281
+ end
1263
1282
  end
1264
1283
 
1265
1284
  context "Database#blank_object?" do
@@ -1383,5 +1402,12 @@ context "Database#column_schema_to_ruby_default" do
1383
1402
  p["10:20:30", :time].should == Time.parse('10:20:30')
1384
1403
  p["CURRENT_DATE", :date].should == nil
1385
1404
  p["CURRENT_TIMESTAMP", :datetime].should == nil
1405
+ p["a", :enum].should == "a"
1406
+
1407
+ db.meta_def(:database_type){:mssql}
1408
+ p["(N'a')", :string].should == "a"
1409
+ p["((-12))", :integer].should == -12
1410
+ p["((12.1))", :float].should == 12.1
1411
+ p["((-12.1))", :decimal].should == BigDecimal.new('-12.1')
1386
1412
  end
1387
- end
1413
+ end
@@ -211,7 +211,7 @@ context "A simple dataset" do
211
211
  specify "should format an insert statement with sub-query" do
212
212
  @sub = Sequel::Dataset.new(nil).from(:something).filter(:x => 2)
213
213
  @dataset.insert_sql(@sub).should == \
214
- "INSERT INTO test (SELECT * FROM something WHERE (x = 2))"
214
+ "INSERT INTO test SELECT * FROM something WHERE (x = 2)"
215
215
  end
216
216
 
217
217
  specify "should format an insert statement with array" do
@@ -2586,6 +2586,56 @@ context "Dataset#insert_sql" do
2586
2586
  specify "should raise an Error if the dataset has no sources" do
2587
2587
  proc{Sequel::Database.new.dataset.insert_sql}.should raise_error(Sequel::Error)
2588
2588
  end
2589
+
2590
+ specify "should accept datasets" do
2591
+ @ds.insert_sql(@ds).should == "INSERT INTO items SELECT * FROM items"
2592
+ end
2593
+
2594
+ specify "should accept datasets with columns" do
2595
+ @ds.insert_sql([:a, :b], @ds).should == "INSERT INTO items (a, b) SELECT * FROM items"
2596
+ end
2597
+
2598
+ specify "should raise if given bad values" do
2599
+ proc{@ds.clone(:values=>'a').send(:_insert_sql)}.should raise_error(Sequel::Error)
2600
+ end
2601
+
2602
+ specify "should accept separate values" do
2603
+ @ds.insert_sql(1).should == "INSERT INTO items VALUES (1)"
2604
+ @ds.insert_sql(1, 2).should == "INSERT INTO items VALUES (1, 2)"
2605
+ @ds.insert_sql(1, 2, 3).should == "INSERT INTO items VALUES (1, 2, 3)"
2606
+ end
2607
+
2608
+ specify "should accept a single array of values" do
2609
+ @ds.insert_sql([1, 2, 3]).should == "INSERT INTO items VALUES (1, 2, 3)"
2610
+ end
2611
+
2612
+ specify "should accept an array of columns and an array of values" do
2613
+ @ds.insert_sql([:a, :b, :c], [1, 2, 3]).should == "INSERT INTO items (a, b, c) VALUES (1, 2, 3)"
2614
+ end
2615
+
2616
+ specify "should raise an array if the columns and values differ in size" do
2617
+ proc{@ds.insert_sql([:a, :b], [1, 2, 3])}.should raise_error(Sequel::Error)
2618
+ end
2619
+
2620
+ specify "should accept a single LiteralString" do
2621
+ @ds.insert_sql('VALUES (1, 2, 3)'.lit).should == "INSERT INTO items VALUES (1, 2, 3)"
2622
+ end
2623
+
2624
+ specify "should accept an array of columns and an LiteralString" do
2625
+ @ds.insert_sql([:a, :b, :c], 'VALUES (1, 2, 3)'.lit).should == "INSERT INTO items (a, b, c) VALUES (1, 2, 3)"
2626
+ end
2627
+
2628
+ specify "should accept an object that responds to values and returns a hash by using that hash as the columns and values" do
2629
+ o = Object.new
2630
+ def o.values; {:c=>'d'}; end
2631
+ @ds.insert_sql(o).should == "INSERT INTO items (c) VALUES ('d')"
2632
+ end
2633
+
2634
+ specify "should accept an object that responds to values and returns something other than a hash by using the object itself as a single value" do
2635
+ o = Date.civil(2000, 1, 1)
2636
+ def o.values; self; end
2637
+ @ds.insert_sql(o).should == "INSERT INTO items VALUES ('2000-01-01')"
2638
+ end
2589
2639
  end
2590
2640
 
2591
2641
  class DummyMummyDataset < Sequel::Dataset
@@ -3219,7 +3269,19 @@ describe "Sequel timezone support" do
3219
3269
  specify "should raise an error when attempting to typecast to a timestamp from an unsupported type" do
3220
3270
  proc{Sequel.database_to_application_timestamp(Object.new)}.should raise_error(Sequel::InvalidValue)
3221
3271
  end
3272
+
3273
+ specify "should raise an InvalidValue error when the DateTime class is used and when a bad application timezone is used when attempting to convert timestamps" do
3274
+ Sequel.application_timezone = :blah
3275
+ Sequel.datetime_class = DateTime
3276
+ proc{Sequel.database_to_application_timestamp('2009-06-01 10:20:30')}.should raise_error(Sequel::InvalidValue)
3277
+ end
3222
3278
 
3279
+ specify "should raise an InvalidValue error when the DateTime class is used and when a bad database timezone is used when attempting to convert timestamps" do
3280
+ Sequel.database_timezone = :blah
3281
+ Sequel.datetime_class = DateTime
3282
+ proc{Sequel.database_to_application_timestamp('2009-06-01 10:20:30')}.should raise_error(Sequel::InvalidValue)
3283
+ end
3284
+
3223
3285
  specify "should have Sequel.default_timezone= should set all other timezones" do
3224
3286
  Sequel.database_timezone.should == nil
3225
3287
  Sequel.application_timezone.should == nil
@@ -41,7 +41,7 @@ describe Sequel::Dataset, " graphing" do
41
41
  ds = @ds1.from_self.from_self.graph(@ds2.from_self.from_self, :x=>:id)
42
42
  ds.sql.should == 'SELECT t1.id, t1.x, t1.y, t2.id AS t2_id, t2.x AS t2_x, t2.y AS t2_y, t2.graph_id FROM (SELECT * FROM (SELECT * FROM points) AS t1) AS t1 LEFT OUTER JOIN (SELECT * FROM (SELECT * FROM (SELECT * FROM lines) AS t1) AS t1) AS t2 ON (t2.x = t1.id)'
43
43
  ds = @ds1.from(@ds1, @ds3).graph(@ds2.from_self, :x=>:id)
44
- ds.sql.should == 'SELECT t1.id, t1.x, t1.y, t3.id AS t3_id, t3.x AS t3_x, t3.y AS t3_y, t3.graph_id FROM (SELECT * FROM points) AS t1, (SELECT * FROM graphs) AS t2 LEFT OUTER JOIN (SELECT * FROM (SELECT * FROM lines) AS t1) AS t3 ON (t3.x = t1.id)'
44
+ ds.sql.should == 'SELECT t1.id, t1.x, t1.y, t3.id AS t3_id, t3.x AS t3_x, t3.y AS t3_y, t3.graph_id FROM (SELECT * FROM (SELECT * FROM points) AS t1, (SELECT * FROM graphs) AS t2) AS t1 LEFT OUTER JOIN (SELECT * FROM (SELECT * FROM lines) AS t1) AS t3 ON (t3.x = t1.id)'
45
45
  end
46
46
 
47
47
  it "#graph should accept a symbol table name as the dataset" do
@@ -825,5 +825,6 @@ context "Schema Parser" do
825
825
  @db.schema(:smallmoney).first.last[:type].should == :decimal
826
826
  @db.schema(:binary).first.last[:type].should == :blob
827
827
  @db.schema(:varbinary).first.last[:type].should == :blob
828
+ @db.schema(:enum).first.last[:type].should == :enum
828
829
  end
829
830
  end
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+ if (begin
3
+ require 'active_model'
4
+ true
5
+ rescue LoadError
6
+ end)
7
+ describe "ActiveModel plugin" do
8
+ before do
9
+ @c = Class.new(Sequel::Model) do
10
+ def delete; end
11
+ end
12
+ @c.plugin :active_model
13
+ @m = @c.new
14
+ @o = @c.load({})
15
+ end
16
+
17
+ specify "should be compliant to the ActiveModel spec" do
18
+ s = ''
19
+ IO.popen('-') do |f|
20
+ if f
21
+ s = f.read
22
+ else
23
+ ActiveModel::Lint.test(@m)
24
+ end
25
+ end
26
+ s.should =~ /0 failures, 0 errors/
27
+ end
28
+
29
+ specify "to_model should return self" do
30
+ @m.to_model.object_id.should == @m.object_id
31
+ end
32
+
33
+ specify "new_record? should be aliased to new" do
34
+ @m.new_record?.should == true
35
+ @o.new_record?.should == false
36
+ end
37
+
38
+ specify "new_record? should be aliased to new" do
39
+ @m.destroyed?.should == false
40
+ @o.destroyed?.should == false
41
+ @m.destroy
42
+ @o.destroy
43
+ @m.destroyed?.should == true
44
+ @o.destroyed?.should == true
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,108 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "AssociationDependencies plugin" do
4
+ before do
5
+ mods = @mods = []
6
+ @c = Class.new(Sequel::Model)
7
+ @c.plugin :association_dependencies
8
+ @Artist = Class.new(@c).set_dataset(:artists)
9
+ ds1 = @Artist.dataset
10
+ def ds1.fetch_rows(s)
11
+ (MODEL_DB.sqls ||= []) << s
12
+ yield({:id=>2, :name=>'Ar'})
13
+ end
14
+ @Album = Class.new(@c).set_dataset(:albums)
15
+ ds1 = @Album.dataset
16
+ def ds1.fetch_rows(s)
17
+ (MODEL_DB.sqls ||= []) << s
18
+ yield({:id=>1, :name=>'Al', :artist_id=>2})
19
+ end
20
+ @Artist.columns :id, :name
21
+ @Album.columns :id, :name, :artist_id
22
+ @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id
23
+ @Artist.many_to_many :other_artists, :class=>@artist, :join_table=>:aoa, :left_key=>:l, :right_key=>:r
24
+ @Album.many_to_one :artist, :class=>@Artist
25
+ MODEL_DB.reset
26
+ end
27
+
28
+ specify "should allow destroying associated many_to_one associated object" do
29
+ @Album.add_association_dependencies :artist=>:destroy
30
+ @Album.load(:id=>1, :name=>'Al', :artist_id=>2).destroy
31
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (id = 1)', 'SELECT * FROM artists WHERE (artists.id = 2)', 'DELETE FROM artists WHERE (id = 2)']
32
+ end
33
+
34
+ specify "should allow deleting associated many_to_one associated object" do
35
+ @Album.add_association_dependencies :artist=>:delete
36
+ @Album.load(:id=>1, :name=>'Al', :artist_id=>2).destroy
37
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (id = 1)', 'DELETE FROM artists WHERE (artists.id = 2)']
38
+ end
39
+
40
+ specify "should allow destroying associated one_to_many objects" do
41
+ @Artist.add_association_dependencies :albums=>:destroy
42
+ @Artist.load(:id=>2, :name=>'Ar').destroy
43
+ MODEL_DB.sqls.should == ['SELECT * FROM albums WHERE (albums.artist_id = 2)', 'DELETE FROM albums WHERE (id = 1)', 'DELETE FROM artists WHERE (id = 2)']
44
+ end
45
+
46
+ specify "should allow deleting associated one_to_many objects" do
47
+ @Artist.add_association_dependencies :albums=>:delete
48
+ @Artist.load(:id=>2, :name=>'Ar').destroy
49
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (albums.artist_id = 2)', 'DELETE FROM artists WHERE (id = 2)']
50
+ end
51
+
52
+ specify "should allow nullifying associated one_to_many objects" do
53
+ @Artist.add_association_dependencies :albums=>:nullify
54
+ @Artist.load(:id=>2, :name=>'Ar').destroy
55
+ MODEL_DB.sqls.should == ['UPDATE albums SET artist_id = NULL WHERE (artist_id = 2)', 'DELETE FROM artists WHERE (id = 2)']
56
+ end
57
+
58
+ specify "should allow nullifying associated many_to_many associations" do
59
+ @Artist.add_association_dependencies :other_artists=>:nullify
60
+ @Artist.load(:id=>2, :name=>'Ar').destroy
61
+ MODEL_DB.sqls.should == ['DELETE FROM aoa WHERE (l = 2)', 'DELETE FROM artists WHERE (id = 2)']
62
+ end
63
+
64
+ specify "should raise an error if attempting to nullify a many_to_one association" do
65
+ proc{@Album.add_association_dependencies :artist=>:nullify}.should raise_error(Sequel::Error)
66
+ end
67
+
68
+ specify "should raise an error if using an unrecognized dependence action" do
69
+ proc{@Album.add_association_dependencies :artist=>:blah}.should raise_error(Sequel::Error)
70
+ end
71
+
72
+ specify "should raise an error if a nonexistent association is used" do
73
+ proc{@Album.add_association_dependencies :blah=>:delete}.should raise_error(Sequel::Error)
74
+ end
75
+
76
+ specify "should raise an error if a invalid association type is used" do
77
+ @Artist.plugin :many_through_many
78
+ @Artist.many_through_many :other_albums, [[:id, :id, :id]]
79
+ proc{@Artist.add_association_dependencies :other_albums=>:nullify}.should raise_error(Sequel::Error)
80
+ end
81
+
82
+ specify "should raise an error if using a many_to_many association type without nullify" do
83
+ proc{@Artist.add_association_dependencies :other_artists=>:delete}.should raise_error(Sequel::Error)
84
+ end
85
+
86
+ specify "should allow specifying association dependencies in the plugin call" do
87
+ @Album.plugin :association_dependencies, :artist=>:destroy
88
+ @Album.load(:id=>1, :name=>'Al', :artist_id=>2).destroy
89
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (id = 1)', 'SELECT * FROM artists WHERE (artists.id = 2)', 'DELETE FROM artists WHERE (id = 2)']
90
+ end
91
+
92
+ specify "should work with subclasses" do
93
+ c = Class.new(@Album)
94
+ c.add_association_dependencies :artist=>:destroy
95
+ c.load(:id=>1, :name=>'Al', :artist_id=>2).destroy
96
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (id = 1)', 'SELECT * FROM artists WHERE (artists.id = 2)', 'DELETE FROM artists WHERE (id = 2)']
97
+ MODEL_DB.reset
98
+
99
+ @Album.load(:id=>1, :name=>'Al', :artist_id=>2).destroy
100
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (id = 1)']
101
+ MODEL_DB.reset
102
+
103
+ @Album.add_association_dependencies :artist=>:destroy
104
+ c2 = Class.new(@Album)
105
+ c2.load(:id=>1, :name=>'Al', :artist_id=>2).destroy
106
+ MODEL_DB.sqls.should == ['DELETE FROM albums WHERE (id = 1)', 'SELECT * FROM artists WHERE (artists.id = 2)', 'DELETE FROM artists WHERE (id = 2)']
107
+ end
108
+ end
@@ -0,0 +1,252 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "class_table_inheritance plugin" do
4
+ before do
5
+ @db = db = MODEL_DB.clone
6
+ def db.schema(table, opts={})
7
+ {:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}]],
8
+ :managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}]],
9
+ :executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}]],
10
+ :staff=>[[:id, {:type=>:integer}], [:manager_id, {:type=>:integer}]],
11
+ }[table]
12
+ end
13
+ def db.dataset(*args)
14
+ ds = super(*args)
15
+ def ds.columns
16
+ {[:employees]=>[:id, :name, :kind],
17
+ [:managers]=>[:id, :num_staff],
18
+ [:executives]=>[:id, :num_managers],
19
+ [:staff]=>[:id, :manager_id],
20
+ [:employees, :managers]=>[:id, :name, :kind, :num_staff],
21
+ [:employees, :managers, :executives]=>[:id, :name, :kind, :num_staff, :num_managers],
22
+ [:employees, :staff]=>[:id, :name, :kind, :manager_id],
23
+ }[opts[:from] + (opts[:join] || []).map{|x| x.table}]
24
+ end
25
+ def ds.insert(*args)
26
+ db << insert_sql(*args)
27
+ 1
28
+ end
29
+ ds
30
+ end
31
+ class ::Employee < Sequel::Model(db)
32
+ def _refresh(x); @values[:id] = 1 end
33
+ def self.columns
34
+ dataset.columns
35
+ end
36
+ plugin :class_table_inheritance, :key=>:kind, :table_map=>{:Staff=>:staff}
37
+ end
38
+ class ::Manager < Employee
39
+ one_to_many :staff_members, :class=>:Staff
40
+ end
41
+ class ::Executive < Manager
42
+ end
43
+ class ::Staff < Employee
44
+ many_to_one :manager
45
+ end
46
+ @ds = Employee.dataset
47
+ @db.reset
48
+ end
49
+ after do
50
+ Object.send(:remove_const, :Executive)
51
+ Object.send(:remove_const, :Manager)
52
+ Object.send(:remove_const, :Staff)
53
+ Object.send(:remove_const, :Employee)
54
+ end
55
+
56
+ specify "should have simple_table = nil" do
57
+ Employee.simple_table.should == nil
58
+ Manager.simple_table.should == nil
59
+ Executive.simple_table.should == nil
60
+ Staff.simple_table.should == nil
61
+ end
62
+
63
+ specify "should use a joined dataset in subclasses" do
64
+ Employee.dataset.sql.should == 'SELECT * FROM employees'
65
+ Manager.dataset.sql.should == 'SELECT * FROM employees INNER JOIN managers USING (id)'
66
+ Executive.dataset.sql.should == 'SELECT * FROM employees INNER JOIN managers USING (id) INNER JOIN executives USING (id)'
67
+ Staff.dataset.sql.should == 'SELECT * FROM employees INNER JOIN staff USING (id)'
68
+ end
69
+
70
+ it "should return rows with the correct class based on the polymorphic_key value" do
71
+ def @ds.fetch_rows(sql)
72
+ yield({:kind=>'Employee'})
73
+ yield({:kind=>'Manager'})
74
+ yield({:kind=>'Executive'})
75
+ yield({:kind=>'Staff'})
76
+ end
77
+ Employee.all.collect{|x| x.class}.should == [Employee, Manager, Executive, Staff]
78
+ end
79
+
80
+ it "should return rows with the correct class based on the polymorphic_key value for subclasses" do
81
+ ds = Manager.dataset
82
+ def ds.fetch_rows(sql)
83
+ yield({:kind=>'Manager'})
84
+ yield({:kind=>'Executive'})
85
+ end
86
+ Manager.all.collect{|x| x.class}.should == [Manager, Executive]
87
+ end
88
+
89
+ it "should return rows with the current class if cti_key is nil" do
90
+ Employee.plugin(:class_table_inheritance)
91
+ def @ds.fetch_rows(sql)
92
+ yield({:kind=>'Employee'})
93
+ yield({:kind=>'Manager'})
94
+ yield({:kind=>'Executive'})
95
+ yield({:kind=>'Staff'})
96
+ end
97
+ Employee.all.collect{|x| x.class}.should == [Employee, Employee, Employee, Employee]
98
+ end
99
+
100
+ it "should return rows with the current class if cti_key is nil in subclasses" do
101
+ Employee.plugin(:class_table_inheritance)
102
+ Object.send(:remove_const, :Executive)
103
+ Object.send(:remove_const, :Manager)
104
+ class ::Manager < Employee
105
+ end
106
+ class ::Executive < Manager
107
+ end
108
+ ds = Manager.dataset
109
+ def ds.fetch_rows(sql)
110
+ yield({:kind=>'Manager'})
111
+ yield({:kind=>'Executive'})
112
+ end
113
+ Manager.all.collect{|x| x.class}.should == [Manager, Manager]
114
+ end
115
+
116
+ it "should fallback to the main class if the given class does not exist" do
117
+ def @ds.fetch_rows(sql)
118
+ yield({:kind=>'Employee'})
119
+ yield({:kind=>'Manager'})
120
+ yield({:kind=>'Blah'})
121
+ yield({:kind=>'Staff'})
122
+ end
123
+ Employee.all.collect{|x| x.class}.should == [Employee, Manager, Employee, Staff]
124
+ end
125
+
126
+ it "should fallback to the main class if the given class does not exist in subclasses" do
127
+ ds = Manager.dataset
128
+ def ds.fetch_rows(sql)
129
+ yield({:kind=>'Manager'})
130
+ yield({:kind=>'Executive'})
131
+ yield({:kind=>'Blah'})
132
+ end
133
+ Manager.all.collect{|x| x.class}.should == [Manager, Executive, Manager]
134
+ end
135
+
136
+ it "should add a before_create hook that sets the model class name for the key" do
137
+ Employee.create
138
+ @db.sqls.should == ["INSERT INTO employees (kind) VALUES ('Employee')"]
139
+ end
140
+
141
+ it "should add a before_create hook that sets the model class name for the key in subclasses" do
142
+ Executive.create
143
+ @db.sqls.should == ["INSERT INTO employees (kind) VALUES ('Executive')",
144
+ "INSERT INTO managers (id) VALUES (1)",
145
+ "INSERT INTO executives (id) VALUES (1)"]
146
+ end
147
+
148
+ it "should ignore existing cti_key value" do
149
+ Employee.create(:kind=>'Manager')
150
+ @db.sqls.should == ["INSERT INTO employees (kind) VALUES ('Employee')"]
151
+ end
152
+
153
+ it "should ignore existing cti_key value in subclasses" do
154
+ Manager.create(:kind=>'Executive')
155
+ @db.sqls.should == ["INSERT INTO employees (kind) VALUES ('Manager')",
156
+ "INSERT INTO managers (id) VALUES (1)"]
157
+ end
158
+
159
+ it "should raise an error if attempting to create an anonymous subclass" do
160
+ proc{Class.new(Manager)}.should raise_error(Sequel::Error)
161
+ end
162
+
163
+ it "should allow specifying a map of names to tables to override implicit mapping" do
164
+ Manager.dataset.sql.should == 'SELECT * FROM employees INNER JOIN managers USING (id)'
165
+ Staff.dataset.sql.should == 'SELECT * FROM employees INNER JOIN staff USING (id)'
166
+ end
167
+
168
+ it "should lazily load attributes for columns in subclass tables" do
169
+ ds = Manager.dataset
170
+ def ds.fetch_rows(sql)
171
+ @db << sql
172
+ yield({:id=>1, :name=>'J', :kind=>'Executive', :num_staff=>2})
173
+ end
174
+ m = Manager[1]
175
+ @db.sqls.should == ['SELECT * FROM employees INNER JOIN managers USING (id) WHERE (id = 1) LIMIT 1']
176
+ @db.reset
177
+ ds = Executive.dataset
178
+ def ds.fetch_rows(sql)
179
+ @db << sql
180
+ yield({:num_managers=>3})
181
+ end
182
+ m.num_managers.should == 3
183
+ @db.sqls.should == ['SELECT num_managers FROM employees INNER JOIN managers USING (id) INNER JOIN executives USING (id) WHERE (id = 1) LIMIT 1']
184
+ m.values.should == {:id=>1, :name=>'J', :kind=>'Executive', :num_staff=>2, :num_managers=>3}
185
+ end
186
+
187
+ it "should include schema for columns for tables for ancestor classes" do
188
+ Employee.db_schema.should == {:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}}
189
+ Manager.db_schema.should == {:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :num_staff=>{:type=>:integer}}
190
+ Executive.db_schema.should == {:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :num_staff=>{:type=>:integer}, :num_managers=>{:type=>:integer}}
191
+ Staff.db_schema.should == {:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :manager_id=>{:type=>:integer}}
192
+ end
193
+
194
+ it "should use the correct primary key (which should have the same name in all subclasses)" do
195
+ [Employee, Manager, Executive, Staff].each{|c| c.primary_key.should == :id}
196
+ end
197
+
198
+ it "should have table_name return the table name of the most specific table" do
199
+ Employee.table_name.should == :employees
200
+ Manager.table_name.should == :managers
201
+ Executive.table_name.should == :executives
202
+ Staff.table_name.should == :staff
203
+ end
204
+
205
+ it "should delete the correct rows from all tables when deleting" do
206
+ Executive.load(:id=>1).delete
207
+ @db.sqls.should == ["DELETE FROM executives WHERE (id = 1)", "DELETE FROM managers WHERE (id = 1)", "DELETE FROM employees WHERE (id = 1)"]
208
+ end
209
+
210
+ it "should insert the correct rows into all tables when inserting" do
211
+ Executive.create(:num_managers=>3, :num_staff=>2, :name=>'E')
212
+ @db.sqls.length.should == 3
213
+ @db.sqls[0].should =~ /INSERT INTO employees \((name|kind), (name|kind)\) VALUES \('(E|Executive)', '(E|Executive)'\)/
214
+ @db.sqls[1].should =~ /INSERT INTO managers \((num_staff|id), (num_staff|id)\) VALUES \([12], [12]\)/
215
+ @db.sqls[2].should =~ /INSERT INTO executives \((num_managers|id), (num_managers|id)\) VALUES \([13], [13]\)/
216
+ end
217
+
218
+ it "should insert the correct rows into all tables with a given primary key" do
219
+ e = Executive.new(:num_managers=>3, :num_staff=>2, :name=>'E')
220
+ e.id = 2
221
+ e.save
222
+ @db.sqls.length.should == 3
223
+ @db.sqls[0].should =~ /INSERT INTO employees \((name|kind|id), (name|kind|id), (name|kind|id)\) VALUES \(('E'|'Executive'|2), ('E'|'Executive'|2), ('E'|'Executive'|2)\)/
224
+ @db.sqls[1].should =~ /INSERT INTO managers \((num_staff|id), (num_staff|id)\) VALUES \(2, 2\)/
225
+ @db.sqls[2].should =~ /INSERT INTO executives \((num_managers|id), (num_managers|id)\) VALUES \([23], [23]\)/
226
+ end
227
+
228
+ it "should update the correct rows in all tables when updating" do
229
+ Executive.load(:id=>2).update(:num_managers=>3, :num_staff=>2, :name=>'E')
230
+ @db.sqls.should == ["UPDATE employees SET name = 'E' WHERE (id = 2)", "UPDATE managers SET num_staff = 2 WHERE (id = 2)", "UPDATE executives SET num_managers = 3 WHERE (id = 2)"]
231
+ end
232
+
233
+ it "should handle many_to_one relationships correctly" do
234
+ ds = Manager.dataset
235
+ def ds.fetch_rows(sql)
236
+ @db << sql
237
+ yield({:id=>3, :name=>'E', :kind=>'Executive', :num_managers=>3})
238
+ end
239
+ Staff.load(:manager_id=>3).manager.should == Executive.load(:id=>3, :name=>'E', :kind=>'Executive', :num_managers=>3)
240
+ @db.sqls.should == ['SELECT * FROM employees INNER JOIN managers USING (id) WHERE (managers.id = 3) LIMIT 1']
241
+ end
242
+
243
+ it "should handle one_to_many relationships correctly" do
244
+ ds = Staff.dataset
245
+ def ds.fetch_rows(sql)
246
+ @db << sql
247
+ yield({:id=>1, :name=>'S', :kind=>'Staff', :manager_id=>3})
248
+ end
249
+ Executive.load(:id=>3).staff_members.should == [Staff.load(:id=>1, :name=>'S', :kind=>'Staff', :manager_id=>3)]
250
+ @db.sqls.should == ['SELECT * FROM employees INNER JOIN staff USING (id) WHERE (staff.manager_id = 3)']
251
+ end
252
+ end