sequel 4.13.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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)"]