sequel 3.4.0 → 3.5.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 (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