sequel 3.4.0 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +84 -0
- data/Rakefile +1 -1
- data/doc/cheat_sheet.rdoc +5 -2
- data/doc/opening_databases.rdoc +2 -0
- data/doc/release_notes/3.5.0.txt +510 -0
- data/lib/sequel/adapters/ado.rb +3 -1
- data/lib/sequel/adapters/ado/mssql.rb +2 -2
- data/lib/sequel/adapters/do.rb +2 -11
- data/lib/sequel/adapters/do/mysql.rb +7 -0
- data/lib/sequel/adapters/do/postgres.rb +2 -2
- data/lib/sequel/adapters/firebird.rb +3 -3
- data/lib/sequel/adapters/informix.rb +3 -3
- data/lib/sequel/adapters/jdbc/h2.rb +3 -3
- data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
- data/lib/sequel/adapters/mysql.rb +60 -21
- data/lib/sequel/adapters/odbc.rb +1 -1
- data/lib/sequel/adapters/openbase.rb +3 -3
- data/lib/sequel/adapters/oracle.rb +1 -5
- data/lib/sequel/adapters/postgres.rb +3 -3
- data/lib/sequel/adapters/shared/mssql.rb +142 -33
- data/lib/sequel/adapters/shared/mysql.rb +54 -31
- data/lib/sequel/adapters/shared/oracle.rb +17 -6
- data/lib/sequel/adapters/shared/postgres.rb +7 -7
- data/lib/sequel/adapters/shared/progress.rb +3 -3
- data/lib/sequel/adapters/shared/sqlite.rb +3 -17
- data/lib/sequel/connection_pool.rb +4 -6
- data/lib/sequel/core.rb +29 -113
- data/lib/sequel/database.rb +14 -12
- data/lib/sequel/dataset.rb +8 -21
- data/lib/sequel/dataset/convenience.rb +1 -1
- data/lib/sequel/dataset/graph.rb +9 -2
- data/lib/sequel/dataset/sql.rb +170 -104
- data/lib/sequel/exceptions.rb +3 -0
- data/lib/sequel/extensions/looser_typecasting.rb +21 -0
- data/lib/sequel/extensions/named_timezones.rb +61 -0
- data/lib/sequel/extensions/schema_dumper.rb +7 -1
- data/lib/sequel/extensions/sql_expr.rb +122 -0
- data/lib/sequel/extensions/string_date_time.rb +4 -4
- data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
- data/lib/sequel/model/associations.rb +105 -45
- data/lib/sequel/model/base.rb +37 -28
- data/lib/sequel/plugins/active_model.rb +35 -0
- data/lib/sequel/plugins/association_dependencies.rb +96 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
- data/lib/sequel/plugins/force_encoding.rb +61 -0
- data/lib/sequel/plugins/many_through_many.rb +32 -11
- data/lib/sequel/plugins/nested_attributes.rb +7 -2
- data/lib/sequel/plugins/subclasses.rb +45 -0
- data/lib/sequel/plugins/touch.rb +118 -0
- data/lib/sequel/plugins/typecast_on_load.rb +61 -0
- data/lib/sequel/sql.rb +31 -30
- data/lib/sequel/timezones.rb +161 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +262 -0
- data/spec/adapters/mysql_spec.rb +46 -8
- data/spec/adapters/postgres_spec.rb +6 -3
- data/spec/adapters/spec_helper.rb +21 -0
- data/spec/adapters/sqlite_spec.rb +1 -1
- data/spec/core/connection_pool_spec.rb +1 -1
- data/spec/core/database_spec.rb +27 -1
- data/spec/core/dataset_spec.rb +63 -1
- data/spec/core/object_graph_spec.rb +1 -1
- data/spec/core/schema_spec.rb +1 -0
- data/spec/extensions/active_model_spec.rb +47 -0
- data/spec/extensions/association_dependencies_spec.rb +108 -0
- data/spec/extensions/class_table_inheritance_spec.rb +252 -0
- data/spec/extensions/force_encoding_spec.rb +75 -0
- data/spec/extensions/looser_typecasting_spec.rb +39 -0
- data/spec/extensions/many_through_many_spec.rb +60 -2
- data/spec/extensions/named_timezones_spec.rb +72 -0
- data/spec/extensions/nested_attributes_spec.rb +29 -1
- data/spec/extensions/schema_dumper_spec.rb +10 -0
- data/spec/extensions/spec_helper.rb +1 -1
- data/spec/extensions/sql_expr_spec.rb +89 -0
- data/spec/extensions/subclasses_spec.rb +52 -0
- data/spec/extensions/thread_local_timezones_spec.rb +45 -0
- data/spec/extensions/touch_spec.rb +155 -0
- data/spec/extensions/typecast_on_load_spec.rb +60 -0
- data/spec/integration/database_test.rb +8 -0
- data/spec/integration/dataset_test.rb +9 -9
- data/spec/integration/plugin_test.rb +139 -0
- data/spec/integration/schema_test.rb +7 -7
- data/spec/integration/spec_helper.rb +32 -1
- data/spec/integration/timezone_test.rb +3 -3
- data/spec/integration/transaction_test.rb +1 -1
- data/spec/integration/type_test.rb +6 -6
- data/spec/model/association_reflection_spec.rb +18 -0
- data/spec/model/associations_spec.rb +169 -9
- data/spec/model/base_spec.rb +2 -0
- data/spec/model/eager_loading_spec.rb +82 -2
- data/spec/model/model_spec.rb +8 -1
- data/spec/model/record_spec.rb +52 -9
- 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
|
-
|
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
|
-
|
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.
|
88
|
+
50.times{Thread.new{@cpool.hold{sleep 0.001}}}
|
89
89
|
@cpool.created_count.should == @max_size
|
90
90
|
end
|
91
91
|
|
data/spec/core/database_spec.rb
CHANGED
@@ -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
|
data/spec/core/dataset_spec.rb
CHANGED
@@ -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
|
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
|
data/spec/core/schema_spec.rb
CHANGED
@@ -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
|