sequel 4.11.0 → 4.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/Rakefile +1 -5
  4. data/doc/opening_databases.rdoc +5 -1
  5. data/doc/release_notes/4.12.0.txt +105 -0
  6. data/lib/sequel/adapters/jdbc.rb +1 -0
  7. data/lib/sequel/adapters/oracle.rb +1 -0
  8. data/lib/sequel/adapters/postgres.rb +17 -8
  9. data/lib/sequel/adapters/shared/cubrid.rb +2 -1
  10. data/lib/sequel/adapters/shared/db2.rb +1 -0
  11. data/lib/sequel/adapters/shared/mssql.rb +1 -0
  12. data/lib/sequel/adapters/shared/postgres.rb +23 -2
  13. data/lib/sequel/adapters/shared/sqlanywhere.rb +1 -0
  14. data/lib/sequel/adapters/sqlite.rb +11 -5
  15. data/lib/sequel/database/query.rb +14 -1
  16. data/lib/sequel/dataset/prepared_statements.rb +2 -1
  17. data/lib/sequel/dataset/query.rb +48 -37
  18. data/lib/sequel/dataset/sql.rb +0 -39
  19. data/lib/sequel/extensions/pg_interval.rb +1 -1
  20. data/lib/sequel/extensions/pg_static_cache_updater.rb +11 -5
  21. data/lib/sequel/model/associations.rb +2 -2
  22. data/lib/sequel/plugins/auto_validations.rb +16 -4
  23. data/lib/sequel/plugins/hook_class_methods.rb +1 -1
  24. data/lib/sequel/plugins/nested_attributes.rb +72 -59
  25. data/lib/sequel/plugins/prepared_statements.rb +16 -5
  26. data/lib/sequel/plugins/prepared_statements_associations.rb +14 -0
  27. data/lib/sequel/sql.rb +2 -43
  28. data/lib/sequel/version.rb +1 -1
  29. data/spec/adapters/postgres_spec.rb +49 -20
  30. data/spec/bin_spec.rb +2 -2
  31. data/spec/core/dataset_spec.rb +18 -6
  32. data/spec/core/schema_spec.rb +2 -1
  33. data/spec/extensions/auto_validations_spec.rb +23 -2
  34. data/spec/extensions/nested_attributes_spec.rb +32 -1
  35. data/spec/extensions/pg_static_cache_updater_spec.rb +12 -0
  36. data/spec/extensions/prepared_statements_associations_spec.rb +17 -17
  37. data/spec/extensions/prepared_statements_spec.rb +11 -8
  38. data/spec/integration/plugin_test.rb +43 -0
  39. data/spec/integration/schema_test.rb +7 -0
  40. data/spec/model/eager_loading_spec.rb +9 -0
  41. metadata +4 -2
@@ -53,6 +53,20 @@ module Sequel
53
53
  opts.send(:cached_fetch, :prepared_statement) do
54
54
  unless opts[:instance_specific]
55
55
  ds, bv = _associated_dataset(opts, {}).unbind
56
+
57
+ f = ds.opts[:from]
58
+ if f && f.length == 1
59
+ s = ds.opts[:select]
60
+ if ds.opts[:join]
61
+ if opts.eager_loading_use_associated_key? && s && s.length == 1 && s.first.is_a?(SQL::ColumnAll)
62
+ table = s.first.table
63
+ ds = ds.select(*opts.associated_class.columns.map{|c| Sequel.identifier(c).qualify(table)})
64
+ end
65
+ elsif !s || s.empty?
66
+ ds = ds.select(*opts.associated_class.columns.map{|c| Sequel.identifier(c)})
67
+ end
68
+ end
69
+
56
70
  if bv.length != assoc_bv.length
57
71
  h = {}
58
72
  bv.each do |k,v|
data/lib/sequel/sql.rb CHANGED
@@ -1339,16 +1339,6 @@ module Sequel
1339
1339
  end
1340
1340
  end
1341
1341
 
1342
- # REMOVE411
1343
- class EmulatedFunction < Function
1344
- def self.new(name, *args)
1345
- Deprecation.deprecate("Sequel::SQL::EmulatedFunction", "Please use Sequel::SQL::Function.new!(name, args, :emulate=>true) to create an emulated SQL function")
1346
- Function.new!(name, args, :emulate=>true)
1347
- end
1348
-
1349
- to_s_method :emulated_function_sql
1350
- end
1351
-
1352
1342
  class GenericExpression
1353
1343
  include AliasMethods
1354
1344
  include BooleanMethods
@@ -1394,18 +1384,9 @@ module Sequel
1394
1384
  attr_reader :table_expr
1395
1385
 
1396
1386
  # Create an object with the given join_type and table expression.
1397
- def initialize(join_type, table, table_alias = nil)
1387
+ def initialize(join_type, table_expr)
1398
1388
  @join_type = join_type
1399
-
1400
- @table_expr = if table.is_a?(AliasedExpression)
1401
- table
1402
- # REMOVE411
1403
- elsif table_alias
1404
- Deprecation.deprecate("The table_alias argument to Sequel::SQL::JoinClause#initialize", "Please use a Sequel::SQL::AliasedExpression as the table argument instead.")
1405
- AliasedExpression.new(table, table_alias)
1406
- else
1407
- table
1408
- end
1389
+ @table_expr = table_expr
1409
1390
  end
1410
1391
 
1411
1392
  # The table/set related to the JOIN, without any alias.
@@ -1827,28 +1808,6 @@ module Sequel
1827
1808
  to_s_method :window_sql, '@opts'
1828
1809
  end
1829
1810
 
1830
- # REMOVE411
1831
- class WindowFunction < GenericExpression
1832
- # The function to use, should be an <tt>SQL::Function</tt>.
1833
- attr_reader :function
1834
-
1835
- # The window to use, should be an <tt>SQL::Window</tt>.
1836
- attr_reader :window
1837
-
1838
- def self.new(function, window)
1839
- Deprecation.deprecate("Sequel::SQL::WindowFunction", "Please use Sequel::SQL::Function.new(name, *args).over(...) to create an SQL window function")
1840
- function.over(window)
1841
- end
1842
-
1843
- # Set the function and window.
1844
- def initialize(function, window)
1845
- Deprecation.deprecate("Sequel::SQL::WindowFunction", "Please use Sequel::SQL::Function.new(name, *args).over(...) to create an SQL window function")
1846
- @function, @window = function, window
1847
- end
1848
-
1849
- to_s_method :window_function_sql, '@function, @window'
1850
- end
1851
-
1852
1811
  # A +Wrapper+ is a simple way to wrap an existing object so that it supports
1853
1812
  # the Sequel DSL.
1854
1813
  class Wrapper < GenericExpression
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 4
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 11
6
+ MINOR = 12
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -179,6 +179,19 @@ describe "A PostgreSQL database" do
179
179
  @db.server_version.should > 70000
180
180
  end
181
181
 
182
+ specify "should create a dataset using the VALUES clause via #values" do
183
+ @db.values([[1, 2], [3, 4]]).map([:column1, :column2]).should == [[1, 2], [3, 4]]
184
+ end
185
+
186
+ specify "should support ordering and limiting with #values" do
187
+ @db.values([[1, 2], [3, 4]]).reverse(:column2, :column1).limit(1).map([:column1, :column2]).should == [[3, 4]]
188
+ @db.values([[1, 2], [3, 4]]).reverse(:column2, :column1).offset(1).map([:column1, :column2]).should == [[1, 2]]
189
+ end
190
+
191
+ specify "should support subqueries with #values" do
192
+ @db.values([[1, 2]]).from_self.cross_join(@db.values([[3, 4]]).as(:x, [:c1, :c2])).map([:column1, :column2, :c1, :c2]).should == [[1, 2, 3, 4]]
193
+ end
194
+
182
195
  specify "should respect the :read_only option per-savepoint" do
183
196
  proc{@db.transaction{@db.transaction(:savepoint=>true, :read_only=>true){@db[:public__testfk].insert}}}.should raise_error(Sequel::DatabaseError)
184
197
  proc{@db.transaction(:auto_savepoint=>true, :read_only=>true){@db.transaction(:read_only=>false){@db[:public__testfk].insert}}}.should raise_error(Sequel::DatabaseError)
@@ -257,6 +270,12 @@ describe "A PostgreSQL database" do
257
270
  ds.insert(:id=>1)
258
271
  ds.select_map(:id).should == [1]
259
272
  end
273
+
274
+ specify "should have notice receiver receive notices" do
275
+ a = nil
276
+ Sequel.connect(DB.opts.merge(:notice_receiver=>proc{|r| a = r.result_error_message})){|db| db.do("BEGIN\nRAISE WARNING 'foo';\nEND;")}
277
+ a.should == "WARNING: foo\n"
278
+ end if DB.adapter_scheme == :postgres && SEQUEL_POSTGRES_USES_PG && DB.server_version >= 90000
260
279
  end
261
280
 
262
281
  describe "A PostgreSQL database with domain types" do
@@ -1035,6 +1054,18 @@ describe "Postgres::Dataset#insert" do
1035
1054
  @ds.first(:xid=>h[:xid])[:value].should == 10
1036
1055
  end
1037
1056
 
1057
+ specify "should have insert_select respect existing returning clause" do
1058
+ h = @ds.returning(:value___v, :xid___x).insert_select(:value=>10)
1059
+ h[:v].should == 10
1060
+ @ds.first(:xid=>h[:x])[:value].should == 10
1061
+ end
1062
+
1063
+ specify "should have prepared insert_select respect existing returning clause" do
1064
+ h = @ds.returning(:value___v, :xid___x).prepare(:insert_select, :insert_select, :value=>10).call
1065
+ h[:v].should == 10
1066
+ @ds.first(:xid=>h[:x])[:value].should == 10
1067
+ end
1068
+
1038
1069
  specify "should correctly return the inserted record's primary key value" do
1039
1070
  value1 = 10
1040
1071
  id1 = @ds.insert(:value=>value1)
@@ -2466,14 +2497,11 @@ describe 'PostgreSQL hstore handling' do
2466
2497
  @ds.get(h2.merge(h3).keys.length).should == 2
2467
2498
  @ds.get(h1.merge(h3).keys.length).should == 3
2468
2499
 
2469
- unless [:do].include?(@db.adapter_scheme)
2470
- # Broken DataObjects thinks operators with ? represent placeholders
2471
- @ds.get(h1.contain_all(%w'a c')).should == true
2472
- @ds.get(h1.contain_all(%w'a d')).should == false
2500
+ @ds.get(h1.contain_all(%w'a c')).should == true
2501
+ @ds.get(h1.contain_all(%w'a d')).should == false
2473
2502
 
2474
- @ds.get(h1.contain_any(%w'a d')).should == true
2475
- @ds.get(h1.contain_any(%w'e d')).should == false
2476
- end
2503
+ @ds.get(h1.contain_any(%w'a d')).should == true
2504
+ @ds.get(h1.contain_any(%w'e d')).should == false
2477
2505
 
2478
2506
  @ds.get(h1.contains(h2)).should == true
2479
2507
  @ds.get(h1.contains(h3)).should == false
@@ -2491,18 +2519,16 @@ describe 'PostgreSQL hstore handling' do
2491
2519
 
2492
2520
  @ds.from(Sequel.hstore('a'=>'b', 'c'=>nil).op.each).order(:key).all.should == [{:key=>'a', :value=>'b'}, {:key=>'c', :value=>nil}]
2493
2521
 
2494
- unless [:do].include?(@db.adapter_scheme)
2495
- @ds.get(h1.has_key?('c')).should == true
2496
- @ds.get(h1.include?('c')).should == true
2497
- @ds.get(h1.key?('c')).should == true
2498
- @ds.get(h1.member?('c')).should == true
2499
- @ds.get(h1.exist?('c')).should == true
2500
- @ds.get(h1.has_key?('d')).should == false
2501
- @ds.get(h1.include?('d')).should == false
2502
- @ds.get(h1.key?('d')).should == false
2503
- @ds.get(h1.member?('d')).should == false
2504
- @ds.get(h1.exist?('d')).should == false
2505
- end
2522
+ @ds.get(h1.has_key?('c')).should == true
2523
+ @ds.get(h1.include?('c')).should == true
2524
+ @ds.get(h1.key?('c')).should == true
2525
+ @ds.get(h1.member?('c')).should == true
2526
+ @ds.get(h1.exist?('c')).should == true
2527
+ @ds.get(h1.has_key?('d')).should == false
2528
+ @ds.get(h1.include?('d')).should == false
2529
+ @ds.get(h1.key?('d')).should == false
2530
+ @ds.get(h1.member?('d')).should == false
2531
+ @ds.get(h1.exist?('d')).should == false
2506
2532
 
2507
2533
  @ds.get(h1.hstore.hstore.hstore.keys.length).should == 2
2508
2534
  @ds.get(h1.keys.length).should == 2
@@ -3022,6 +3048,7 @@ describe 'PostgreSQL interval types' do
3022
3048
  ['1 second', '00:00:01', 1, [[:seconds, 1]]],
3023
3049
  ['1 minute', '00:01:00', 60, [[:seconds, 60]]],
3024
3050
  ['1 hour', '01:00:00', 3600, [[:seconds, 3600]]],
3051
+ ['123000 hours', '123000:00:00', 442800000, [[:seconds, 442800000]]],
3025
3052
  ['1 day', '1 day', 86400, [[:days, 1]]],
3026
3053
  ['1 week', '7 days', 86400*7, [[:days, 7]]],
3027
3054
  ['1 month', '1 mon', 86400*30, [[:months, 1]]],
@@ -3390,7 +3417,8 @@ describe 'pg_static_cache_updater extension' do
3390
3417
  q = Queue.new
3391
3418
  q1 = Queue.new
3392
3419
 
3393
- @db.listen_for_static_cache_updates(@Thing, :timeout=>0, :loop=>proc{q.push(nil); q1.pop.call})
3420
+ @db.listen_for_static_cache_updates(@Thing, :timeout=>0, :loop=>proc{q.push(nil); q1.pop.call}, :before_thread_exit=>proc{q.push(nil)})
3421
+
3394
3422
  q.pop
3395
3423
  q1.push(proc{@db[:things].insert(1, 'A')})
3396
3424
  q.pop
@@ -3405,5 +3433,6 @@ describe 'pg_static_cache_updater extension' do
3405
3433
  @Thing.all.should == []
3406
3434
 
3407
3435
  q1.push(proc{throw :stop})
3436
+ q.pop
3408
3437
  end
3409
3438
  end if DB.adapter_scheme == :postgres && SEQUEL_POSTGRES_USES_PG && DB.server_version >= 90000
data/spec/bin_spec.rb CHANGED
@@ -88,8 +88,8 @@ END
88
88
  DB2.tables.sort_by{|t| t.to_s}.should == [:a, :b]
89
89
  DB[:a].all.should == [{:a=>1, :name=>'foo'}]
90
90
  DB[:b].all.should == [{:a=>1}]
91
- DB2.schema(:a).should == [[:a, {:allow_null=>false, :default=>nil, :primary_key=>true, :db_type=>"integer", :type=>:integer, :ruby_default=>nil}], [:name, {:allow_null=>true, :default=>nil, :primary_key=>false, :db_type=>"varchar(255)", :type=>:string, :ruby_default=>nil}]]
92
- DB2.schema(:b).should == [[:a, {:allow_null=>true, :default=>nil, :primary_key=>false, :db_type=>"integer", :type=>:integer, :ruby_default=>nil}]]
91
+ DB2.schema(:a).map{|col, sch| [col, *sch.values_at(:allow_null, :default, :primary_key, :db_type, :type, :ruby_default)]}.should == [[:a, false, nil, true, "integer", :integer, nil], [:name, true, nil, false, "varchar(255)", :string, nil]]
92
+ DB2.schema(:b).map{|col, sch| [col, *sch.values_at(:allow_null, :default, :primary_key, :db_type, :type, :ruby_default)]}.should == [[:a, true, nil, false, "integer", :integer, nil]]
93
93
  DB2.indexes(:a).should == {}
94
94
  DB2.indexes(:b).should == {:b_a_index=>{:unique=>false, :columns=>[:a]}}
95
95
  DB2.foreign_key_list(:a).should == []
@@ -2127,12 +2127,20 @@ describe "Dataset#join_table" do
2127
2127
  @d.cross_join(:categories).sql.should == 'SELECT * FROM "items" CROSS JOIN "categories"'
2128
2128
  end
2129
2129
 
2130
- specify "should raise an error if additional arguments are provided to join methods that don't take conditions" do
2131
- proc{@d.natural_join(:categories, :id=>:id)}.should raise_error(ArgumentError)
2132
- proc{@d.natural_left_join(:categories, :id=>:id)}.should raise_error(ArgumentError)
2133
- proc{@d.natural_right_join(:categories, :id=>:id)}.should raise_error(ArgumentError)
2134
- proc{@d.natural_full_join(:categories, :id=>:id)}.should raise_error(ArgumentError)
2135
- proc{@d.cross_join(:categories, :id=>:id)}.should raise_error(ArgumentError)
2130
+ specify "should support options hashes for join methods that don't take conditions" do
2131
+ @d.natural_join(:categories, :table_alias=>:a).sql.should == 'SELECT * FROM "items" NATURAL JOIN "categories" AS "a"'
2132
+ @d.natural_left_join(:categories, :table_alias=>:a).sql.should == 'SELECT * FROM "items" NATURAL LEFT JOIN "categories" AS "a"'
2133
+ @d.natural_right_join(:categories, :table_alias=>:a).sql.should == 'SELECT * FROM "items" NATURAL RIGHT JOIN "categories" AS "a"'
2134
+ @d.natural_full_join(:categories, :table_alias=>:a).sql.should == 'SELECT * FROM "items" NATURAL FULL JOIN "categories" AS "a"'
2135
+ @d.cross_join(:categories, :table_alias=>:a).sql.should == 'SELECT * FROM "items" CROSS JOIN "categories" AS "a"'
2136
+ end
2137
+
2138
+ specify "should raise an error if non-hash arguments are provided to join methods that don't take conditions" do
2139
+ proc{@d.natural_join(:categories, nil)}.should raise_error(Sequel::Error)
2140
+ proc{@d.natural_left_join(:categories, nil)}.should raise_error(Sequel::Error)
2141
+ proc{@d.natural_right_join(:categories, nil)}.should raise_error(Sequel::Error)
2142
+ proc{@d.natural_full_join(:categories, nil)}.should raise_error(Sequel::Error)
2143
+ proc{@d.cross_join(:categories, nil)}.should raise_error(Sequel::Error)
2136
2144
  end
2137
2145
 
2138
2146
  specify "should raise an error if blocks are provided to join methods that don't pass them" do
@@ -2184,6 +2192,10 @@ describe "Dataset#join_table" do
2184
2192
  @d.from('stats').join('players', {:id => :player_id}, :implicit_qualifier=>:p).sql.should == 'SELECT * FROM "stats" INNER JOIN "players" ON ("players"."id" = "p"."player_id")'
2185
2193
  end
2186
2194
 
2195
+ specify "should support the :reset_implicit_qualifier option" do
2196
+ @d.from(:stats).join(:a, [:b], :reset_implicit_qualifier=>false).join(:players, {:id => :player_id}).sql.should == 'SELECT * FROM "stats" INNER JOIN "a" USING ("b") INNER JOIN "players" ON ("players"."id" = "stats"."player_id")'
2197
+ end
2198
+
2187
2199
  specify "should default :qualify option to default_join_table_qualification" do
2188
2200
  def @d.default_join_table_qualification; false; end
2189
2201
  @d.from('stats').join(:players, :id => :player_id).sql.should == 'SELECT * FROM "stats" INNER JOIN "players" ON ("id" = "player_id")'
@@ -1550,7 +1550,7 @@ describe "Schema Parser" do
1550
1550
  specify "should correctly parse all supported data types" do
1551
1551
  sm = Module.new do
1552
1552
  def schema_parse_table(t, opts)
1553
- [[:x, {:type=>schema_column_type(t.to_s)}]]
1553
+ [[:x, {:db_type=>t.to_s, :type=>schema_column_type(t.to_s)}]]
1554
1554
  end
1555
1555
  end
1556
1556
  @db.extend(sm)
@@ -1563,6 +1563,7 @@ describe "Schema Parser" do
1563
1563
  @db.schema(:"character varying").first.last[:type].should == :string
1564
1564
  @db.schema(:varchar).first.last[:type].should == :string
1565
1565
  @db.schema(:"varchar(255)").first.last[:type].should == :string
1566
+ @db.schema(:"varchar(255)").first.last[:max_length].should == 255
1566
1567
  @db.schema(:text).first.last[:type].should == :string
1567
1568
  @db.schema(:date).first.last[:type].should == :date
1568
1569
  @db.schema(:datetime).first.last[:type].should == :datetime
@@ -2,13 +2,13 @@ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
2
 
3
3
  describe "Sequel::Plugins::AutoValidations" do
4
4
  before do
5
- db = Sequel.mock(:fetch=>{:v=>1})
5
+ db = Sequel.mock(:fetch=>proc{|sql| sql =~ /a{51}/ ? {:v=>0} : {:v=>1}})
6
6
  def db.schema_parse_table(*) true; end
7
7
  def db.schema(t, *)
8
8
  t = t.first_source if t.is_a?(Sequel::Dataset)
9
9
  return [] if t != :test
10
10
  [[:id, {:primary_key=>true, :type=>:integer, :allow_null=>false}],
11
- [:name, {:primary_key=>false, :type=>:string, :allow_null=>false}],
11
+ [:name, {:primary_key=>false, :type=>:string, :allow_null=>false, :max_length=>50}],
12
12
  [:num, {:primary_key=>false, :type=>:integer, :allow_null=>true}],
13
13
  [:d, {:primary_key=>false, :type=>:date, :allow_null=>false}],
14
14
  [:nnd, {:primary_key=>false, :type=>:string, :allow_null=>false, :ruby_default=>'nnd'}]]
@@ -42,6 +42,10 @@ describe "Sequel::Plugins::AutoValidations" do
42
42
  @m.set(:d=>Date.today, :num=>1)
43
43
  @m.valid?.should == false
44
44
  @m.errors.should == {[:name, :num]=>["is already taken"]}
45
+
46
+ @m.set(:name=>'a'*51)
47
+ @m.valid?.should == false
48
+ @m.errors.should == {:name=>["is longer than 50 characters"]}
45
49
  end
46
50
 
47
51
  it "should handle databases that don't support index parsing" do
@@ -86,6 +90,13 @@ describe "Sequel::Plugins::AutoValidations" do
86
90
 
87
91
  @c.skip_auto_validations(:unique)
88
92
  @m.valid?.should == true
93
+
94
+ @m.set(:name=>'a'*51)
95
+ @m.valid?.should == false
96
+ @m.errors.should == {:name=>["is longer than 50 characters"]}
97
+
98
+ @c.skip_auto_validations(:max_length)
99
+ @m.valid?.should == true
89
100
  end
90
101
 
91
102
  it "should allow skipping all auto validations" do
@@ -95,6 +106,8 @@ describe "Sequel::Plugins::AutoValidations" do
95
106
  @m.valid?.should == true
96
107
  @m.set(:d=>'/', :num=>'a', :name=>'1')
97
108
  @m.valid?.should == true
109
+ @m.set(:name=>'a'*51)
110
+ @m.valid?.should == true
98
111
  end
99
112
 
100
113
  it "should work correctly in subclasses" do
@@ -110,6 +123,10 @@ describe "Sequel::Plugins::AutoValidations" do
110
123
  @m.set(:d=>Date.today, :num=>1)
111
124
  @m.valid?.should == false
112
125
  @m.errors.should == {[:name, :num]=>["is already taken"]}
126
+
127
+ @m.set(:name=>'a'*51)
128
+ @m.valid?.should == false
129
+ @m.errors.should == {:name=>["is longer than 50 characters"]}
113
130
  end
114
131
 
115
132
  it "should work correctly in STI subclasses" do
@@ -128,6 +145,10 @@ describe "Sequel::Plugins::AutoValidations" do
128
145
  @m.valid?.should == false
129
146
  @m.errors.should == {[:name, :num]=>["is already taken"]}
130
147
  @m.db.sqls.should == ["SELECT count(*) AS count FROM test WHERE ((name = '1') AND (num = 1)) LIMIT 1"]
148
+
149
+ @m.set(:name=>'a'*51)
150
+ @m.valid?.should == false
151
+ @m.errors.should == {:name=>["is longer than 50 characters"]}
131
152
  end
132
153
 
133
154
  it "should work correctly when changing the dataset" do
@@ -38,6 +38,7 @@ describe "NestedAttributes plugin" do
38
38
  @Artist.one_to_one :first_album, :class=>@Album, :key=>:artist_id
39
39
  @Album.many_to_one :artist, :class=>@Artist, :reciprocal=>:albums
40
40
  @Album.many_to_many :tags, :class=>@Tag, :left_key=>:album_id, :right_key=>:tag_id, :join_table=>:at
41
+ @Tag.many_to_many :albums, :class=>@Album, :left_key=>:tag_id, :right_key=>:album_id, :join_table=>:at
41
42
  @Artist.nested_attributes :albums, :first_album, :destroy=>true, :remove=>true
42
43
  @Artist.nested_attributes :concerts, :destroy=>true, :remove=>true
43
44
  @Album.nested_attributes :artist, :tags, :destroy=>true, :remove=>true
@@ -117,9 +118,12 @@ describe "NestedAttributes plugin" do
117
118
  end
118
119
 
119
120
  it "should add new objects to the cached association array as soon as the *_attributes= method is called" do
120
- a = @Artist.new({:name=>'Ar', :albums_attributes=>[{:name=>'Al', :tags_attributes=>[{:name=>'T'}]}]})
121
+ a = @Artist.new({:name=>'Ar', :first_album_attributes=>{:name=>'B'}, :albums_attributes=>[{:name=>'Al', :tags_attributes=>[{:name=>'T'}]}]})
121
122
  a.albums.should == [@Album.new(:name=>'Al')]
123
+ a.albums.first.artist.should == a
122
124
  a.albums.first.tags.should == [@Tag.new(:name=>'T')]
125
+ a.first_album.should == @Album.new(:name=>'B')
126
+ a.first_album.artist.should == a
123
127
  end
124
128
 
125
129
  it "should support creating new objects with composite primary keys" do
@@ -608,4 +612,31 @@ describe "NestedAttributes plugin" do
608
612
  proc{al.set(:tags_attributes=>[{:id=>30, :name=>'T2', :number=>3}])}.should raise_error(Sequel::Error)
609
613
  proc{al.set(:tags_attributes=>[{:name=>'T2', :number=>3}])}.should raise_error(Sequel::Error)
610
614
  end
615
+
616
+ it "should allow per-call options via the set_nested_attributes method" do
617
+ @Tag.columns :id, :name, :number
618
+ @Album.nested_attributes :tags
619
+
620
+ al = @Album.load(:id=>10, :name=>'Al')
621
+ t = @Tag.load(:id=>30, :name=>'T', :number=>10)
622
+ al.associations[:tags] = [t]
623
+ al.set_nested_attributes(:tags, [{:id=>30, :name=>'T2'}, {:name=>'T3'}], :fields=>[:name])
624
+ @db.sqls.should == []
625
+ al.save
626
+ check_sql_array("UPDATE albums SET name = 'Al' WHERE (id = 10)",
627
+ "UPDATE tags SET name = 'T2' WHERE (id = 30)",
628
+ "INSERT INTO tags (name) VALUES ('T3')",
629
+ ["INSERT INTO at (album_id, tag_id) VALUES (10, 1)", "INSERT INTO at (tag_id, album_id) VALUES (1, 10)"])
630
+ proc{al.set_nested_attributes(:tags, [{:id=>30, :name=>'T2', :number=>3}], :fields=>[:name])}.should raise_error(Sequel::Error)
631
+ proc{al.set_nested_attributes(:tags, [{:name=>'T2', :number=>3}], :fields=>[:name])}.should raise_error(Sequel::Error)
632
+ end
633
+
634
+ it "should have set_nested_attributes method raise error if called with a bad association" do
635
+ proc{@Album.load(:id=>10, :name=>'Al').set_nested_attributes(:tags2, [{:id=>30, :name=>'T2', :number=>3}], :fields=>[:name])}.should raise_error(Sequel::Error)
636
+ end
637
+
638
+ it "should have set_nested_attributes method raise error if called with an association that doesn't support nested attributes" do
639
+ @Tag.columns :id, :name, :number
640
+ proc{@Album.load(:id=>10, :name=>'Al').set_nested_attributes(:tags, [{:id=>30, :name=>'T2', :number=>3}], :fields=>[:name])}.should raise_error(Sequel::Error)
641
+ end
611
642
  end
@@ -77,4 +77,16 @@ describe "pg_static_cache_updater extension" do
77
77
  @db.fetch = {:v=>1234}
78
78
  proc{@db.listen_for_static_cache_updates(Class.new(Sequel::Model(@db[:table])))}.should raise_error(Sequel::Error)
79
79
  end
80
+
81
+ specify "#listen_for_static_cache_updates should handle a :before_thread_exit option" do
82
+ a = []
83
+ @db.listen_for_static_cache_updates([@model], :yield=>[nil, nil, 12345], :before_thread_exit=>proc{a << 1}).join
84
+ a.should == [1]
85
+ end
86
+
87
+ specify "#listen_for_static_cache_updates should call :before_thread_exit option even if listen raises an exception" do
88
+ a = []
89
+ @db.listen_for_static_cache_updates([@model], :yield=>[nil, nil, 12345], :after_listen=>proc{raise ArgumentError}, :before_thread_exit=>proc{a << 1}).join
90
+ a.should == [1]
91
+ end
80
92
  end
@@ -31,25 +31,25 @@ describe "Sequel::Plugins::PreparedStatementsAssociations" do
31
31
 
32
32
  specify "should run correct SQL for associations" do
33
33
  @Artist.load(:id=>1).albums
34
- @db.sqls.should == ["SELECT * FROM albums WHERE (albums.artist_id = 1) -- prepared"]
34
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE (albums.artist_id = 1) -- prepared"]
35
35
 
36
36
  @Artist.load(:id=>1).album
37
- @db.sqls.should == ["SELECT * FROM albums WHERE (albums.artist_id = 1) LIMIT 1 -- prepared"]
37
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE (albums.artist_id = 1) LIMIT 1 -- prepared"]
38
38
 
39
39
  @Album.load(:id=>1, :artist_id=>2).artist
40
- @db.sqls.should == ["SELECT * FROM artists WHERE (artists.id = 2) LIMIT 1 -- prepared"]
40
+ @db.sqls.should == ["SELECT id, id2 FROM artists WHERE (artists.id = 2) LIMIT 1 -- prepared"]
41
41
 
42
42
  @Album.load(:id=>1, :artist_id=>2).tags
43
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) WHERE (albums_tags.album_id = 1) -- prepared"]
43
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) WHERE (albums_tags.album_id = 1) -- prepared"]
44
44
 
45
45
  @Album.load(:id=>1, :artist_id=>2).tag
46
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) WHERE (albums_tags.album_id = 1) LIMIT 1 -- prepared"]
46
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) WHERE (albums_tags.album_id = 1) LIMIT 1 -- prepared"]
47
47
 
48
48
  @Artist.load(:id=>1).tags
49
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) WHERE (albums.artist_id = 1) -- prepared"]
49
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) WHERE (albums.artist_id = 1) -- prepared"]
50
50
 
51
51
  @Artist.load(:id=>1).tag
52
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) WHERE (albums.artist_id = 1) LIMIT 1 -- prepared"]
52
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) WHERE (albums.artist_id = 1) LIMIT 1 -- prepared"]
53
53
  end
54
54
 
55
55
  specify "should run correct SQL for composite key associations" do
@@ -63,25 +63,25 @@ describe "Sequel::Plugins::PreparedStatementsAssociations" do
63
63
  @Artist.one_through_many :tag, :clone=>:tags
64
64
 
65
65
  @Artist.load(:id=>1, :id2=>2).albums
66
- @db.sqls.should == ["SELECT * FROM albums WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) -- prepared"]
66
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) -- prepared"]
67
67
 
68
68
  @Artist.load(:id=>1, :id2=>2).album
69
- @db.sqls.should == ["SELECT * FROM albums WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) LIMIT 1 -- prepared"]
69
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) LIMIT 1 -- prepared"]
70
70
 
71
71
  @Album.load(:id=>1, :artist_id=>2, :artist_id2=>3).artist
72
- @db.sqls.should == ["SELECT * FROM artists WHERE ((artists.id = 2) AND (artists.id2 = 3)) LIMIT 1 -- prepared"]
72
+ @db.sqls.should == ["SELECT id, id2 FROM artists WHERE ((artists.id = 2) AND (artists.id2 = 3)) LIMIT 1 -- prepared"]
73
73
 
74
74
  @Album.load(:id=>1, :artist_id=>2, :id2=>3).tags
75
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) WHERE ((albums_tags.album_id = 1) AND (albums_tags.album_id2 = 3)) -- prepared"]
75
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) WHERE ((albums_tags.album_id = 1) AND (albums_tags.album_id2 = 3)) -- prepared"]
76
76
 
77
77
  @Album.load(:id=>1, :artist_id=>2, :id2=>3).tag
78
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) WHERE ((albums_tags.album_id = 1) AND (albums_tags.album_id2 = 3)) LIMIT 1 -- prepared"]
78
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) WHERE ((albums_tags.album_id = 1) AND (albums_tags.album_id2 = 3)) LIMIT 1 -- prepared"]
79
79
 
80
80
  @Artist.load(:id=>1, :id2=>2).tags
81
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) INNER JOIN albums ON ((albums.id = albums_tags.album_id) AND (albums.id2 = albums_tags.album_id2)) WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) -- prepared"]
81
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) INNER JOIN albums ON ((albums.id = albums_tags.album_id) AND (albums.id2 = albums_tags.album_id2)) WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) -- prepared"]
82
82
 
83
83
  @Artist.load(:id=>1, :id2=>2).tag
84
- @db.sqls.should == ["SELECT tags.* FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) INNER JOIN albums ON ((albums.id = albums_tags.album_id) AND (albums.id2 = albums_tags.album_id2)) WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) LIMIT 1 -- prepared"]
84
+ @db.sqls.should == ["SELECT tags.id, tags.id2 FROM tags INNER JOIN albums_tags ON ((albums_tags.tag_id = tags.id) AND (albums_tags.tag_id2 = tags.id2)) INNER JOIN albums ON ((albums.id = albums_tags.album_id) AND (albums.id2 = albums_tags.album_id2)) WHERE ((albums.artist_id = 1) AND (albums.artist_id2 = 2)) LIMIT 1 -- prepared"]
85
85
  end
86
86
 
87
87
  specify "should not run query if no objects can be associated" do
@@ -120,19 +120,19 @@ describe "Sequel::Plugins::PreparedStatementsAssociations" do
120
120
  @Album.dataset = @Album.dataset.where(:a=>2)
121
121
  @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id
122
122
  @Artist.load(:id=>1).albums
123
- @db.sqls.should == ["SELECT * FROM albums WHERE ((a = 2) AND (albums.artist_id = 1)) -- prepared"]
123
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE ((a = 2) AND (albums.artist_id = 1)) -- prepared"]
124
124
  end
125
125
 
126
126
  specify "should use a prepared statement if the :conditions association option" do
127
127
  @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id, :conditions=>{:a=>2}
128
128
  @Artist.load(:id=>1).albums
129
- @db.sqls.should == ["SELECT * FROM albums WHERE ((a = 2) AND (albums.artist_id = 1)) -- prepared"]
129
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE ((a = 2) AND (albums.artist_id = 1)) -- prepared"]
130
130
  end
131
131
 
132
132
  specify "should not use a prepared statement if :conditions association option uses an identifier" do
133
133
  @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id, :conditions=>{Sequel.identifier('a')=>2}
134
134
  @Artist.load(:id=>1).albums
135
- @db.sqls.should == ["SELECT * FROM albums WHERE ((a = 2) AND (albums.artist_id = 1)) -- prepared"]
135
+ @db.sqls.should == ["SELECT id, artist_id, id2, artist_id2 FROM albums WHERE ((a = 2) AND (albums.artist_id = 1)) -- prepared"]
136
136
  end
137
137
 
138
138
  specify "should run a regular query if :dataset option is used when defining the association" do