sequel 4.13.0 → 4.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +24 -0
  3. data/doc/active_record.rdoc +4 -4
  4. data/doc/advanced_associations.rdoc +2 -2
  5. data/doc/association_basics.rdoc +11 -11
  6. data/doc/cheat_sheet.rdoc +7 -7
  7. data/doc/core_extensions.rdoc +1 -1
  8. data/doc/dataset_filtering.rdoc +1 -1
  9. data/doc/extensions.rdoc +1 -1
  10. data/doc/migration.rdoc +3 -3
  11. data/doc/model_hooks.rdoc +1 -1
  12. data/doc/opening_databases.rdoc +4 -0
  13. data/doc/postgresql.rdoc +2 -2
  14. data/doc/prepared_statements.rdoc +1 -1
  15. data/doc/querying.rdoc +31 -31
  16. data/doc/release_notes/4.13.0.txt +1 -1
  17. data/doc/release_notes/4.14.0.txt +68 -0
  18. data/doc/schema_modification.rdoc +1 -1
  19. data/doc/sharding.rdoc +2 -2
  20. data/doc/sql.rdoc +1 -1
  21. data/doc/virtual_rows.rdoc +2 -2
  22. data/lib/sequel/adapters/jdbc/jtds.rb +4 -0
  23. data/lib/sequel/adapters/mysql.rb +18 -18
  24. data/lib/sequel/adapters/mysql2.rb +7 -7
  25. data/lib/sequel/adapters/shared/mysql.rb +15 -5
  26. data/lib/sequel/adapters/shared/postgres.rb +71 -58
  27. data/lib/sequel/adapters/shared/sqlite.rb +2 -2
  28. data/lib/sequel/ast_transformer.rb +1 -1
  29. data/lib/sequel/connection_pool/sharded_single.rb +8 -8
  30. data/lib/sequel/connection_pool/sharded_threaded.rb +8 -8
  31. data/lib/sequel/database/connecting.rb +1 -1
  32. data/lib/sequel/database/schema_generator.rb +12 -0
  33. data/lib/sequel/database/schema_methods.rb +8 -7
  34. data/lib/sequel/database/transactions.rb +1 -2
  35. data/lib/sequel/dataset/actions.rb +4 -4
  36. data/lib/sequel/dataset/graph.rb +4 -0
  37. data/lib/sequel/dataset/query.rb +18 -18
  38. data/lib/sequel/dataset/sql.rb +3 -3
  39. data/lib/sequel/extensions/_pretty_table.rb +1 -0
  40. data/lib/sequel/extensions/arbitrary_servers.rb +3 -2
  41. data/lib/sequel/extensions/columns_introspection.rb +1 -0
  42. data/lib/sequel/extensions/connection_validator.rb +1 -0
  43. data/lib/sequel/extensions/constraint_validations.rb +1 -0
  44. data/lib/sequel/extensions/current_datetime_timestamp.rb +1 -0
  45. data/lib/sequel/extensions/dataset_source_alias.rb +1 -0
  46. data/lib/sequel/extensions/date_arithmetic.rb +1 -0
  47. data/lib/sequel/extensions/empty_array_ignore_nulls.rb +1 -0
  48. data/lib/sequel/extensions/error_sql.rb +1 -0
  49. data/lib/sequel/extensions/eval_inspect.rb +1 -0
  50. data/lib/sequel/extensions/filter_having.rb +1 -0
  51. data/lib/sequel/extensions/from_block.rb +1 -0
  52. data/lib/sequel/extensions/graph_each.rb +1 -0
  53. data/lib/sequel/extensions/hash_aliases.rb +1 -0
  54. data/lib/sequel/extensions/looser_typecasting.rb +1 -0
  55. data/lib/sequel/extensions/meta_def.rb +1 -0
  56. data/lib/sequel/extensions/migration.rb +1 -0
  57. data/lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb +1 -0
  58. data/lib/sequel/extensions/named_timezones.rb +1 -0
  59. data/lib/sequel/extensions/null_dataset.rb +1 -0
  60. data/lib/sequel/extensions/pagination.rb +1 -0
  61. data/lib/sequel/extensions/pg_array.rb +2 -2
  62. data/lib/sequel/extensions/pg_array_ops.rb +1 -0
  63. data/lib/sequel/extensions/pg_enum.rb +1 -0
  64. data/lib/sequel/extensions/pg_hstore_ops.rb +1 -0
  65. data/lib/sequel/extensions/pg_json_ops.rb +1 -0
  66. data/lib/sequel/extensions/pg_loose_count.rb +1 -0
  67. data/lib/sequel/extensions/pg_range_ops.rb +1 -0
  68. data/lib/sequel/extensions/pg_row_ops.rb +1 -0
  69. data/lib/sequel/extensions/pg_static_cache_updater.rb +1 -0
  70. data/lib/sequel/extensions/pretty_table.rb +1 -0
  71. data/lib/sequel/extensions/query.rb +1 -0
  72. data/lib/sequel/extensions/query_literals.rb +1 -0
  73. data/lib/sequel/extensions/schema_caching.rb +1 -0
  74. data/lib/sequel/extensions/schema_dumper.rb +1 -1
  75. data/lib/sequel/extensions/select_remove.rb +1 -0
  76. data/lib/sequel/extensions/sequel_3_dataset_methods.rb +1 -0
  77. data/lib/sequel/extensions/server_block.rb +1 -0
  78. data/lib/sequel/extensions/set_overrides.rb +1 -0
  79. data/lib/sequel/extensions/split_array_nil.rb +1 -0
  80. data/lib/sequel/extensions/thread_local_timezones.rb +1 -0
  81. data/lib/sequel/extensions/to_dot.rb +1 -0
  82. data/lib/sequel/model/associations.rb +5 -5
  83. data/lib/sequel/model/base.rb +3 -3
  84. data/lib/sequel/plugins/association_proxies.rb +1 -1
  85. data/lib/sequel/plugins/caching.rb +8 -3
  86. data/lib/sequel/plugins/class_table_inheritance.rb +20 -11
  87. data/lib/sequel/plugins/composition.rb +18 -18
  88. data/lib/sequel/plugins/json_serializer.rb +3 -3
  89. data/lib/sequel/plugins/lazy_attributes.rb +23 -9
  90. data/lib/sequel/plugins/many_through_many.rb +21 -21
  91. data/lib/sequel/plugins/nested_attributes.rb +7 -3
  92. data/lib/sequel/plugins/pg_row.rb +1 -1
  93. data/lib/sequel/plugins/rcte_tree.rb +13 -13
  94. data/lib/sequel/plugins/sharding.rb +6 -6
  95. data/lib/sequel/plugins/timestamps.rb +4 -4
  96. data/lib/sequel/plugins/touch.rb +7 -7
  97. data/lib/sequel/plugins/tree.rb +1 -1
  98. data/lib/sequel/plugins/validation_class_methods.rb +36 -36
  99. data/lib/sequel/plugins/validation_helpers.rb +3 -4
  100. data/lib/sequel/plugins/xml_serializer.rb +29 -29
  101. data/lib/sequel/sql.rb +22 -11
  102. data/lib/sequel/version.rb +1 -1
  103. data/spec/adapters/postgres_spec.rb +10 -0
  104. data/spec/core/database_spec.rb +3 -4
  105. data/spec/core/dataset_spec.rb +16 -1
  106. data/spec/core/expression_filters_spec.rb +12 -0
  107. data/spec/core/object_graph_spec.rb +5 -0
  108. data/spec/core/placeholder_literalizer_spec.rb +8 -0
  109. data/spec/extensions/caching_spec.rb +18 -0
  110. data/spec/extensions/class_table_inheritance_spec.rb +34 -0
  111. data/spec/extensions/many_through_many_spec.rb +4 -0
  112. data/spec/extensions/nested_attributes_spec.rb +59 -4
  113. data/spec/extensions/pg_array_associations_spec.rb +5 -0
  114. data/spec/extensions/single_table_inheritance_spec.rb +23 -1
  115. data/spec/integration/plugin_test.rb +17 -0
  116. data/spec/model/eager_loading_spec.rb +8 -0
  117. metadata +4 -2
@@ -12,50 +12,50 @@ module Sequel
12
12
  # album = Album[1]
13
13
  # puts album.to_xml
14
14
  # # Output:
15
- # <?xml version="1.0"?>
16
- # <album>
17
- # <id>1</id>
18
- # <name>RF</name>
19
- # <artist_id>2</artist_id>
20
- # </album>
15
+ # # <?xml version="1.0"?>
16
+ # # <album>
17
+ # # <id>1</id>
18
+ # # <name>RF</name>
19
+ # # <artist_id>2</artist_id>
20
+ # # </album>
21
21
  #
22
22
  # You can provide options to control the XML output:
23
23
  #
24
24
  # puts album.to_xml(:only=>:name)
25
25
  # puts album.to_xml(:except=>[:id, :artist_id])
26
26
  # # Output:
27
- # <?xml version="1.0"?>
28
- # <album>
29
- # <name>RF</name>
30
- # </album>
27
+ # # <?xml version="1.0"?>
28
+ # # <album>
29
+ # # <name>RF</name>
30
+ # # </album>
31
31
  #
32
32
  # album.to_xml(:include=>:artist)
33
33
  # # Output:
34
- # <?xml version="1.0"?>
35
- # <album>
36
- # <id>1</id>
37
- # <name>RF</name>
38
- # <artist_id>2</artist_id>
39
- # <artist>
40
- # <id>2</id>
41
- # <name>YJM</name>
42
- # </artist>
43
- # </album>
34
+ # # <?xml version="1.0"?>
35
+ # # <album>
36
+ # # <id>1</id>
37
+ # # <name>RF</name>
38
+ # # <artist_id>2</artist_id>
39
+ # # <artist>
40
+ # # <id>2</id>
41
+ # # <name>YJM</name>
42
+ # # </artist>
43
+ # # </album>
44
44
  #
45
45
  # You can use a hash value with <tt>:include</tt> to pass options
46
46
  # to associations:
47
47
  #
48
48
  # album.to_xml(:include=>{:artist=>{:only=>:name}})
49
49
  # # Output:
50
- # <?xml version="1.0"?>
51
- # <album>
52
- # <id>1</id>
53
- # <name>RF</name>
54
- # <artist_id>2</artist_id>
55
- # <artist>
56
- # <name>YJM</name>
57
- # </artist>
58
- # </album>
50
+ # # <?xml version="1.0"?>
51
+ # # <album>
52
+ # # <id>1</id>
53
+ # # <name>RF</name>
54
+ # # <artist_id>2</artist_id>
55
+ # # <artist>
56
+ # # <name>YJM</name>
57
+ # # </artist>
58
+ # # </album>
59
59
  #
60
60
  # +to_xml+ also exists as a class and dataset method, both
61
61
  # of which return all objects in the dataset:
data/lib/sequel/sql.rb CHANGED
@@ -909,7 +909,7 @@ module Sequel
909
909
  # Return a +StringExpression+ representing the concatenation of the receiver
910
910
  # with the given argument.
911
911
  #
912
- # :x.sql_string + :y => # "x" || "y"
912
+ # :x.sql_string + :y # => "x" || "y"
913
913
  def +(ce)
914
914
  StringExpression.new(:'||', self, ce)
915
915
  end
@@ -984,14 +984,14 @@ module Sequel
984
984
  # and converts it to a +BooleanExpression+. The operator and args
985
985
  # used depends on the case of the right (2nd) argument:
986
986
  #
987
- # * 0..10 - left >= 0 AND left <= 10
988
- # * [1,2] - left IN (1,2)
989
- # * nil - left IS NULL
990
- # * true - left IS TRUE
991
- # * false - left IS FALSE
992
- # * /as/ - left ~ 'as'
993
- # * :blah - left = blah
994
- # * 'blah' - left = 'blah'
987
+ # 0..10 :: left >= 0 AND left <= 10
988
+ # [1,2] :: left IN (1,2)
989
+ # nil :: left IS NULL
990
+ # true :: left IS TRUE
991
+ # false :: left IS FALSE
992
+ # /as/ :: left ~ 'as'
993
+ # :blah :: left = blah
994
+ # 'blah' :: left = 'blah'
995
995
  #
996
996
  # If multiple arguments are given, they are joined with the op given (AND
997
997
  # by default, OR possible). If negate is set to true,
@@ -1022,7 +1022,7 @@ module Sequel
1022
1022
  when Regexp
1023
1023
  StringExpression.like(l, r)
1024
1024
  when DelayedEvaluation
1025
- Sequel.delay{from_value_pair(l, r.callable.call)}
1025
+ Sequel.delay{|ds| from_value_pair(l, r.call(ds))}
1026
1026
  when Dataset::PlaceholderLiteralizer::Argument
1027
1027
  r.transform{|v| from_value_pair(l, v)}
1028
1028
  else
@@ -1220,7 +1220,18 @@ module Sequel
1220
1220
  @callable = callable
1221
1221
  end
1222
1222
 
1223
- to_s_method :delayed_evaluation_sql, '@callable'
1223
+ # Call the underlying callable and return the result. If the
1224
+ # underlying callable only accepts a single argument, call it
1225
+ # with the given dataset.
1226
+ def call(ds)
1227
+ if @callable.respond_to?(:arity) && @callable.arity == 1
1228
+ @callable.call(ds)
1229
+ else
1230
+ @callable.call
1231
+ end
1232
+ end
1233
+
1234
+ to_s_method :delayed_evaluation_sql
1224
1235
  end
1225
1236
 
1226
1237
  # Represents an SQL function call.
@@ -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 = 13
6
+ MINOR = 14
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
@@ -1533,6 +1533,16 @@ describe "Postgres::Database functions, languages, schemas, and triggers" do
1533
1533
  @d.send(:drop_trigger_sql, :test, :identity, :if_exists=>true, :cascade=>true).should == 'DROP TRIGGER IF EXISTS identity ON "test" CASCADE'
1534
1534
  # Make sure if exists works
1535
1535
  @d.drop_trigger(:test, :identity, :if_exists=>true, :cascade=>true)
1536
+
1537
+ if @d.supports_trigger_conditions?
1538
+ @d.send(:create_trigger_sql, :test, :identity, :tf, :each_row=>true, :when=> {:new__name => 'b'}).should == %q{CREATE TRIGGER identity BEFORE INSERT OR UPDATE OR DELETE ON "test" FOR EACH ROW WHEN ("new"."name" = 'b') EXECUTE PROCEDURE tf()}
1539
+ @d.create_trigger(:test, :identity, :tf, :each_row=>true, :events => :update, :when=> {:new__name => 'b'})
1540
+ proc{@d[:test].filter(:name=>'a').update(:value=>nil)}.should_not raise_error
1541
+ @d[:test].filter(:name=>'a').all.should == [{:name=>'a', :value=>nil}]
1542
+ proc{@d[:test].filter(:name=>'a').update(:name=>'b')}.should raise_error(Sequel::DatabaseError)
1543
+ @d[:test].filter(:name=>'a').all.should == [{:name=>'a', :value=>nil}]
1544
+ @d.drop_trigger(:test, :identity)
1545
+ end
1536
1546
  end
1537
1547
  end
1538
1548
 
@@ -720,16 +720,15 @@ shared_examples_for "Database#transaction" do
720
720
  @db.sqls.should == ['BEGIN', 'DROP TABLE test;', 'ROLLBACK']
721
721
  end
722
722
 
723
- specify "should handle errors when sending ROLLBACK" do
723
+ specify "should raise original exception if there is an exception raised when rolling back" do
724
724
  ec = Class.new(StandardError)
725
725
  meta_def(@db, :database_error_classes){[ec]}
726
726
  meta_def(@db, :log_connection_execute){|c, sql| sql =~ /ROLLBACK/ ? raise(ec, 'bad') : super(c, sql)}
727
727
  begin
728
728
  @db.transaction{raise ArgumentError, 'asdf'}
729
- rescue Sequel::DatabaseError => e
729
+ rescue => e
730
730
  end
731
- e.should_not be_nil
732
- e.wrapped_exception.should be_a_kind_of(ec)
731
+ e.should be_a_kind_of(ArgumentError)
733
732
  @db.sqls.should == ['BEGIN']
734
733
  end
735
734
 
@@ -593,8 +593,18 @@ describe "Dataset#where" do
593
593
  "SELECT * FROM test WHERE (((name < 'b') AND (table.id = 1)) OR is_active(blah, xx, x.y_z))"
594
594
  end
595
595
 
596
- specify "should raise an error if an invalid argument is used" do
596
+ specify "should handle arbitrary objects" do
597
+ o = Object.new
598
+ def o.sql_literal(ds)
599
+ "foo"
600
+ end
601
+ @dataset.filter(o).sql.should == 'SELECT * FROM test WHERE foo'
602
+ end
603
+
604
+ specify "should raise an error if an numeric is used" do
597
605
  proc{@dataset.filter(1)}.should raise_error(Sequel::Error)
606
+ proc{@dataset.filter(1.0)}.should raise_error(Sequel::Error)
607
+ proc{@dataset.filter(BigDecimal.new('1.0'))}.should raise_error(Sequel::Error)
598
608
  end
599
609
 
600
610
  specify "should raise an error if a NumericExpression or StringExpression is used" do
@@ -3707,6 +3717,11 @@ describe "Sequel::Dataset#qualify" do
3707
3717
  ds.sql.should == 'SELECT t.* FROM t WHERE t.b'
3708
3718
  end
3709
3719
 
3720
+ specify "should handle SQL::DelayedEvaluations that take dataset arguments" do
3721
+ ds = @ds.filter(Sequel.delay{|ds| ds.first_source}).qualify
3722
+ ds.sql.should == 'SELECT t.* FROM t WHERE t.t'
3723
+ end
3724
+
3710
3725
  specify "should handle all other objects by returning them unchanged" do
3711
3726
  @ds.select("a").filter{a(3)}.filter('blah').order(Sequel.lit('true')).group(Sequel.lit('a > ?', 1)).having(false).qualify.sql.should == "SELECT 'a' FROM t WHERE (a(3) AND (blah)) GROUP BY a > 1 HAVING 'f' ORDER BY true"
3712
3727
  end
@@ -1116,6 +1116,12 @@ describe "Sequel.delay" do
1116
1116
  @o._a.should == 2
1117
1117
  end
1118
1118
 
1119
+ specify "should call the block with the current dataset if it accepts one argument" do
1120
+ ds = Sequel.mock[:b].where(Sequel.delay{|ds| ds.first_source})
1121
+ ds.sql.should == "SELECT * FROM b WHERE b"
1122
+ ds.from(:c).sql.should == "SELECT * FROM c WHERE c"
1123
+ end
1124
+
1119
1125
  specify "should have the condition specifier handling respect delayed evaluations" do
1120
1126
  ds = Sequel.mock[:b].where(:a=>Sequel.delay{@o.b})
1121
1127
  ds.sql.should == "SELECT * FROM b WHERE (a IS NULL)"
@@ -1125,6 +1131,12 @@ describe "Sequel.delay" do
1125
1131
  ds.sql.should == "SELECT * FROM b WHERE (a IN (1, 2))"
1126
1132
  end
1127
1133
 
1134
+ specify "should have the condition specifier handling call block with the current dataset if it accepts one argument" do
1135
+ ds = Sequel.mock[:b].where(:a=>Sequel.delay{|ds| ds.first_source})
1136
+ ds.sql.should == "SELECT * FROM b WHERE (a = b)"
1137
+ ds.from(:c).sql.should == "SELECT * FROM c WHERE (a = c)"
1138
+ end
1139
+
1128
1140
  specify "should raise if called without a block" do
1129
1141
  proc{Sequel.delay}.should raise_error(Sequel::Error)
1130
1142
  end
@@ -171,6 +171,11 @@ describe Sequel::Dataset, "graphing" do
171
171
  ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points INNER JOIN lines ON (lines.x = points.id)'
172
172
  end
173
173
 
174
+ it "should accept a :join_only option" do
175
+ ds = @ds1.graph(:lines, {:x=>:id}, :join_only=>true)
176
+ ds.sql.should == 'SELECT * FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
177
+ end
178
+
174
179
  it "should not select any columns from the graphed table if :select option is false" do
175
180
  ds = @ds1.graph(:lines, {:x=>:id}, :select=>false).graph(:graphs, :id=>:graph_id)
176
181
  ds.sql.should == 'SELECT points.id, points.x, points.y, graphs.id AS graphs_id, graphs.name, graphs.x AS graphs_x, graphs.y AS graphs_y, graphs.lines_x FROM points LEFT OUTER JOIN lines ON (lines.x = points.id) LEFT OUTER JOIN graphs ON (graphs.id = lines.graph_id)'
@@ -41,6 +41,14 @@ describe "Dataset::PlaceholderLiteralizer" do
41
41
  @db.sqls.should == ["SELECT s FROM items WHERE ((a = 1) AND (b = (c + 1)) AND (id = 1)) HAVING h", "SELECT s2 FROM items WHERE ((a = 2) AND (b = (d + 1)) AND (id = 2)) HAVING h2"]
42
42
  end
43
43
 
44
+ specify "should handle calls with placeholders and delayed arguments that take dataset argument" do
45
+ d = @ds.select(Sequel.delay{|ds| ds.first_source})
46
+ loader = @c.loader(d){|pl, ds| ds.where(:a=>pl.arg).where(:b=>Sequel.+(pl.arg, 1)).where(pl.arg)}
47
+ loader.first(1, :c, :id=>1).should == @h
48
+ loader.first(2, :d, :id=>2).should == @h
49
+ @db.sqls.should == ["SELECT items FROM items WHERE ((a = 1) AND (b = (c + 1)) AND (id = 1))", "SELECT items FROM items WHERE ((a = 2) AND (b = (d + 1)) AND (id = 2))"]
50
+ end
51
+
44
52
  specify "should handle calls with a placeholders used as filter arguments" do
45
53
  loader = @c.loader(@ds){|pl, ds| ds.where(pl.arg)}
46
54
  loader.first(:id=>1).should == @h
@@ -249,4 +249,22 @@ describe Sequel::Model, "caching" do
249
249
  @c.cache_delete_pk(1).should == nil
250
250
  @c.cache_get_pk(1).should == nil
251
251
  end
252
+
253
+ it "should support overriding the cache key prefix" do
254
+ c2 = Class.new(@c)
255
+ def c2.cache_key_prefix; "ceetwo" end
256
+ c3 = Class.new(c2)
257
+ @c.cache_key(:id).should_not == c2.cache_key(:id)
258
+ c2.cache_key(:id).should == c3.cache_key(:id)
259
+
260
+ @c[1]
261
+ c2.cache_get_pk(1).should == nil
262
+ m = c2[1]
263
+ c2.cache_get_pk(1).values.should == @c[1].values
264
+ c3.cache_get_pk(1).values.should == m.values
265
+
266
+ m.name << m.name
267
+ m.save
268
+ c2[1].values.should == c3[1].values
269
+ end
252
270
  end
@@ -78,6 +78,17 @@ describe "class_table_inheritance plugin" do
78
78
  Manager.all.collect{|x| x.class}.should == [Manager, Executive]
79
79
  end
80
80
 
81
+ it "should have refresh return all columns in subclass after loading from superclass" do
82
+ Employee.dataset._fetch = [{:id=>1, :name=>'A', :kind=>'Executive'}]
83
+ Executive.instance_dataset._fetch = [{:id=>1, :name=>'A', :kind=>'Executive', :num_staff=>3, :num_managers=>2}]
84
+ a = Employee.first
85
+ a.class.should == Executive
86
+ a.values.should == {:id=>1, :name=>'A', :kind=>'Executive'}
87
+ a.refresh.values.should == {:id=>1, :name=>'A', :kind=>'Executive', :num_staff=>3, :num_managers=>2}
88
+ @db.sqls.should == ["SELECT employees.id, employees.name, employees.kind FROM employees LIMIT 1",
89
+ "SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id) WHERE (executives.id = 1) LIMIT 1"]
90
+ end
91
+
81
92
  it "should return rows with the current class if cti_key is nil" do
82
93
  Employee.plugin(:class_table_inheritance)
83
94
  Employee.dataset._fetch = [{:kind=>'Employee'}, {:kind=>'Manager'}, {:kind=>'Executive'}, {:kind=>'Staff'}]
@@ -171,6 +182,29 @@ describe "class_table_inheritance plugin" do
171
182
  m.values.should == {:id=>1, :name=>'J', :kind=>'Executive', :num_staff=>2, :num_managers=>3}
172
183
  end
173
184
 
185
+ it "should lazily load columns in middle classes correctly when loaded from parent class" do
186
+ Employee.dataset._fetch = {:id=>1, :kind=>'Executive'}
187
+ Manager.dataset._fetch = {:num_staff=>2}
188
+ e = Employee[1]
189
+ e.should be_a_kind_of(Executive)
190
+ @db.sqls.should == ["SELECT employees.id, employees.name, employees.kind FROM employees WHERE (id = 1) LIMIT 1"]
191
+ e.num_staff.should == 2
192
+ @db.sqls.should == ["SELECT managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id) WHERE (managers.id = 1) LIMIT 1"]
193
+ end
194
+
195
+ it "should eagerly load lazily columns in subclasses when loaded from parent class" do
196
+ Employee.dataset._fetch = {:id=>1, :kind=>'Executive'}
197
+ Manager.dataset._fetch = {:id=>1, :num_staff=>2}
198
+ Executive.dataset._fetch = {:id=>1, :num_managers=>3}
199
+ e = Employee.all.first
200
+ e.should be_a_kind_of(Executive)
201
+ @db.sqls.should == ["SELECT employees.id, employees.name, employees.kind FROM employees"]
202
+ e.num_staff#.should == 2
203
+ @db.sqls.should == ["SELECT managers.id, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id) WHERE (managers.id IN (1))"]
204
+ e.num_managers#.should == 3
205
+ @db.sqls.should == ['SELECT executives.id, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id) WHERE (executives.id IN (1))']
206
+ end
207
+
174
208
  it "should include schema for columns for tables for ancestor classes" do
175
209
  Employee.db_schema.should == {:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}}
176
210
  Manager.db_schema.should == {:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :num_staff=>{:type=>:integer}}
@@ -872,6 +872,10 @@ describe "many_through_many eager loading methods" do
872
872
  @c1.association_join(:tags).sql.should == "SELECT * FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) INNER JOIN albums ON (albums.id = albums_artists.album_id) INNER JOIN albums_tags ON (albums_tags.album_id = albums.id) INNER JOIN tags ON (tags.id = albums_tags.tag_id)"
873
873
  end
874
874
 
875
+ it "should support custom selects when using association_join" do
876
+ @c1.select{a(b)}.association_join(:tags).sql.should == "SELECT a(b) FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) INNER JOIN albums ON (albums.id = albums_artists.album_id) INNER JOIN albums_tags ON (albums_tags.album_id = albums.id) INNER JOIN tags ON (tags.id = albums_tags.tag_id)"
877
+ end
878
+
875
879
  it "should eagerly graph a single many_through_many association" do
876
880
  a = @c1.eager_graph(:tags).all
877
881
  a.should == [@c1.load(:id=>1)]
@@ -36,12 +36,16 @@ describe "NestedAttributes plugin" do
36
36
  @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id
37
37
  @Artist.one_to_many :concerts, :class=>@Concert, :key=>:artist_id
38
38
  @Artist.one_to_one :first_album, :class=>@Album, :key=>:artist_id
39
+ @Artist.one_to_one :first_concert, :class=>@Concert, :key=>:artist_id
40
+ @Concert.one_to_many :albums, :class=>@Album, :key=>:artist_id, :primary_key=>:artist_id
39
41
  @Album.many_to_one :artist, :class=>@Artist, :reciprocal=>:albums
40
42
  @Album.many_to_many :tags, :class=>@Tag, :left_key=>:album_id, :right_key=>:tag_id, :join_table=>:at
41
43
  @Tag.many_to_many :albums, :class=>@Album, :left_key=>:tag_id, :right_key=>:album_id, :join_table=>:at
42
44
  @Artist.nested_attributes :albums, :first_album, :destroy=>true, :remove=>true
43
45
  @Artist.nested_attributes :concerts, :destroy=>true, :remove=>true
44
46
  @Album.nested_attributes :artist, :tags, :destroy=>true, :remove=>true
47
+ @Artist.nested_attributes :first_concert
48
+ @Concert.nested_attributes :albums
45
49
  @db.sqls
46
50
  end
47
51
 
@@ -76,7 +80,7 @@ describe "NestedAttributes plugin" do
76
80
  @Album.class_eval do
77
81
  plugin :validation_helpers
78
82
  def validate
79
- validates_presence :artist_id
83
+ validates_integer :artist_id
80
84
  super
81
85
  end
82
86
  end
@@ -96,6 +100,55 @@ describe "NestedAttributes plugin" do
96
100
  ["INSERT INTO albums (artist_id, name) VALUES (1, 'Al')", "INSERT INTO albums (name, artist_id) VALUES ('Al', 1)"])
97
101
  end
98
102
 
103
+ it "should support creating new one_to_many and one_to_one objects with composite keys with presence validations on the foreign key" do
104
+ insert = nil
105
+ @Album.class_eval do
106
+ plugin :validation_helpers
107
+ def validate
108
+ validates_integer :artist_id
109
+ super
110
+ end
111
+ end
112
+ @Concert.class_eval do
113
+ define_method :_insert do
114
+ insert = values.dup
115
+ end
116
+ def before_create # Have to define the CPK somehow.
117
+ self.tour = 'To'
118
+ self.date = '2004-04-05'
119
+ super
120
+ end
121
+ def after_create
122
+ super
123
+ self.artist_id = 3
124
+ end
125
+ end
126
+
127
+ c = @Concert.new(:playlist=>'Pl')
128
+ @db.sqls.should == []
129
+ c.albums_attributes = [{:name=>'Al'}]
130
+ c.save
131
+ insert.should == {:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl'}
132
+ check_sql_array(["INSERT INTO albums (name, artist_id) VALUES ('Al', 3)", "INSERT INTO albums (artist_id, name) VALUES (3, 'Al')"])
133
+
134
+ @Concert.class_eval do
135
+ plugin :validation_helpers
136
+ def validate
137
+ validates_integer :artist_id
138
+ super
139
+ end
140
+ end
141
+
142
+ a = @Artist.new(:name=>'Ar')
143
+ a.id = 1
144
+ a.first_concert_attributes = {:playlist=>'Pl'}
145
+ @db.sqls.should == []
146
+ a.save
147
+ check_sql_array(["INSERT INTO artists (name, id) VALUES ('Ar', 1)", "INSERT INTO artists (id, name) VALUES (1, 'Ar')"],
148
+ "UPDATE concerts SET artist_id = NULL WHERE (artist_id = 1)")
149
+ insert.should == {:tour=>'To', :date=>'2004-04-05', :artist_id=>1, :playlist=>'Pl'}
150
+ end
151
+
99
152
  it "should should not remove existing values from object when validating" do
100
153
  @Artist.one_to_one :first_album, :class=>@Album, :key=>:id
101
154
  @Artist.nested_attributes :first_album
@@ -130,7 +183,7 @@ describe "NestedAttributes plugin" do
130
183
  insert = nil
131
184
  @Concert.class_eval do
132
185
  define_method :_insert do
133
- insert = values
186
+ insert = values.dup
134
187
  end
135
188
  def before_create # Have to define the CPK somehow.
136
189
  self.tour = 'To'
@@ -151,7 +204,7 @@ describe "NestedAttributes plugin" do
151
204
  @Album.class_eval do
152
205
  unrestrict_primary_key
153
206
  define_method :_insert do
154
- insert = values
207
+ insert = values.dup
155
208
  end
156
209
  end
157
210
  a = @Artist.new({:name=>'Ar', :albums_attributes=>[{:id=>7, :name=>'Al'}]})
@@ -166,7 +219,7 @@ describe "NestedAttributes plugin" do
166
219
  @Artist.nested_attributes :concerts, :unmatched_pk=>:create
167
220
  @Concert.class_eval do
168
221
  define_method :_insert do
169
- insert = values
222
+ insert = values.dup
170
223
  end
171
224
  end
172
225
  a = @Artist.new({:name=>'Ar', :concerts_attributes=>[{:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl'}]})
@@ -271,6 +324,7 @@ describe "NestedAttributes plugin" do
271
324
  ar = @Artist.load(:id=>20, :name=>'Ar')
272
325
  ar.associations[:albums] = [al]
273
326
  ar.set(:albums_attributes=>[{:id=>10, :_remove=>'t'}])
327
+ ar.associations[:albums].should == []
274
328
  @db.sqls.should == []
275
329
  @Album.dataset._fetch = {:id=>1}
276
330
  ar.save
@@ -284,6 +338,7 @@ describe "NestedAttributes plugin" do
284
338
  t = @Tag.load(:id=>20, :name=>'T')
285
339
  a.associations[:tags] = [t]
286
340
  a.set(:tags_attributes=>[{:id=>20, :_remove=>true}])
341
+ a.associations[:tags].should == []
287
342
  @db.sqls.should == []
288
343
  a.save
289
344
  @db.sqls.should == ["DELETE FROM at WHERE ((album_id = 10) AND (tag_id = 20))", "UPDATE albums SET name = 'Al' WHERE (id = 10)"]