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.
- 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
|